Apache received a private report of CVE-2021-44228 — Log4Shell — on November 24, 2021. Attackers were exploiting it in the wild by December 1, it went public on December 9, and Apache shipped three patches in eleven days: 2.15.0 on December 6, then 2.16.0 on December 13 after the initial fix turned out to be incomplete (CVE-2021-45046), then 2.17.0 on December 17 after a separate denial-of-service flaw (CVE-2021-45105) surfaced. Any team running a nightly or release-gated dependency scan had a multi-day window where a known-exploited CVE sat in production undetected. That gap is the entire argument for moving software composition analysis (SCA), static analysis (SAST), and dynamic analysis (DAST) into the pull-request loop instead of a pre-release checklist. The harder problem is doing that without turning every PR into a 45-minute wait behind a scanner that blocks on noise. This post lays out a reference architecture — where each gate sits, what it blocks versus warns on, and how to keep exploitability, not raw CVE count, as the blocking condition — drawing on OpenSSF's SLSA framework, CISA's Known Exploited Vulnerabilities (KEV) catalog, and the SARIF standard that most CI security tooling now speaks natively.
Where in the pipeline should each gate actually run?
SAST belongs on every commit or PR because it needs no running environment and completes in seconds to low minutes on incremental diffs; SCA belongs at build time, ideally as a pre-merge check, because dependency resolution is already happening then; DAST belongs against a deployed preview or staging environment, which means it necessarily runs later — after merge or on a per-PR ephemeral environment — because it needs a live HTTP target to attack. Pushing DAST earlier than that is a common mistake: teams try to run a DAST scanner against a partially built app in CI and get unreliable results, then conclude DAST is too slow for CI entirely. The fix is architectural, not procedural — spin up an ephemeral preview environment per PR (common with Kubernetes namespace-per-PR or platforms like Vercel/Render preview URLs), run a fast, scoped DAST pass against it, and treat that as a separate, parallel gate rather than trying to serialize it before merge.
How do you stop SAST and SCA from producing more noise than developers will tolerate?
You stop the noise by gating on reachability and exploitability, not on raw finding count or CVSS score alone. CVSS severity is a measure of theoretical impact, not likelihood of exploitation, and multiple vendor studies from 2023–2024 found that the large majority of CVEs an SCA tool flags in a typical application sit in code paths never actually invoked at runtime — meaning a naive "block on any critical CVE" policy blocks PRs for issues nobody can exploit. CISA's Known Exploited Vulnerabilities (KEV) catalog is the standard corrective: instead of blocking on CVSS ≥ 7, block on "CVSS high or critical AND KEV-listed," which shrinks the blocking set to CVEs with confirmed in-the-wild exploitation. Safeguard's guardrail model documents exactly this pattern — a policy rule with a condition like severity == "critical" AND kev == true and effect BLOCK, versus a broader SLSA or license check set to WARN instead of BLOCK, so only confirmed-exploited risk stops a merge.
What makes a gate "non-blocking" without making it toothless?
A gate is non-blocking-by-default when its failure state defaults to annotating the PR rather than failing the CI job, with blocking reserved for a narrow, pre-agreed severity band. The practical mechanism is SARIF (Static Analysis Results Interchange Format), the OASIS standard that GitHub Code Scanning and most SAST/DAST tools use to upload findings as inline PR review comments tied to the exact line — a developer sees the finding in the diff without the pipeline failing outright. Safeguard's own CI integration follows this pattern: safeguard gate check --policy production --fail-on critical --format sarif --output results.sarif uploads annotations to GitHub's Security tab on every run, but only the --fail-on critical condition returns a non-zero exit code that fails the build. Everything below that threshold — a WARN-level license mismatch, a medium-severity finding, a component past its SLSA-provenance check — surfaces as visible feedback and a ticket, not a blocked merge.
How should exceptions work so a gate doesn't just get disabled under deadline pressure?
Exceptions need to be time-boxed, auditable, and require a named approver — otherwise the first missed deadline becomes the precedent for disabling the gate entirely rather than filing an exception. The documented Safeguard pattern is a request/approval workflow: a developer requests an exception against a specific policy and finding, provides a justification, sets an expiration date, and an approver with the right role grants or denies it, with every decision recorded to an audit trail. Emergency "breakglass" exceptions — bypassing a blocking gate under incident pressure — should require two-person approval and fire an alert to the security channel specifically because they are the path most likely to be abused if left as a single click. A --allow-exceptions flag in the CI gate step lets the same pipeline honor an approved, unexpired exception automatically rather than forcing someone to manually skip the check.
Which frameworks should define what "good" looks like for these gates?
NIST's Secure Software Development Framework (SP 800-218) and the OWASP Top 10 give you the rule taxonomy — what a SAST/DAST rule set should actually check for — while OpenSSF's SLSA framework gives you the build-integrity ladder that sits alongside them. SLSA v1.0, finalized in April 2023, defines a cumulative Build track running from L0 (no guarantees) through L3: L1 requires machine-generated build provenance, even if incomplete or unsigned; L2 requires that provenance be signed and come from a hosted, tamper-resistant build platform rather than a developer's laptop; L3 adds isolation between builds and protects the signing material from the build steps themselves, so a compromised dependency can't forge its own provenance. Mapping SLSA levels onto your gate policy — for example, requiring SLSA L2 provenance to merge and SLSA L3 to deploy to production, as Safeguard's policy templates support with a "Require SLSA Level 2 attestations" condition — gives you a second, independent axis of trust alongside CVE and CWE-based findings: not just "is this code free of known bugs" but "can we prove how this artifact was built."
Where does Safeguard fit into this architecture?
Safeguard's role in this reference architecture starts at the SCA, policy-gate, and supply-chain-attestation layer that's been the core of the platform, and now extends to first-party SAST and DAST as well: source-to-sink static analysis and defensive, verified-target dynamic testing that share one findings model with SCA, secrets, container, and IaC results, so a runtime issue and the source-code sink that caused it get correlated and prioritized together instead of arriving as two disconnected alerts. Detection depth on the SAST/DAST side is still expanding rather than mature and broad-coverage, so most teams will run it alongside whichever other static and dynamic scanners they already trust. The safeguard gate check CLI step evaluates SBOM, license, CVE, KEV, and SLSA-provenance conditions against a named policy, exits non-zero only on a blocking match, and writes SARIF or JUnit output so findings land as PR annotations rather than opaque pipeline failures. The underlying guardrail model spans all six lifecycle enforcement points — IDE, commit, CI, registry, admission, and runtime — so the same KEV-gated policy that blocks a PR can also block a container registry push or a Kubernetes admission if it slips through earlier stages. And where a dependency upgrade introduces a new CVE, Griffin AI can open a pull request pinning the last safe version and updating the lockfile automatically, turning a blocking finding into a same-day merge instead of a ticket that sits in a backlog until the next audit.