Lab 01 — Security Release Gate
Difficulty: 3/5 | Runs locally: yes
Pairs with the Phase 03 WARMUP Chapters 6–7, 10–11 (scanner limits, where checks belong, vulnerability management, exceptions) and the HITCHHIKERS-GUIDE ("Gate Semantics").
Why This Lab Exists (Purpose & Goal)
A security program lives or dies on one tension: stop real risk from shipping without becoming the bottleneck everyone routes around. A gate that blocks on every untuned scanner warning gets disabled within a week; a gate that blocks on nothing protects nothing. The goal of this lab is to build the explainable release gate that threads that needle — it blocks on what is genuinely dangerous, stays green on what is fine, and tells the developer exactly why and how to fix it.
The Concept, In the Weeds
A gate has three modes, and choosing wrong is the failure:
- block — when the evidence is strong, the impact is material, and a remediation exists (reachable critical/high vuln, a leaked secret, a missing SBOM/provenance/signature, an expired exception, a "fixed" finding with no regression test);
- warn / measure — for lower-confidence or still-tuning signals;
- exception — risk consciously accepted, but only with an owner, scope, compensating controls, and a hard expiry — never a permanent silence.
The decisive judgment is reachability and exploitability, not raw count. Most dependency findings are in code paths you never call; turning all of them into release failures is noise that trains developers to ignore the gate. So the gate consumes normalized findings plus release evidence and reasons about whether the issue is actually reachable and material. And every block must carry the finding ID, evidence, why-it-matters, the exact remediation, and a local reproduction — because a noisy, opaque gate is a bypassed gate, and a bypassed gate is no control.
Why This Matters for Protecting the Company
This is the control that prevents known-vulnerable, unsigned, or untracked code from reaching production at the speed of CI — across every team, automatically, every release. Done well it scales security to hundreds of engineers without a human reviewing each merge. Done poorly it either ships risk (too lenient) or destroys developer trust and gets removed (too noisy) — and a removed gate is worse than none, because everyone believes it's still there. Building one that engineers respect is how a security team earns the right to enforce anything at all.
Build It
Implement the gate over normalized findings + release evidence: block reachable critical/high vulns, leaked secrets, missing SBOM/provenance/signature, expired exceptions, and untested remediations; do not turn every scanner warning into a failure. Each decision must be explainable.
LAB_MODULE=solution pytest -q
Attack / Failure Cases
Dependency noise (don't block on unreachable findings); suppression abuse (expired/owner-less exceptions must fail); unsigned artifact substitution; a "fixed" finding with no regression test.
Secure Implementation Patterns
The anti-pattern. A gate that blocks on raw counts (noise → developers disable it) or one that honors a permanent suppression (risk ships forever):
if len(findings) > 0: fail() # blocks on unreachable noise -> gate gets bypassed
if finding.id in SUPPRESSED: pass # no expiry -> "temporary" exception is permanent
The secure pattern — reachability-aware, evidence-required, expiry-checked (mirrors solution.py):
def evaluate_release(findings, evidence, *, today) -> tuple[bool, tuple[str, ...]]:
reasons = []
# release EVIDENCE must be present (supply chain — Phase 03 Ch.8)
if not evidence.sbom: reasons.append("missing-sbom")
if not evidence.provenance: reasons.append("missing-provenance")
if not evidence.signature_verified: reasons.append("artifact-signature-unverified")
for f in findings:
# an exception only counts while it is UNEXPIRED
active = f.exception_expires is not None and f.exception_expires >= today
if f.secret and not active:
reasons.append(f"{f.finding_id}:secret")
if f.severity.lower() in {"critical", "high"} and f.reachable and not active:
reasons.append(f"{f.finding_id}:reachable-{f.severity.lower()}") # REACHABLE, not raw count
if f.regression_test is False and f.exception_expires is not None and not active:
reasons.append(f"{f.finding_id}:expired-exception-without-test")
blocked = sorted(set(reasons))
return (len(blocked) == 0), tuple(blocked) # block iff there is a real reason
Two design choices do the work: block on reachable critical/high (not on count), and treat an exception as valid only while unexpired — so suppressions decay and must be re-justified.
Production practices to carry forward:
- Reachability/exploitability filtering is the whole game — most dependency CVEs are unreachable; rank by reachability × exploitability × asset-criticality, fix the reachable-critical few now.
- Every block message carries finding id, evidence, why it matters, the exact fix, and a local repro — a noisy/opaque gate gets bypassed, and a bypassed gate is no control.
- Exceptions need owner + scope + compensating control + hard expiry; re-review at expiry, never auto-renew.
- Provide an auditable emergency-release path (time-bounded, approved, with mandatory post-release correction) so the gate doesn't block legitimate incident response.
Validation — What You Should Be Able to Do Now
- Decide block vs warn vs exception for a finding and justify it by reachability × exploitability × asset criticality, not by count.
- Explain why a green pipeline is not proof of security (scanners miss design flaws, business logic, and authorization gaps).
- Design a defensible exception (owner + scope + compensating control + expiry) and explain why permanent suppressions are a top failure mode.
- Write a block message a developer can act on (finding, evidence, why, fix, local repro).
The Broader Perspective
You built an enforcement point that has to be both correct and adopted — and adoption is itself a security property, because an ignored control is no control. That dual requirement (technically right and trusted by the people it governs) is the essence of program-level security engineering, and it reappears at every scale: a Kubernetes admission policy that breaks deploys gets exempted into uselessness (Phase 07); a detection that pages on noise gets muted (Phase 09); an AI guard that blocks useful work gets removed (Phase 11). The staff-level skill is making the secure path the easy path so the gate is something teams want, not a wall they tunnel under.
Interview Angle
- "How do you prioritize 10,000 dependency findings?" — Not by count: dedup, rank by reachability × exploitability × asset criticality, fix the reachable-critical few now, batch the rest, record exceptions with owner+expiry. SBOM answers "are we affected and where."
- "Design a defensible exception process." — Owner, business justification, compensating controls, hard expiry, re-reviewed at expiry (never auto-renewed).
Extension (Stretch)
Wire the gate into a real CI pipeline with seeded scanner fixtures (block on seeded criticals, green on approved fixtures) and an auditable emergency-exception path.
References
- Phase 03 WARMUP Chapters 6–7, 10–11; OWASP SAMM; NIST SSDF (SP 800-218).