lock-in.
Architecture Decisions
- SBOM-First Scanning: Direct lockfile parsing (package-lock.json, yarn.lock, go.sum) is fast but loses context. Generating an SPDX or CycloneDX SBOM before scanning preserves exact version pins, transitive relationships, and build metadata. SBOMs also satisfy emerging regulatory requirements (EO 14028, EU CRA).
- Policy-as-Code Gates: Hardcoded severity thresholds fail in practice. A policy engine evaluates scan results against contextual rules (e.g., allow CVSS < 7.0 if no public exploit, block if package is unmaintained, permit exceptions via signed allowlist).
- Asynchronous Reporting: Fail the PR only on actionable findings. Post-merge, push full results to a centralized database for trend analysis, compliance reporting, and automated remediation PR generation.
- TypeScript Integration Layer: Engineers maintain tooling in their primary language. A lightweight TypeScript parser standardizes output, applies risk scoring, and interfaces with CI orchestration.
Step-by-Step Implementation
1. Generate SBOM
Use syft to create a CycloneDX SBOM from your build artifact or source tree:
syft dir:. -o cyclonedx-json > sbom.cdx.json
2. Scan SBOM
Run trivy against the SBOM. Trivy queries multiple vulnerability databases (NVD, GitHub Advisories, OSV) and returns structured JSON:
trivy sbom ./sbom.cdx.json --format json --output trivy-results.json
3. Policy Evaluation & TypeScript Parser
Create a deterministic gate that parses results, applies risk filters, and exits with appropriate status codes.
// scan-policy.ts
import { readFileSync } from "fs";
import { exit } from "process";
interface TrivyResult {
Results: Array<{
Target: string;
Vulnerabilities?: Array<{
Severity: string;
CVSS?: Record<string, { V3Score?: number }>;
VulnerabilityID: string;
PkgName: string;
InstalledVersion: string;
FixedVersion?: string;
Title: string;
}>;
}>;
}
interface PolicyConfig {
maxSeverity: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
allowlist: string[];
requireFix: boolean;
}
function evaluateScan(results: TrivyResult, policy: PolicyConfig): number {
const severityRank: Record<string, number> = {
UNKNOWN: 0, LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4,
};
const threshold = severityRank[policy.maxSeverity];
let exitCode = 0;
for (const target of results.Results) {
if (!target.Vulnerabilities) continue;
for (const vuln of target.Vulnerabilities) {
if (policy.allowlist.includes(vuln.VulnerabilityID)) continue;
const score = severityRank[vuln.Severity] ?? 0;
const cvss3 = vuln.CVSS?.nvd?.V3Score ?? 0;
const isExploitable = cvss3 >= 7.0 || vuln.Severity === "CRITICAL";
if (score >= threshold) {
if (policy.requireFix && !vuln.FixedVersion) {
console.error(`[BLOCKED] ${vuln.VulnerabilityID} in ${vuln.PkgName}@${vuln.InstalledVersion}: No fix available`);
exitCode = 1;
} else if (isExploitable) {
console.error(`[BLOCKED] ${vuln.VulnerabilityID} in ${vuln.PkgName}: CVSS ${cvss3} exceeds threshold`);
exitCode = 1;
} else {
console.warn(`[WARN] ${vuln.VulnerabilityID} in ${vuln.PkgName}@${vuln.InstalledVersion}`);
}
}
}
}
return exitCode;
}
const scanResults = JSON.parse(readFileSync("trivy-results.json", "utf-8")) as TrivyResult;
const policy: PolicyConfig = {
maxSeverity: "HIGH",
allowlist: ["CVE-2023-44487"], // Example: HTTP/2 rapid reset (mitigated at infra)
requireFix: true,
};
exit(evaluateScan(scanResults, policy));
4. CI Orchestration
Chain the steps in your pipeline. Fail on exitCode !== 0. Archive SBOM and scan results for audit trails.
# .github/workflows/dependency-scan.yml
name: Dependency Vulnerability Scan
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.50.0
curl -sfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- run: syft dir:. -o cyclonedx-json > sbom.cdx.json
- run: trivy sbom ./sbom.cdx.json --format json --output trivy-results.json
- run: npx ts-node scan-policy.ts
- uses: actions/upload-artifact@v4
with:
name: supply-chain-artifacts
path: |
sbom.cdx.json
trivy-results.json
This architecture decouples detection from enforcement. The scanner remains stateless. Policy lives in code. Artifacts are versioned. Engineers receive deterministic feedback. Security teams retain auditability.
Pitfall Guide
-
Scanning Only Direct Dependencies
Lockfile parsers often miss transitive packages or fail to resolve deduplicated versions. Always scan the final build artifact or generated SBOM. Transitive dependencies account for ~70% of exploited vulnerabilities.
-
Treating CVSS Scores as Absolute Truth
CVSS measures theoretical exploitability, not runtime risk. A CVSS 9.8 vulnerability in a build-time-only tool or a disabled module poses zero production risk. Implement context-aware scoring that factors execution environment, network exposure, and mitigation controls.
-
No Exception/Allowlist Process
Hard blocking every finding breaks CI. Maintain a signed, time-bound allowlist with documented risk acceptance. Rotate exceptions quarterly. Auto-expire allowances tied to planned remediation sprints.
-
Scanning Only on Merge to Main
Vulnerabilities introduced in feature branches accumulate until integration. Run lightweight scans on pull requests. Defer full SBOM generation and policy evaluation to main branch pushes to balance speed and coverage.
-
Ignoring License Compliance Alongside Security
Dependency scanning without license evaluation creates legal exposure. Pair security scans with license classification (MIT, Apache-2.0, GPL-3.0, SSPL). Enforce organizational license policies in the same gate.
-
Storing Scan Results Without Version Correlation
Dashboards that show "current vulnerabilities" lose historical context. Store results alongside commit SHA, SBOM hash, and build ID. Enable trend analysis, rollback validation, and compliance reporting.
-
Relying Solely on SaaS Dashboards
Vendor platforms abstract away policy logic and create data silos. Keep scan engines open-source. Export results to your internal database. Maintain control over thresholds, exceptions, and remediation routing.
Best Practice: Implement a risk-based triage matrix. Classify findings by exploit availability, affected component type (runtime vs build-time), network exposure, and fix availability. Route high-confidence, actionable findings to automated remediation PRs. Route ambiguous findings to security review queues.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Early-stage startup (1-5 devs) | CLI scanning in PR with hardcoded severity gate | Minimal overhead, immediate visibility, no platform debt | Low (engineering hours only) |
| Mid-size team (10-50 devs) | CI-integrated scanning + SBOM + allowlist policy | Balances speed with compliance, reduces alert fatigue | Medium (pipeline maintenance + triage) |
| Enterprise/regulated | Full SCA platform + policy-as-code + SBOM versioning + automated remediation | Meets audit requirements, scales across hundreds of repos, enables supply chain attestation | High (platform licensing + dedicated SRE/security) |
| Open-source project | Public vulnerability scanning + automated dependabot/renovate PRs | Community trust, transparent security posture, low maintenance | Low (automation-driven) |
| Legacy monolith with 10k+ deps | Phased rollout: scan build artifacts first, then lockfiles, then enforce policy | Prevents CI paralysis, enables incremental remediation | Medium-High (remediation sprint allocation) |
Configuration Template
trivy-config.yaml
severity:
- HIGH
- CRITICAL
ignore-unfixed: false
exit-code: 1
format: json
output: trivy-results.json
db:
skip-update: false
light: true
scan:
security-checks: vuln,license
timeout: 5m
.github/workflows/dependency-scan.yml (Production-ready)
name: Dependency Vulnerability Scan
on:
pull_request:
branches: [main, release/**]
push:
branches: [main]
jobs:
supply-chain-scan:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci --ignore-scripts
- name: Install scanners
run: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.50.0
curl -sfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- name: Generate SBOM
run: syft dir:. -o cyclonedx-json > sbom.cdx.json
- name: Scan vulnerabilities
run: trivy sbom ./sbom.cdx.json --config trivy-config.json
- name: Evaluate policy
run: npx ts-node --esm scan-policy.ts
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: supply-chain-${{ github.sha }}
path: |
sbom.cdx.json
trivy-results.json
retention-days: 90
Quick Start Guide
- Install scanners:
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin and curl -sfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- Generate SBOM:
syft dir:. -o cyclonedx-json > sbom.cdx.json
- Run scan:
trivy sbom ./sbom.cdx.json --format json --output trivy-results.json
- Evaluate with policy:
npx ts-node scan-policy.ts (exits 0 on pass, 1 on block)
- Add to CI: Copy the workflow template, commit, and verify PR status checks pass/fail deterministically
Dependency vulnerability scanning is not a tooling problem. It is an engineering discipline. Treat dependencies as first-class citizens in your build pipeline, enforce policy through code, and correlate findings with immutable artifacts. The supply chain will not secure itself, but your pipeline can.