Software supply chain attacks surged 742% over three years. Here is how to add SBOM generation and dependency scanning to your CI/CD pipeline before a compromised package ships to production.
Status: DRAFT
In 2020, a compromised update to SolarWinds' Orion software reached 18,000 customers — including multiple US government agencies. The attackers didn't exploit a zero-day vulnerability. They compromised the build pipeline and injected malicious code that was then signed, packaged, and distributed by the vendor itself.
The package looked legitimate because it was built by the legitimate build system. This is a supply chain attack, and they have increased by 742% in three years. The defense is knowing exactly what is in your software — and that starts with an SBOM.
An SBOM (Software Bill of Materials) is a machine-readable inventory of every component in your software: every library, every transitive dependency, every version number, every license.
Without an SBOM, when Log4Shell dropped in December 2021, teams had no systematic way to answer the question "do we have Log4j in our codebase?" They ran grep across repos, checked package.json files manually, and asked developers. It took days to know for certain.
With an SBOM generated at build time, the answer takes thirty seconds: query the SBOM for Log4j, get a list of every service version, fix them in order of exposure.
This is why the US government now mandates SBOMs for software sold to federal agencies, and why enterprise customers increasingly require them before software procurement.
A complete shift-left security setup has four distinct layers:
Code commit | v [Layer 1: SAST - static code analysis]Build | v [Layer 2: Dependency scanning]Package | v [Layer 3: SBOM generation + container scanning]Deploy | v [Layer 4: Runtime policy enforcement]Each layer catches a different class of problem. None of them alone is sufficient.
Static Application Security Testing (SAST) analyzes code for security vulnerabilities without running it. Add it to your CI pipeline as a step that runs on every pull request:
## GitHub Actions SAST with Semgrep- name: Run Semgrep SAST uses: semgrep/semgrep-action@v1 with: config: >- p/owasp-top-ten p/nodejs p/secrets env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}For secrets scanning — preventing API keys, database passwords, and tokens from being committed — add GitLeaks:
- name: Detect Secrets with GitLeaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Block PRs from merging if either of these steps finds a HIGH or CRITICAL severity finding. Be cautious with auto-blocking on MEDIUM — the false positive rate is high enough to frustrate developers.
Every npm install or pip install brings in dozens of transitive dependencies you didn't explicitly choose. Any of them can have known vulnerabilities.
## Trivy dependency scan in CI- name: Scan Dependencies with Trivy uses: aquasecurity/trivy-action@master with: scan-type: 'fs' scan-ref: '.' format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' exit-code: '1' ## fail the build on CRITICAL/HIGHFor Node.js projects, also run:
## Check for known vulnerabilities in npm packagesnpm audit --audit-level=high ## fail on HIGH or above ## Auto-fix safe updatesnpm audit fixThe philosophy: fail CI on CRITICAL and HIGH severity CVEs with a fix available. Warn on HIGH with no fix available (don't block — a CVE with no patch available just means you need to watch it). Let MEDIUM and LOW through with a report.
Generate the SBOM at build time, when you know exactly what went into the artifact. The two dominant SBOM formats are SPDX and CycloneDX — CycloneDX has better tool ecosystem support and is what most teams use.
## Install Syft for SBOM generationcurl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \ | sh -s -- -b /usr/local/bin ## Generate SBOM for a container imagesyft your-org/payment-service:v1.2.3 \ -o cyclonedx-json \ > payment-service-v1.2.3-sbom.json ## Generate SBOM for a directorysyft dir:./src \ -o spdx-json \ > sbom-spdx.jsonIn your CI pipeline:
- name: Generate SBOM run: | syft ${{ env.IMAGE_NAME }}:${{ github.sha }} \ -o cyclonedx-json \ > sbom.json- name: Upload SBOM as Artifact uses: actions/upload-artifact@v4 with: name: sbom-${{ github.sha }} path: sbom.json - name: Scan SBOM for Vulnerabilities run: | grype sbom:./sbom.json \ --fail-on critical ## fail on CRITICAL CVEs in the SBOMGrype takes the SBOM as input and checks every component against vulnerability databases. This is better than scanning the image directly because it runs faster and produces more accurate results.
Before the image is pushed to your registry, scan it for OS-level vulnerabilities:
- name: Build Container Image run: docker build -t payment-service:${{ github.sha }} . - name: Scan Container Image uses: aquasecurity/trivy-action@master with: image-ref: 'payment-service:${{ github.sha }}' format: 'table' severity: 'CRITICAL,HIGH' exit-code: '1' - name: Push to Registry if: success() ## only push if scan passed run: docker push payment-service:${{ github.sha }}The key pattern: scan before push. If the scan fails, the image never reaches your registry, and it can never be deployed.
Here is the complete GitHub Actions workflow for a production microservice at a company like Razorpay:
name: Security Pipelineon: push: branches: [main] pull_request: branches: [main] jobs: security-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Secret Detection uses: gitleaks/gitleaks-action@v2 - name: SAST Scan uses: semgrep/semgrep-action@v1 with: config: p/owasp-top-ten - name: Dependency Audit run: npm audit --audit-level=high - name: Build Image run: docker build -t $IMAGE:${{ github.sha }} . - name: Generate SBOM run: | syft $IMAGE:${{ github.sha }} \ -o cyclonedx-json > sbom.json - name: Vulnerability Scan run: grype sbom:./sbom.json --fail-on critical - name: Upload SBOM uses: actions/upload-artifact@v4 with: name: sbom path: sbom.json - name: Push Image if: github.ref == 'refs/heads/main' && success() run: docker push $IMAGE:${{ github.sha }}Once you are generating and scanning SBOMs, the next step is signing your images so consumers can verify they haven't been tampered with:
## Install Cosignbrew install cosign ## macOS## or download from GitHub releases for Linux CI ## Sign image after scan passescosign sign \ --key cosign.key \ your-registry/payment-service:v1.2.3 ## Verify signature before deploymentcosign verify \ --key cosign.pub \ your-registry/payment-service:v1.2.3Add verification to your Kubernetes admission webhook (using Kyverno or OPA Gatekeeper) so unsigned images are rejected at deploy time, not just at build time.
Start with secret scanning and dependency auditing — these have the highest signal-to-noise ratio and the fewest false positives. Get engineers comfortable with security failures in CI before adding SBOM generation and image signing.
Store SBOMs in a dedicated artifact store (Grype's --db-file or a dedicated S3 bucket with naming convention sbom/{service}/{version}.json). When a new CVE drops, you need to query your SBOMs to find which services are affected — this is only possible if SBOMs are centrally stored and named consistently.
Set a policy: any CRITICAL CVE with a fix available must be patched within 72 hours. Any CRITICAL CVE with no fix available gets a documented exception with a remediation plan. Make this policy visible in your security dashboard, not buried in CI logs.
| Tool | What It Does | Open Source |
|---|---|---|
| Syft | SBOM generation | Yes |
| Grype | SBOM vulnerability scan | Yes |
| Trivy | Image + fs scanning | Yes |
| Semgrep | SAST | Yes (OSS rules) |
| Snyk | Full supply chain platform | Freemium |
The open-source stack (Syft + Grype + Trivy + Semgrep) covers all four layers and costs nothing. Snyk and similar platforms add managed policies, developer-friendly remediation advice, and centralized dashboards — worth the cost for teams that want managed tooling.
INFORMATION📚 **References & Further Reading** * [Syft Documentation](https://github.com/anchore/syft) - SBOM generation tool * [Grype Documentation](https://github.com/anchore/grype) - Vulnerability scanner for SBOMs * [CISA SBOM Resources](https://www.cisa.gov/sbom) - US government SBOM guidance * [OpenSSF Scorecard](https://securityscorecards.dev/) - Automated supply chain security checks * [Cosign](https://docs.sigstore.dev/cosign/overview/) - Container image signing
Use Trivy's .trivyignore file to suppress specific CVE IDs with documented justification, or filter by fixed-only vulnerabilities using the --ignore-unfixed flag. For base image CVEs with no upstream fix, add a time-boxed exception in your security policy and monitor the NVD feed for patch availability rather than blocking all CI builds indefinitely.
Deploy Kyverno or OPA Gatekeeper with a policy that calls Cosign verify against your public key for every image in a Pod spec. Configure the admission webhook in fail-closed mode so images without a valid Cosign signature and attached SBOM attestation are rejected before the kubelet pulls them. Store signing keys in AWS KMS or HashiCorp Vault rather than as Kubernetes Secrets.
Discussion0