Skip to content

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 ls output.)
  • 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 ci

Now 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: true or 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.