AppSec Shift-Left: SAST, SCA and Secrets Scanning Without Slowing the Team
How Basilisk OffSec rolls out AppSec gradually, measuring developer friction and avoiding the permanently red pipeline nobody bothers to read.

Every time a CISO announces shift-left on a Thursday, some platform team spends the weekend ripping gates out of the pipeline. The promise of catching vulnerabilities before merge is real, but execution usually collapses into a SAST tool with 4,000 findings, an SCA flagging a 2017 CVE in a test library, and a secrets scanner failing because someone committed an .env.example. The outcome is predictable: ignored PRs, broken builds, frustrated developers, and security reduced to bureaucracy. At Basilisk OffSec we run AppSec as an internal product, with SLAs, friction metrics, and a staged rollout that begins as warning-only and ends with surgical blocking. This guide walks the full path: how to define criticality, wire SAST, SCA and secrets scanning so they earn trust, harden the pipeline itself, and measure whether developers actually respect the result.
What 'critical' actually means: severity by context
Before installing any tool, define what you call critical. Without that definition, Semgrep, Snyk and CodeQL will compete to see who alarms more, and every alert will carry the same visual weight regardless of blast radius. Use the service threat model as the baseline and tier your assets into three buckets. A tier-1 microservice handling PII or money is not the same risk surface as an internal batch job that reads a read-only replica. For a tier-1 service, any CWE-89 (SQL injection) or CWE-78 (OS command injection) caught by SAST should break the build immediately. For an internal batch job, a weekly digest may be enough. This severity-by-context mapping cuts relevant false positives by 60 to 80 percent, according to measurements from our 12-squad pilot in 2025. The mapping is not a spreadsheet you write once; it is a policy-as-code artifact that lives next to the service and is reviewed when the data classification changes.
SAST without alert fatigue
SAST alone does not fix anything. Pair Semgrep (custom YAML rules, cheap to maintain, fast on diffs) with CodeQL for deeper interprocedural dataflow in Java, C# and Go. Run Semgrep on the changed files only for the PR check and keep a full-tree scan on a nightly schedule, so the developer-facing latency stays low while coverage stays complete. Start every new rule in comment-only mode on GitHub: the bot posts findings on the PR without failing the check. Track two things for 30 days: median time from comment to fix, and the won't-fix rate. If won't-fix exceeds 40 percent, your rule baseline is wrong, not the team. After that window, promote only rules with precision above 85 percent to a blocking gate. Precision is measured, not assumed: sample 30 findings, have an engineer label each as true or false positive, and compute the ratio before you turn the gate red.
Concrete configuration you can copy
A minimal Semgrep gate looks like semgrep ci --config auto --baseline-commit $(git merge-base origin/main HEAD), which scans only what the branch introduced and suppresses pre-existing debt so you do not block a PR for sins committed last year. A custom rule is a few lines of YAML: a pattern such as pattern: exec.Command($CMD, ...) with a metavariable-pattern that flags tainted input reaching the argument, tagged severity: ERROR and mapped to CWE-78. For CodeQL, wire the official action with languages: java and let it publish to the code-scanning tab so findings deduplicate against Semgrep instead of doubling the noise. Keep every rule in a versioned repository with a CODEOWNERS entry, so a rule change is itself a reviewed PR and never a silent policy shift that surprises a squad on Monday morning.
SCA and the reachability problem
SCA is where most teams trip. Trivy, Grype and osv-scanner are solid, but every one of them will surface hundreds of transitive CVEs in dependencies you never call. The only metric that matters is reachability. A CVE in a code path your service never executes is a backlog item, not a build breaker. Use Endor Labs, Socket, or osv-scanner with the --experimental-call-analysis flag to filter CVEs in uninvoked functions, and gate only on reachable, fixable, high-or-critical severity. Combine that with a signed SBOM generated at build time (syft or trivy sbom) and a quarantine policy that blocks a newly published version for 48 hours, which defeats most malicious-release supply-chain attacks. Do not forget typosquatting and namespace hijacking: an internal proxy such as Artifactory or Nexus with an allowlist solves 90 percent of dependency-confusion risk by refusing to resolve unknown internal names against the public registry.
Secrets scanning and the rotation reflex
Secrets scanning is the quick win most teams botch. Gitleaks and TruffleHog on a pre-commit hook catch the leak before the push, but a local hook is advisory and easily bypassed with --no-verify, so you need a second server-side scanner: GitHub Advanced Security push protection, or Gitleaks running as a required Action. The critical mental model: once a secret lands in a pushed commit, treat it as public, full stop. Force rotation; do not rewrite history and call it fixed. In 2025 we watched three incidents where teams burned hours on git filter-repo while the exposed AWS key was already being used to spin up mining instances. Keep an automated runbook that revokes the credential in under five minutes, invalidates dependent sessions, and opens a post-mortem ticket. TruffleHog's verified-secrets mode, which actually tests whether a found key is live, is worth the extra minute because it lets you triage by real exposure rather than by regex hit count.
Hardening the pipeline itself
A scanner is only as trustworthy as the runner it executes on. Use ephemeral, single-use runners so a compromised job cannot persist into the next build. Replace long-lived cloud credentials with short-lived OIDC federation: the CI job exchanges its signed identity token for a scoped, minutes-long cloud role, so there is no static AWS_SECRET_ACCESS_KEY to leak in the first place. Pin third-party Actions to a full commit SHA rather than a floating tag, because a tag can be repointed at malicious code after you audited it. Set least-privilege token permissions explicitly (permissions: contents: read) instead of inheriting broad defaults. Sign build artifacts and container images with Sigstore cosign and verify the signature at admission, so the pipeline that scanned the code is also the pipeline that proves what shipped. The security tooling must not become the softest target on the path to production.
Staged rollout: from warning to blocking
Do not flip everything to blocking on day one. Phase one is observe: every tool runs, nothing fails the build, and you collect baselines for finding volume, precision and fix latency. Phase two is warn on new, ignore old: the baseline-commit trick means only newly introduced issues surface, which keeps the signal aligned with what the developer just wrote. Phase three is block the narrow set of high-precision, high-severity, reachable findings, and only those. Publish the promotion criteria openly so a squad can predict exactly what will start failing and when. Give every gate an escape hatch with an audit trail: a documented, expiring exception approved by the service owner, not a silent bypass. An exception that is visible, time-boxed and reviewed is a feature; a bypass nobody can see is how gates rot back into the state that made the CISO announce shift-left in the first place.
Friction metrics and the internal SLA
Friction metrics separate functional AppSec from security theater. Track four KPIs per squad: median security-pipeline runtime (target under four minutes), rerun rate caused by flaky scanners (target under five percent), critical-finding MTTR (target under seven days), and internal NPS from product teams on the tooling. If NPS drops below zero, pause expansion and investigate before adding another scanner. Treat the platform team's own promises as an SLA: triage response time for a new critical, uptime of the scanning service, and a maximum added latency budget on the PR check. Layer in monthly purple-team sessions on real flows to verify that what SAST finds matches what an attacker would actually exploit; a linter finding with no exploit path is a rule to downgrade, and an exploited flaw the linter missed is a rule to write.
Common pitfalls
The recurring failures are boringly consistent. Blocking on the full historical backlog instead of the diff, which punishes the wrong developer. Gating on unreachable CVEs, which trains everyone to reflexively click ignore until they ignore the reachable one too. Storing scanner suppression comments with no expiry, so a temporary waiver becomes permanent blindness. Running secrets scanning only client-side and trusting the hook. Letting the security pipeline run on a runner with production credentials attached. Measuring finding volume as if more alerts meant more security, when the real goal is a low, trusted, actioned stream. And the most human one: shipping the tooling without a channel where a developer can say this rule is wrong and get a fast, respectful answer. Every one of these turns a defensible control into the thing teams route around.
A shippable checklist
Before you call the rollout done, confirm the following. Asset tiers defined and stored as policy-as-code. SAST running diff-scoped on PRs with a nightly full scan, new rules starting comment-only. SCA gating only on reachable, fixable, high-or-critical CVEs, with a signed SBOM per build and a quarantine window on new releases. An internal package proxy with an allowlist against dependency confusion. Secrets scanning enforced server-side with push protection, plus an automated revoke-and-rotate runbook under five minutes. Ephemeral runners, OIDC instead of static cloud keys, SHA-pinned Actions, least-privilege tokens, and signed-and-verified artifacts. Four friction KPIs dashboarded per squad with a published SLA. Documented, expiring, audited exceptions. If any line is missing, you have a scanner deployment, not an AppSec program.
FAQ
Should SAST or SCA block first? Start with SCA on reachable criticals, because dependency findings usually have a clear, low-effort fix (bump a version) and a clean true-positive story, so the first blocking gate earns trust rather than resentment. SAST blocking comes after you have measured per-rule precision, since a noisy SAST gate is the fastest way to lose the room.
How do we handle a secret that is already in git history? Rotate first, always, and treat the value as compromised the moment it was pushed. History rewriting with git filter-repo is housekeeping you do after rotation to shrink future exposure, never the incident response itself. Automate the revoke path so the mean time to rotate is minutes, not the hours a manual scramble costs while the key is being abused.
The practical takeaway is simple: do not flip everything to blocking on day one. Start with warnings, define criticality by context, harden the pipeline that does the scanning, measure friction, and promote only rules with proven precision. Six months in, you will have a pipeline developers respect because it rarely cries wolf, and when it does alarm, it is worth looking at. That is real shift-left, not shift-blame.


