Lesson 3 of 9 · 20m
Advisory vs Blocking: Gating Without a Revolt
Severity isn't exploitability, day-one findings need a baseline not a wall, and every gate needs designed escape hatches — or developers will build their own.
The fastest way to destroy a DevSecOps program is to turn on a scanner, set it to block on "medium and above," and walk away. Within a month there will be a Slack thread titled "can we just disable the security job," and the developers will be right — you handed them a gate that fires on noise.
Severity is not exploitability
A CVSS 9.8 in a function your code never calls is less urgent than a 6.5 sitting in your auth path. Raw scanner severity is a starting point, not a verdict. What actually matters:
- Is the vulnerable code path reachable? (Reachability analysis, call graphs, or a human reading
npm lsoutput.) - Is it exploitable in your configuration? A debug-mode flag nobody set, a dependency only loaded by tests — these change the answer completely.
- Is it exposed? Internet-facing and internal-only are different urgencies.
You won't have perfect answers to these. But "does the scanner know" is a worse question than "could an attacker actually get here."
Baselines: gate the delta, not the world
On day one, every scanner finds a backlog — years of accumulated findings. You cannot block on all of it. The standard move:
# record existing findings as a baseline; only NEW findings fail the gate
gitleaks detect --baseline-path .gitleaks-baseline.json --source .
# semgrep in CI mode is diff-aware: it reports findings in the PR's changes
semgrep ciNow the backlog becomes a tracked queue — a real ticket list with owners — while the gate only enforces "don't make it worse." That's a promise a team can actually keep, and it turns "4,000 findings" into "12 new ones this month," which is a conversation people will have.
The exception path is part of the design
Every gate needs three escape hatches, designed up front rather than improvised at 2am:
- Inline suppression with justification —
// nosemgrep: rules.sqli-concat -- parameterized driver, see ADR-14. Reviewable in the diff, greppable later. - A kill switch —
continue-on-error: trueor a repo variable that drops a job to advisory. For scanner outages, not emergencies of convenience. - Risk acceptance with an expiry — "accepted until 2025-12-01, owner: payments team." A forever-exception is an unfixed bug wearing paperwork.
Field notes
- Track gate bypass count as a metric. If it climbs, your gate is too expensive — that's feedback about the program, not about the developers.
- Roll out every new check in advisory mode for two weeks, tune until the false-positive rate is boring, then flip to blocking.
- Whoever owns the gate owns its false positives. If you can't tune it, don't gate on it.