Security Gates in CI That Don't Make Every Pull Request Crawl
A scanner that blocks on every historic finding gets bypassed within a week. Baselines, severity floors, and the split between fast PR checks and slow nightly ones.
I turned on every scanner we had in one afternoon. Static analysis, dependency checks, container scanning, a dynamic scanner pointed at staging — all of them blocking. The pipeline went from four minutes to nineteen, and the pull request queue picked up 140 findings that predated the person opening the tab.
It took nine days for someone to find the bypass. After that, the gates were decorative.
The scanners were fine. The policy was wrong. Here is the shape that has held up since.
Two scans in the PR, and only two
Everything that runs on every pull request must finish in under a minute, or it will not be waited for.
SAST catches the patterns worth stopping for — SQL string concatenation, eval on untrusted input, path traversal, hardcoded credentials. Semgrep with a focused ruleset does this:
semgrep --config p/security-audit --config p/secrets \
--error --quiet .
SCA checks whether a newly introduced dependency is already known-bad:
osv-scanner --recursive --exit-code 1 .
Image scanning belongs in the build job, not the lint job — it has a different artifact to look at. Secret scanning belongs in pre-commit, before anything reaches CI at all. Keeping these separate means each gate has one job and one owner.
The trick: fail on what you added, not what you inherited
This is the single change that made the gates stick. Generate a baseline of today's findings, commit it, and have CI compare against it rather than against zero.
semgrep --config p/security-audit --json . > full.json
jq '[.results[] | {check_id, path, start}]' full.json > current.json
node scripts/diff-baseline.js baseline.json current.json > new.json
test "$(jq 'length' new.json)" -eq 0
Now the rule a reviewer can actually agree with: you may not introduce a new finding, and you may not make an existing one worse. The 140 historic items stay visible as a backlog instead of an obstacle. Teams burn that list down deliberately, in quiet weeks, rather than under deadline pressure.
Without the baseline you are asking the next person to fix the last three years of code as the price of shipping a one-line change. They will find a way around you instead.
Severity floors, written down
Pick a threshold and state it in the contributing guide so nobody has to guess:
- PR blocks: new criticals and highs.
- Report only: mediums and lows, logged to the dashboard.
- Never blocks: findings in vendored code, test fixtures, or packages you do not ship.
--ignore-unfixed deserves a second mention here. A vulnerability with no available patch cannot be actioned today, so failing the build on it just manufactures a red light nobody can turn green.
Slow work goes after the merge
The nightly job is where the thorough scans live: full dependency tree, no baseline comparison, dynamic scanning against a freshly deployed staging environment, and a report that lands in a channel rather than a blocking check. Those scans are allowed to take twenty minutes because nobody is refreshing a pull request waiting on them.
The PR gate protects the front door. The nightly scan is the audit. Confusing the two is how you end up with both a slow merge and an unwatched report.
Ownership, or it decays quietly
A finding with no owner is a finding that will be there next quarter. Route new high and critical results to the team that owns the path they landed in, and review the backlog on the same cadence as everything else. Scanners do not create accountability — they create tickets, which is a different and much less useful thing.
Summary
Keep pull request scanning fast and narrowly scoped, and let the slow, exhaustive work run on a schedule. Fail builds on findings the current change introduced rather than on the accumulated history of the repository, set an explicit severity floor, ignore what cannot be patched, and assign every remaining finding an owner. Security gates earn their place the same way any other CI step does: by being fast enough to wait for and specific enough to act on.
SDP Clouds Team
DevOps and cloud engineers writing practical, battle-tested guides on CI/CD, Kubernetes, infrastructure as code, and production operations — every article is based on real incidents and real pipelines, not docs-page rewrites.
More about us →