A practical look at how SCA scanning finds vulnerable dependencies, cuts CVE noise, and where SAST, DAST, and IAST fit into CI/CD.
Look at any modern service and count the lines of code you actually wrote versus the lines you pulled in. A typical Node or Python app ships with hundreds of transitive dependencies, and most of the code running in production came from a registry, not your team. That inversion is the whole story: your dependencies are the largest, least-audited part of your attack surface, and they change every time someone runs an install.
You review your own pull requests. You do not review the twelfth-level transitive package that a logging library pulled in three years ago. Attackers know this. The supply chain is where the leverage is, because one compromised or vulnerable package fans out across thousands of projects. Software Composition Analysis (SCA) exists to give you visibility into that layer.
SCA scans your dependency manifests and lockfiles against vulnerability databases. It resolves the exact versions you ship (this is why the lockfile matters, not just the manifest), matches them against known CVEs and advisories, and reports what is vulnerable and what fixes exist.
The tooling landscape is broad and mostly overlapping:
npm audit / pip-audit: language-native, already installed, good for a quick gate.The cheapest possible check is the one your package manager ships. Here is a GitHub Actions step that fails a build on high-severity npm advisories:
- name: Audit dependencies
run: npm audit --audit-level=high
For broader coverage, including containers and lockfiles across ecosystems, Trivy is a solid default:
- name: Trivy dependency scan
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: fs
scan-ref: .
severity: HIGH,CRITICAL
exit-code: '1'
ignore-unfixed: true
Note ignore-unfixed. There is no point failing a build over a CVE that has no available patch. Let it report, but do not block on something you cannot act on today.
Run SCA on a real repo and the first result is discouraging: dozens or hundreds of findings, most of them CRITICAL by CVSS score. If you treat every one as a blocker, engineers learn to ignore the scanner. That is the failure mode you are trying to avoid.
The key insight is that a CVE in a dependency is only exploitable if your code actually reaches the vulnerable function. A parsing bug in a code path you never call is not a live risk to you. Modern tools lean into this.
Reachability analysis: newer SCA engines build a call graph and check whether the vulnerable symbol is reachable from your code. Snyk, Semgrep's SCA, and others use this to demote unreachable findings.
EPSS and KEV: prioritize by real-world exploitation. EPSS scores the probability a CVE gets exploited; CISA's KEV catalog lists what is being exploited right now. A medium-CVSS bug that is in KEV outranks a critical one nobody has ever weaponized.
The practical rule: block the pipeline on findings that are fixable, reachable, and high-severity. Everything else goes to a tracked backlog, not a red build.
Most vulnerabilities are fixed by an upgrade you have not applied yet. The lowest-effort defense is staying close to current, which is why automated update PRs matter. Renovate and Dependabot both open pull requests as new versions land, so upgrades become small, reviewable, and continuous instead of a terrifying annual bump.
A minimal Dependabot config that groups minor and patch updates to keep PR volume sane:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
groups:
minor-and-patch:
update-types:
- minor
- patch
Pair this with committed lockfiles and pinned versions. Lockfiles guarantee that CI, staging, and production resolve the exact same tree, so a scan result is meaningful. Floating ranges (^1.2.0) without a lockfile mean the thing you scanned is not necessarily the thing you shipped.
SCA is one of four scan types, and confusion between them is common. They look at different things and run at different points.
SCA scans your dependencies. It answers "am I shipping a known-vulnerable component?" Runs early, on every commit and in the dependency-update PR.
SAST (Static Application Security Testing) scans your own source code without running it, looking for injection, hardcoded secrets, unsafe deserialization, and similar patterns. Tools like Semgrep and CodeQL run in CI on pull requests. Fast, good coverage, prone to false positives on custom rules.
DAST (Dynamic Application Security Testing) tests the running application from the outside, like an attacker probing HTTP endpoints. OWASP ZAP is the standard open-source option. It catches auth and configuration issues that only appear at runtime, but needs a deployed environment, so it runs later, against staging.
IAST (Interactive) instruments the running app during tests and observes real execution to confirm whether a flaw is genuinely exploitable. Lower false positives, at the cost of runtime instrumentation. It sits between SAST and DAST.
A reasonable pipeline placement:
None of these replaces another. SCA will never find the SQL injection you wrote by hand, and SAST will never tell you a third-party parser has a CVE.
While you are scanning dependencies, generate a Software Bill of Materials. An SBOM (CycloneDX or SPDX) is the machine-readable inventory of everything you ship, and it is what lets you answer "are we affected?" within minutes the next time a Log4Shell-class bug drops, instead of grepping repos for a week. Most SCA tools, including Trivy, emit an SBOM as a side output. For the full picture on generating, signing, and verifying them, see our guide on supply-chain security with SBOMs and attestation.
Start with the free, native tools and a hard gate on what is actually actionable. Run npm audit or Trivy in CI, fail only on high or critical findings that are fixable and reachable, and route the rest to a backlog so the signal stays trustworthy. Turn on Dependabot or Renovate day one, group the noisy updates, and commit your lockfiles so scans reflect reality. Layer SAST (Semgrep or CodeQL) on pull requests and DAST (ZAP) against staging, but do not expect any one scanner to cover another's blind spot. Generate an SBOM as part of the build so your next zero-day response is a query, not a fire drill. For how these pieces fit into a broader program, see our application security best practices pillar.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A practical guide to hashing passwords, adding MFA, hardening sessions, and avoiding the JWT mistakes that break production auth.
A production-focused Argo CD and GitOps guide: declarative Applications and ApplicationSets, app-of-apps, sync waves and hooks, automated self-heal and prune, progressive delivery with Argo Rollouts, projects/RBAC, and secure secrets — with copy-paste examples.
Explore more articles in this category
Run only the CI jobs a change actually affects using if conditionals, trigger path filters, and per-job path detection in a monorepo.
A prioritized toolkit for cutting CI time: measure the critical path first, then cache, parallelize, run only what changed, and shrink the work itself.
Stop stashing long-lived AWS access keys in GitHub secrets and let OIDC hand your workflows short-lived, scoped credentials instead.
Evergreen posts worth revisiting.