This page is for engineers moving into DevSecOps or security-focused platform roles, from a first security job to a senior lead. Most rounds start with shift-left and the difference between SAST, DAST and SCA, then go deep on secrets, container images, SBOMs and supply-chain attacks, IaC scanning and policy as code. Senior rounds test judgement: which findings break the build, how exceptions work, and how you handle a leaked key. Each question shows what the interviewer is checking, the shape of a strong answer and a short answer to say out loud. Practise them, then swap in your own stories.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Idea: find security problems as early as possible, when they are cheapest and quickest to fix.
In practice: checks in the editor, a pre-commit hook for secrets, scan results as pull request comments, secure templates.
Balance: early checks do not replace runtime monitoring; you still watch production.
"Shift-left means moving security checks earlier in the life of a change, so a problem is found while the developer still has the code open, not weeks later in a pen test. In a normal day that looks like a plugin in the editor that flags an unsafe function, a pre-commit hook that stops a secret before it's committed, and a pull request that gets comments from the static analysis and dependency scanners within a few minutes. It also means secure defaults, like a service template that already has sensible headers and a non-root container. The point is that the feedback is fast, specific and in the tools they already use. What it doesn't mean is handing every security decision to developers or dropping runtime monitoring. Some issues only show up in a running system, so you still check the right side too."
Describing shift-left as simply running more scanners, with no thought for speed, noise or who fixes the findings.
End gate: one review before release; slow, late findings, security seen as a blocker.
DevSecOps: automated checks on every change, security owned by the team that builds the service.
Security team's new role: builds guardrails, tools and paved roads, and handles the hard cases.
"With an end gate, the team builds for weeks and then a security reviewer looks at the release just before it ships. Findings come late, fixes are rushed, and security becomes the team that says no. That model also can't keep up when a team deploys many times a day. DevSecOps spreads the work out. Automated checks run on every change, the team that owns the service owns its security findings, and fixes happen in the normal flow of work. The security team doesn't disappear. It builds the guardrails, like pipeline scanners, secure templates and deploy-time policies, sets the rules for what blocks a release, trains security champions in each team, and spends its human time on the things tools can't judge, like design reviews for risky features."
Saying DevSecOps means there is no longer a security team, or that tools replace human design review.
Before and at pull request: secrets hook, fast SAST and SCA on the change, IaC scan.
Build: image scan, SBOM, image signing and build provenance.
Pre-production: DAST against a deployed test environment.
Deploy and runtime: admission policy checks signatures, continuous rescans as new CVEs appear.
"I'd start on the developer's machine with a pre-commit secrets hook, because a secret should never reach the repository. On the pull request I'd run the fast checks: SAST on the changed code, SCA on the dependency files, a secrets scan and an IaC scan, all posting comments inline. At build time I'd scan the final container image, generate an SBOM, sign the image by its digest and record provenance about how it was built. Once it's deployed to a test environment, DAST runs against the live app, with a quick passive scan on every deploy and a deeper one nightly. At deploy time an admission policy checks the image is signed by our pipeline and meets our rules. After that, I keep rescanning what's running, because a new CVE can appear in an image that was clean yesterday."
Putting every scan in one long blocking stage, or stopping at the build and ignoring what is already running.
Outcomes: time to fix by severity, findings that escaped to production, secrets that reached a remote.
Coverage: share of repos and images with the core checks turned on.
Health: scan time added to pipelines, false positive rate, exceptions past expiry.
Avoid: raw finding counts and number of scans run.
"I'd look at outcomes first. How long does it take to fix a critical or high finding, from detection to deploy, and is that getting shorter? How many issues are found in production or by pen testers that our pipeline should have caught? How many secrets reach the remote repository instead of being stopped locally? Then coverage: how many repositories and images actually have the core checks switched on, because a great scanner on a third of the estate is a gap. And the developer side: how many minutes the checks add to a pipeline, how often a finding turns out to be false, and how many exceptions are past their expiry. I'd avoid raw finding counts, because they go up when you add a scanner and down when people suppress things, and number of scans run, which only measures activity."
Reporting the number of scans run or the total finding count as proof of success.
SAST: reads your own source code without running it; finds unsafe patterns like injection or weak crypto.
DAST: attacks the running app from outside; finds runtime and configuration issues.
SCA: checks your third-party dependencies against known vulnerabilities and licences.
"SAST is static analysis. It reads the code we wrote without running it and flags patterns like building a SQL query from user input or using a weak hash. It's fast and points to the exact line, but it can't see how the app is configured when it runs, and it can be noisy. DAST is the opposite: it treats the running app as a black box and sends real requests, so it catches things like missing security headers, a debug page left open or an injection that only shows up once everything is wired together. It can't tell you which line caused it. SCA looks at the open-source libraries we pull in, often a large share of what we ship, and matches their versions against known vulnerabilities and licence rules. None of our code has to be wrong for SCA to find a critical issue."
Mixing up SCA with SAST, or claiming one tool type covers everything.
Baseline: record existing findings as debt; only new findings show on pull requests.
Tune: turn off noisy rules, mark confirmed false positives, focus on high-confidence rules.
Triage the backlog: fix by severity and exposure, with owners and dates.
"First I'd stop showing developers the whole pile. I'd take a baseline of what exists today and treat it as tracked debt, so on pull requests they only see findings their change introduced. Then I'd tune. I'd go through the rules producing the most findings, sample them, and if a rule is mostly wrong for our code, I turn it off or narrow it. I'd start with a small set of high-confidence rules, like injection and hard-coded credentials, and add more once people trust the tool. Suppressions go in with a short reason so they can be reviewed later. For the old backlog, I'd sort by severity and whether the code is reachable from the internet, and work through the top items with the owning teams. A scanner that is right nine times out of ten gets fixed; one that is right once in ten gets muted."
Blocking every build on all existing findings from day one, or blaming developers for ignoring a noisy tool.
Where: against a temporary or staging environment after deploy, never production by default.
Two depths: a quick passive baseline per deploy, a full active scan on a schedule.
Common pain: authentication, a crawler that misses API routes, state changes from active attacks.
"I run DAST after the app is deployed to a test environment, either a short-lived one per branch or a shared staging stack. On every deploy I run a baseline scan, which mostly watches responses passively and finishes in a few minutes. The full active scan, which actually sends attack payloads, runs nightly because it can take much longer. The usual problems are practical. If the scanner can't log in, it only tests the login page, so I give it a test account and a scripted login. A crawler also misses API endpoints, so I feed it the OpenAPI spec. Active scans submit forms and can fill a database with junk or trigger emails, so the environment needs throwaway data. And I avoid pointing an active scan at production unless everyone has agreed on the scope and timing."
Running an active scan against production without agreement, or assuming an unauthenticated scan covers the app.
Find the path: use the dependency tree to see which direct dependency pulls it in.
Judge exposure: is the vulnerable function used, is the input attacker-controlled, is it exploited in the wild?
Fix: upgrade the parent, or override the transitive version; record a reasoned exception if not affected.
"A library I don't import can still run in my process, because one of my direct dependencies pulls it in. So first I'd find the path with the dependency tree, like npm ls or mvn dependency:tree, to see who brings it in and at which version. Then I'd judge exposure. I read the advisory to see which function or configuration is vulnerable, and check whether our code path ever reaches it with input an attacker controls. I also look at whether it's being exploited in the wild. For the fix, the cleanest route is upgrading the direct dependency to a version that pulls a patched copy. If that isn't out yet, most package managers let me force the transitive version, like overrides in npm or dependency management in Maven, and I run the tests. If we're truly not affected, I record that reasoning with an expiry date rather than just silencing it."
# Who pulls in the vulnerable package, and at what version?
npm ls minimist
# Maven equivalent, filtered to one artifact
mvn dependency:tree -Dincludes=org.yaml:snakeyaml
Saying a transitive dependency can't affect you because you never import it.
Pick the pattern: one clear unsafe call, based on a real bug or review finding.
Write the rule: match on code structure, not plain text, with a clear message and fix.
Test and roll out: positive and negative test files, warn first, then block.
"Say we kept finding subprocess calls with shell equals true in our Python services, which opens the door to command injection when any part of the command comes from input. I'd write a rule in a structure-aware tool like Semgrep, so it matches the call itself and not just text in a comment. The rule names the pattern, gives a message that says what to do instead, which is pass the command as a list, and sets a severity. I'd keep a small test file with code that should match and code that shouldn't, and run the rule against it in the rule repo's own pipeline so a later edit can't silently break it. Then I'd run it across all repos in report-only mode, look at the hits, fix the false positives, and only after that make it block new findings on pull requests."
rules:
- id: python-subprocess-shell-true
languages: [python]
severity: ERROR
message: Avoid shell=True in subprocess calls. Pass the command as a list of arguments.
pattern-either:
- pattern: subprocess.run(..., shell=True, ...)
- pattern: subprocess.Popen(..., shell=True, ...)
- pattern: subprocess.call(..., shell=True, ...)
- pattern: subprocess.check_output(..., shell=True, ...)
Writing a grep-style text match with no tests and making it block every build on day one.
Finding: which check, what the flaw was and why it mattered.
Fix: how it was confirmed and fixed with the owning team.
Lasting change: a rule, template or training that stops the same class of bug.
"In a project at my last company, the SAST check flagged a new report endpoint that built a SQL query by joining a sort column from the request straight into the string. The developer had used parameters for the values, but you can't bind a column name, so he'd concatenated that one part. I checked it on a test environment and confirmed that a crafted sort parameter changed the query. We fixed it with an allow-list of the column names the report could sort by. The more useful part came after. I searched the codebase for the same pattern and found two older endpoints doing the same thing, which we fixed too. Then I added a custom rule for dynamic column names, and a short note to our code review checklist, since parameterised queries were giving people a false sense of safety."
A vague story with no technical detail about the flaw, or claiming credit for the whole fix alone.
Pre-commit: fastest feedback, but it's optional and can be bypassed.
Server side: push protection or a pipeline scan that everyone passes through.
History: a one-off and periodic scan of all commits, since old secrets may still be valid.
"I'd use all three, because each one covers the others' gaps. A pre-commit hook with a tool like gitleaks catches a key before it's ever committed, which is the cheapest place, but it lives on the developer's laptop, so it may not be installed and anyone can skip it with a flag. So I also need a server-side check that nobody can skip: push protection on the code host, or a secrets scan in the pipeline on every pull request. Neither of those looks backwards, though, and a key committed three years ago is still in the history and may still work. So when I start with a repository I scan the full history once, and then schedule it again periodically because detection rules improve over time. Anything found gets rotated, not just deleted."
# Scan the full git history of the current repository
gitleaks git --redact -v .
# Older releases use: gitleaks detect --source . --redact
Relying only on a pre-commit hook, or treating a found secret as fixed once it's removed from the latest commit.
Contain: revoke or rotate the key now; the commit alone is no protection.
Investigate: check the provider's logs for use of the key since it was pushed; who could see the repo.
Clean up: update every service that used it, optionally purge from history, write it up.
Prevent: find why it happened and fix the process, not the person.
"First I treat the key as stolen, even though it's been an hour and the repo is internal. I'd get the key's owner to create a new key, deploy it to the services that need it from our secrets manager, and then revoke the old one, in that order if we can do it quickly, so we don't take payments down. If there's any sign of misuse, I revoke straight away and accept a short outage. Next I check the provider's logs for any calls made with the old key since the push, from addresses we don't recognise, and I check who had access to that branch, including forks and CI logs. Then the cleanup: purge it from history if policy asks, and close the alert with notes. Finally I talk to the developer without blame, find out why the key was in the code, and fix that, maybe with a pre-commit hook and a proper way to load secrets locally."
Deleting the commit or making the repo private and calling it done, without rotating the key.
History: the old commit still holds the password; anyone can check it out.
Copies: clones, forks, CI caches and logs already have it; rewriting history cannot reach them.
Fix: change the password first, then clean up history if needed.
"Git keeps every version, so the password is still sitting in the earlier commit. Anyone with read access can look at the history or check out that commit and read it. Even if we rewrite history and force-push, that only changes the server copy. Every clone made in the meantime, any fork, the CI runner's checkout, build logs and the code host's cached views may still have it. And if the repo was ever public, automated scrapers look for exactly this and can find a key very quickly. So the only thing that actually closes the risk is to change the password, update whatever uses it, and check the logs for any use of the old one. Cleaning the history afterwards is good hygiene, so the next scan is quiet, but it's the second step, not the fix."
Saying a force-push or a private repo removes the need to change the password.
Token: the CI platform issues a short-lived signed OIDC token for each job.
Trust: the cloud trusts that issuer and only accepts tokens with the right repo, branch or environment claims.
Exchange: the job swaps the token for temporary cloud credentials limited to a deploy role.
"Most CI platforms and the big clouds support OIDC federation, so there's no stored key at all. When a job runs, the CI platform issues it a short-lived token signed by the platform, with claims saying which repository, branch or environment it's running for. On the cloud side, I set up a trust relationship with that issuer and a role the pipeline can assume. The important part is the condition on that trust: only accept tokens where the subject is our repo and the main branch or the production environment, otherwise any repo on the same platform could try to assume it. The job exchanges the token for temporary credentials that expire quickly, and the role only has the permissions the deploy needs. So there's nothing long-lived to leak, rotate or find in a log later."
Saying the fix is to store the key as an encrypted CI secret and rotate it more often, with no mention of short-lived credentials.
Build: stops a bad image before it's pushed; the developer gets feedback on the change.
Registry: rescans stored images as new CVEs are published.
Deploy and runtime: blocks unscanned or unsigned images, and tracks what is actually running.
"I'd scan at all three points because they answer different questions. At build time the pipeline scans the final image, including OS packages and language libraries, and fails on the issues we've agreed should block. That gives the developer feedback while the change is fresh. But the vulnerability database updates every day, so an image that was clean last month can have a critical CVE today. That's where registry rescans come in: the registry or a scheduled job rescans stored images and raises new findings against images still in use. At deploy time an admission check makes sure only images that came through our pipeline, scanned and signed, can run at all. And I want an inventory of what's actually running in each cluster, because that's the list that matters when a new critical CVE lands, not every old tag in the registry."
Treating a passing build-time scan as permanent proof the image is safe.
Proves: this exact image digest was signed by our pipeline's identity and hasn't changed since.
Does not prove: that the image is free of vulnerabilities.
Enforce: sign by digest in CI; an admission controller verifies identity and signature before pods run.
"Signing proves two things: who produced this image, and that it hasn't changed since. It says nothing about whether the image is free of vulnerabilities; a signed image can still be full of CVEs. In the pipeline, after the build and scan, I sign the image by its digest, not its tag, because tags can be moved. With a tool like Cosign you can use keyless signing, where the CI job's OIDC identity gets a short-lived certificate and the signature is recorded in a public transparency log, so there's no long-lived signing key to protect. At deploy time an admission controller in the cluster checks each image: is there a valid signature, and was it made by our pipeline's identity, say this repository's release workflow? If not, the pod is refused. That stops someone pushing an image straight to the registry and running it."
IMAGE="registry.example.com/shop/api@sha256:<digest>"
cosign sign --yes "$IMAGE"
cosign verify "$IMAGE" \
--certificate-identity-regexp '^https://github.com/example/shop/\.github/workflows/release\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Saying a signed image means the image is secure or vulnerability-free.
Shrink the surface: approved minimal or distroless bases, owned and rebuilt on a schedule.
Block what's fixable: fail on critical and high with a fix available; track the rest.
Risk signals: exploited in the wild, reachable package, internet-facing service.
Timelines: a fix window per severity, with time-boxed exceptions.
"I'd start by reducing the surface. A platform team owns a small set of approved base images, ideally minimal or distroless, and rebuilds them automatically on a schedule and whenever a fix lands, so teams pick up patches by rebuilding rather than chasing packages. Then the gate itself: builds fail on critical and high findings that have a fix available, because those are actionable today. Findings with no fix don't block, but they're tracked, and we re-evaluate them when a fix is published. I'd raise the priority of anything known to be exploited in the wild or sitting in an internet-facing service, and lower it where the package isn't used at runtime, recorded with a short statement so the next scan agrees. Each severity gets a fix window, and exceptions have an owner and an expiry. What I track is time to fix, not the total CVE count."
Demanding zero CVEs in every image, or giving up and allowing everything because some CVEs have no fix.
Secrets: a token in ENV is stored in the image and its history.
Base and dependencies: unpinned latest tag, npm install instead of a locked install, dev dependencies shipped.
Runtime: runs as root; COPY of everything can pull in .env and .git.
"The worst problem is the token in ENV. It's baked into the image and its history, so anyone who can pull the image can read it. It should be passed as a build secret that's only mounted for the install step, and that token should be rotated now. Next, node:latest isn't pinned, so the same Dockerfile can build a different image tomorrow. I'd pin a specific version, ideally by digest. COPY with a dot copies the whole folder, which can drag in .env files and the .git directory, so I'd want a .dockerignore. npm install can pull newer versions and rewrite the lockfile if it's out of step with package.json; npm ci installs exactly what's locked and fails if the two disagree. And it runs as root, so I'd add a USER line. I'd also suggest a multi-stage build with a slim runtime image, so build tools and dev dependencies don't ship."
FROM node:latest
ENV NPM_TOKEN=npm_live_token_here
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "server.js"]
Missing the baked-in token, or saying deleting it in a later layer fixes the leak.
What: a machine-readable list of every component and version inside a build.
Formats: SPDX and CycloneDX are the common ones.
Use: stored with each release; searched when a new CVE lands; shared with customers who ask.
"An SBOM, a software bill of materials, is a machine-readable list of everything inside a piece of software: every library, OS package and version, and ideally where each came from. The common formats are SPDX and CycloneDX. I generate it in the pipeline from the final build output, like the container image, because that's what actually ships, and store it alongside the release or attach it to the image. The real value shows up on a bad day. When a critical CVE is published in a popular library, instead of asking every team to check, we search the SBOMs of everything deployed and know within minutes which services and versions are affected. Customers and regulators in some sectors also ask for them. An SBOM doesn't fix anything by itself; it's only useful if it's kept current and someone can query it."
Describing an SBOM as a vulnerability report, or as a one-time document made for an audit.
Packages: typosquatting and malicious updates from a hijacked maintainer account.
Resolution: dependency confusion between internal and public names.
Build: a compromised build server or CI step injecting code into the artifact.
Defences: lockfiles and pinning, private registry proxy, isolated builds, signing and provenance.
"The first is typosquatting: publishing a package whose name is one letter off a popular one, hoping someone mistypes it. Reviewing new dependencies and using an allow-list or curated proxy helps. The second is a malicious update, where an attacker takes over a real maintainer's account and publishes a bad version. Lockfiles with integrity hashes and not auto-upgrading straight to brand-new versions give you time before it lands. The third is dependency confusion, where a public package with the same name as your internal one gets pulled instead. Scoped names and a single registry proxy fix that. The fourth is attacking the build itself, like a compromised CI plugin or build server that injects code after the source was reviewed. Isolated, ephemeral builds, pinned CI actions and signed provenance let you prove an artifact came from the expected source and pipeline."
Only talking about known CVEs in dependencies, with no idea of malicious packages or build compromise.
Attack: a public package reuses your internal package's name with a higher version.
Why it works: the resolver checks several registries and picks the highest version.
Defences: scoped names tied to the private registry, one proxy that never fetches internal names publicly, lockfiles with hashes, reserved public names.
"Dependency confusion happens when your code uses an internal package, say acme-auth, and an attacker publishes a package with the same name to the public registry with a very high version number. If your build is set up to look at both the private and the public registry, the resolver may pick the higher public version and run the attacker's install script on your build machines. To prevent it I'd do a few things. Use scoped names, like an organisation scope in npm, and map that scope only to the private registry. Point all builds at a single internal proxy that serves internal names only from the private feed and never fetches them from outside. Avoid setups like pip's extra index option, which merges several indexes. Commit lockfiles with integrity hashes so a surprise package fails the install. And consider registering the internal names publicly as empty placeholders."
# .npmrc: the @acme scope only ever resolves from the private registry
@acme:registry=https://npm.internal.example.com/
registry=https://proxy.internal.example.com/npm/
Thinking a private registry alone solves it, without checking how the resolver merges public and private sources.
Provenance: a record of which source, build steps and builder produced an artifact.
SLSA build levels: provenance exists, then signed by a hosted build platform, then a hardened, isolated build.
Use: verify provenance at deploy time, not just generate it.
"Provenance is a signed statement that says how an artifact was made: which source repository and commit, which build definition, which builder ran it and what came out, identified by digest. It answers the question: did this image really come from our reviewed code through our pipeline? SLSA is a framework that describes levels of trust in that. At the first build level you simply produce provenance, so there's a record. The next level asks that a hosted build platform generates and signs it, so a developer can't forge it on a laptop. The higher level asks for a hardened platform, where builds are isolated from each other and the signing material isn't reachable from the build steps, so even a malicious build script can't fake the record. Generating it isn't the end: the deploy step should check the provenance matches the expected repo and builder."
Treating provenance as just a log file, or describing SLSA as a vulnerability scanner.
Risk: a tag like v4 can be moved to point at new code, including malicious code.
Pin: a full commit hash can't be changed, so you run exactly what you reviewed.
Keep current: an update bot raises pull requests to move the pin.
"A third-party action or plugin runs inside your pipeline, often with access to secrets and deploy credentials. If you reference it by a tag, you're trusting whoever controls that repository, because a tag is just a pointer that can be moved to a new commit. If the maintainer's account is compromised, the attacker can repoint the tag, and your next pipeline run pulls their code without any change on your side. Pinning to the full commit hash means you run exactly the code you looked at, and it can't change underneath you. The cost is that you won't get fixes automatically, so I pair it with a dependency bot that opens pull requests to update the pins, and a short review of what changed. I'd also limit which third-party actions are allowed at all and give each job the smallest token permissions it needs."
steps:
# Pinned to a full commit hash; the comment records the version it matches
- uses: actions/checkout@<full-40-character-commit-sha> # v4
Saying a version tag is fixed and can never change.
Scope: search SBOMs, lockfiles and build logs for the bad version and when it was installed.
Contain: block the version at the registry proxy, pin to a known-good one, stop affected pipelines.
Assume compromise: rotate secrets reachable from machines that ran it; check for the advisory's indicators.
Recover: rebuild from clean runners and redeploy; write it up.
"First, scope. I search our SBOMs and lockfiles for the bad version, and the build logs and proxy logs for when anything actually downloaded it, because that tells me which pipelines, developer laptops and deployed images are involved. Then I contain it. I block that version in our package proxy so nothing new can pull it, pin to the last known-good version, and pause pipelines that might pick it up. Because malicious packages usually run code at install time, I assume any machine that installed it may be compromised. That means rotating secrets those CI runners and laptops could reach, like registry tokens and cloud credentials, and checking for the indicators listed in the advisory. Then I rebuild the affected images on clean runners and redeploy. Afterwards I'd look at why we took a brand-new version so fast, and whether we should delay new releases for a few days."
Simply upgrading to the next version and moving on, without asking where the malicious version ran and what it could reach.
Catches: public storage, open security groups, missing encryption, over-broad IAM, disabled logging.
Source scan: fast, runs on every pull request, but can't always see values from variables and modules.
Plan scan: sees the resolved values Terraform will actually apply.
"IaC scanning checks infrastructure code for misconfigurations before they exist in the cloud: a storage bucket open to the public, a security group allowing SSH from anywhere, a database without encryption, a role with wildcard permissions or audit logging switched off. Scanning the source files is quick and gives feedback on the pull request, and tools like Checkov or Trivy do that well. The catch is that a lot of the real values aren't in the file. A CIDR block might come from a variable set per environment, or the risky setting might be inside a module pulled from a registry. The plan has all of that resolved, so if I run terraform plan, export it as JSON and scan that, I'm checking what will actually be created. I usually do both: source for speed, plan for accuracy before apply."
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
checkov -f plan.json --framework terraform_plan
Assuming a clean source scan means the deployed infrastructure is safe.
Idea: rules written as versioned, tested code instead of a wiki page or manual review.
OPA: takes JSON input, evaluates Rego rules, returns a decision such as a list of deny messages.
Where: against a Terraform plan in CI, and at admission time in the platform.
"Policy-as-code means our security rules live in a repository as code, with reviews, tests and versions, and a tool enforces them the same way every time. OPA is a general policy engine: you give it JSON input and it evaluates rules written in Rego. Here the input is a Terraform plan exported as JSON. The rule walks through every resource change, looks for ingress security group rules whose CIDR list includes 0.0.0.0/0 and whose port range covers 22, and adds a deny message naming the resource. In CI I'd run it with conftest against plan.json and fail the job if there's any deny. I'd keep unit tests with a good and a bad sample plan next to the policy. The same engine can also run at deploy time, for example as a Kubernetes admission check, so the rule doesn't depend on everyone using the pipeline."
package main
import rego.v1
deny contains msg if {
some rc in input.resource_changes
rc.type == "aws_security_group_rule"
rule := rc.change.after
rule.type == "ingress"
"0.0.0.0/0" in rule.cidr_blocks
rule.from_port <= 22
rule.to_port >= 22
msg := concat(" ", [rc.address, "opens SSH to the internet"])
}
Describing policy-as-code as a document of rules with no automated enforcement or tests.
Always block: verified secrets, critical or high findings with a fix, clear policy breaks like public storage.
Warn and track: medium and low, no-fix CVEs, low-confidence rules.
Only new issues: block what the change introduces; existing debt goes to a backlog with dates.
Escape hatch: time-boxed exceptions with an owner, recorded and reviewed.
"I block on things that are high confidence and fixable now. A verified secret always blocks. So do critical and high vulnerabilities that have a fix available, and clear policy breaks like a public bucket or a container running privileged. Medium and low findings, CVEs with no fix yet and rules we don't fully trust yet only warn and go to the team's backlog. I also compare against a baseline, so a pull request is only blocked for problems it introduces. Blocking someone for five-year-old debt in a file they didn't touch is how you lose people. Every blocking rule needs an escape hatch: a time-boxed exception, approved by someone other than the author, with an owner and an expiry date, so a real emergency can ship without anyone switching the scanner off. I decide the rules with the teams, start with warn-only, and promote rules to blocking once their false positive rate is low."
Blocking on every finding of any severity, or never blocking anything so the checks are only advisory.
Don't switch the gate off: it would also let every other issue through.
Narrow exception: allow only this CVE ID, for a short time, with an owner.
Fix centrally: patched version or override in shared templates; mitigations if there's no fix yet.
Track: list of affected services and a date the exception ends.
"I'd say no to turning off the whole gate, but yes to unblocking people today. Switching it off would let every other critical issue through too, and gates that go off tend to stay off. Instead I'd add a narrow exception for that one CVE ID, valid for a few days, owned by me or the security lead, so builds pass for this issue only. At the same time I'd work on the real fix. If a patched version exists, I'd update the shared build templates or base images so most teams get it by rebuilding. If there's no patch yet, I'd check the advisory for a configuration change that disables the vulnerable feature and push that out. Using our SBOMs, I'd list the affected services, especially internet-facing ones, and track them. When the exception expires, the gate blocks again for anyone still unpatched."
Either turning the gate off completely, or refusing any exception and leaving every team blocked.
Situation: the check and why people objected, in their words.
Action: what you measured and changed: speed, noise, messages, fix guidance.
Result: adoption, and what the check caught once it stuck.
"At my last company I added a dependency scan to the shared pipeline template, set to block on high findings. Within a week two teams had copied the template and removed the step, and the feedback was blunt: it added several minutes and blocked on findings in test-only libraries. I sat with both teams and they were mostly right. So I made three changes. I moved the scan to run in parallel with the tests, which took the extra time down to under a minute. I excluded dev-only dependencies from blocking, while still reporting them. And I changed the failure message to show the exact version to upgrade to. Then I brought both teams back to the standard template and turned blocking on for new findings only. After that, nobody asked to remove it, and it stopped a vulnerable library upgrade the next month."
A story where the answer was simply escalating to management to force the check on people.
Inventory: how you found the affected services, and what the gaps were.
Coordination: owners, priority order, a single tracker, regular updates.
Lesson: what you built so the next one is faster.
"When a critical CVE came out in a widely used Java library, my last team was asked which of our services were affected. Honestly, the first day was slow. We had scan results for new builds, but nothing for services that hadn't been built in months, so we ended up searching repositories and asking teams. We found most of them that day, and two more later in old batch jobs nobody had listed. We prioritised internet-facing services, put everything in one tracker with an owner per service, and posted updates twice a day. The whole estate was patched in about a week. Afterwards I pushed for two changes: SBOMs generated for every build and stored centrally, and a scheduled rebuild so nothing goes months without a scan. The next big CVE, we had a list of affected services within an hour."
Claiming it was quick and easy with no gaps found, or describing no lasting change.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.