Supply Chain Security Testing: A Complete Guide for DevSecOps Teams
Software supply chain attacks have become one of the most effective attack vectors in modern software.
Software supply chain attacks have become one of the most effective attack vectors in modern software. SolarWinds compromised 18,000 organizations through a single tampered build. XZ Utils nearly backdoored OpenSSH in half the Linux servers on the internet. The pattern is consistent: attackers don't attack your software — they attack the infrastructure that builds it.
This guide covers the full spectrum of supply chain security testing, from individual tool usage to building a comprehensive defense-in-depth strategy.
Understanding the Attack Surface
A software supply chain attack can occur at any point between "developer writes code" and "code runs in production":
Source code → Build system → Dependencies → Artifacts → Registry → Deployment → Runtime
↑ ↑ ↑ ↑ ↑ ↑ ↑
repo CI/CD npm/pip container Docker k8s process
compromise compromise compromise tampering Hub abuse miscfg injectionEach arrow represents a potential attack surface. Supply chain security testing systematically addresses each one.
Layer 1: Source Code Integrity
Signed Commits
Start at the source: require signed commits to prove code came from legitimate contributors.
# Configure git to sign commits
git config --global commit.gpgsign true
git config --global user.signingkey YOUR_GPG_KEY_ID
# Or use SSH signing (simpler)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
# Verify a commit
git verify-commit HEADBranch Protection Rules
On GitHub:
# Repository settings
required_pull_request_reviews:
required_approving_review_count: 1
dismiss_stale_reviews: true
require_code_owner_reviews: true
required_status_checks:
strict: true
contexts:
- security-scan
- dependency-review
require_signed_commits: true
enforce_admins: trueDependency Review
GitHub's dependency review action blocks PRs that introduce vulnerable dependencies:
# .github/workflows/dependency-review.yaml
name: Dependency Review
on:
pull_request:
permissions:
contents: read
pull-requests: write
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Review dependencies
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
deny-licenses: GPL-2.0, AGPL-3.0
comment-summary-in-pr: alwaysLayer 2: Dependency Security
The Dependency Confusion Attack
In a dependency confusion attack, a public malicious package with the same name as an internal package gets installed from the public registry instead.
Defenses:
# Pin exact versions in lock files (npm)
npm ci # Uses package-lock.json exactly
# Use package scoping for internal packages
# @mycompany/internal-lib instead of internal-lib
# Configure registry for scoped packages (.npmrc)
@mycompany:registry=https://npm.mycompany.com
# Verify packages have expected owners
npm owner ls reactScanning Dependencies at Multiple Levels
# Scan lock files (fastest, catches what you install)
osv-scanner --lockfile package-lock.json
# Scan installed packages (catches transitive deps)
pip install pip-audit
pip-audit
# Scan container image (catches OS + app packages)
trivy image myapp:latest
# Check for outdated dependencies
npm outdated
pip list --outdatedSupply Chain Specific Tools
# socket.sh - detects malicious npm packages
npm install -g @socketsecurity/cli
socket scan .
# Snyk for SCA
snyk test
snyk container test myapp:latest
# Semgrep for supply chain patterns
semgrep --config "p/supply-chain" .Layer 3: Build System Security
Isolating Builds
The build environment is high-value target. Controls:
# Use ephemeral, minimal build environments
jobs:
build:
runs-on: ubuntu-latest # Fresh VM per job
container:
image: golang:1.22-alpine # Minimal base image
steps:
# Pin actions to SHA, not tag (tags are mutable)
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722034518b972 # v5.0.2
# Verify third-party tools before using them
- name: Download and verify tool
run: |
curl -sSfL https://example.com/tool -o tool
echo "expected_sha256sum tool" | sha256sum --check
chmod +x toolPinning Actions to SHA
Action tags like @v4 can be moved to point to different code:
# Insecure - tag can be moved
- uses: actions/checkout@v4
# Secure - SHA is immutable
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# Use a tool to manage this
# dependabot will automatically update pinned SHAsUse pin-github-action or Dependabot to automate SHA management.
Restricting Build Permissions
# Minimum permissions for CI jobs
permissions:
contents: read # Only what's needed
# Don't grant packages:write unless building containers
# Don't grant issues:write unless needed
# id-token:write only for OIDC (Cosign/Sigstore)Detecting Exfiltration Attempts
Build systems can be used to exfiltrate secrets. Monitor for:
# GitHub Actions: use allowed-endpoint filtering
- uses: step-security/harden-runner@v2
with:
egress-policy: audit # Log outbound connections
# egress-policy: block # Block unexpected endpoints
allowed-endpoints: >
api.github.com:443
registry.npmjs.org:443
objects.githubusercontent.com:443Layer 4: Artifact Integrity
SBOM Generation
Generate an SBOM for every artifact you ship:
# Container image SBOM
syft myapp:v1.2.3 -o spdx-json > sbom.spdx.json
syft myapp:v1.2.3 -o cyclonedx-json > sbom.cdx.json
# Binary SBOM
syft myapp-linux-amd64 -o spdx-json > sbom.spdx.jsonArtifact Signing
Sign every artifact at build time:
- name: Sign container image
run: |
cosign sign --yes \
myapp@${{ steps.build.outputs.digest }}
- name: Sign binary
run: |
cosign sign-blob --yes \
--bundle myapp-linux-amd64.bundle \
myapp-linux-amd64Provenance Generation
Generate SLSA provenance to record build details:
# Use slsa-github-generator for SLSA 3 provenance
provenance:
needs: build
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.10.0
with:
image: myapp
digest: ${{ needs.build.outputs.digest }}Layer 5: Registry Security
Image Pull Policy
Never use latest in production:
# Kubernetes deployment
containers:
- name: myapp
image: ghcr.io/myorg/myapp@sha256:abc123... # Pin to digest
imagePullPolicy: IfNotPresentAdmission Control
Reject unsigned or untrusted images at the cluster level:
# Sigstore Policy Controller
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-signatures
spec:
images:
- glob: "ghcr.io/myorg/**"
authorities:
- keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: "https://token.actions.githubusercontent.com"
subjectRegExp: "https://github.com/myorg/.*"Layer 6: Continuous Monitoring
Supply chain security isn't a one-time check — vulnerabilities are discovered constantly.
Scheduled Rescanning
# .github/workflows/security-rescan.yaml
name: Weekly Security Rescan
on:
schedule:
- cron: '0 8 * * 1' # Monday 8 AM
jobs:
rescan:
runs-on: ubuntu-latest
strategy:
matrix:
image:
- ghcr.io/myorg/myapp:v1.2.3
- ghcr.io/myorg/myapp:v1.2.2
steps:
- name: Pull latest vuln DB
run: grype db update
- name: Scan image
run: |
grype ${{ matrix.image }} \
--fail-on critical \
-o json > results.json
- name: Notify if critical found
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{"text": "Critical vulnerability found in ${{ matrix.image }}"}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}SBOM-Based Monitoring
Store SBOMs and rescan them against updated vulnerability databases:
#!/bin/bash
# rescan-all-sboms.sh
SBOM_DIR=/storage/sboms
for sbom in "$SBOM_DIR"/*.spdx.json; do
echo "Scanning: $sbom"
version=$(basename "$sbom" .spdx.json)
grype "sbom:$sbom" \
--fail-on critical \
-o json > "/tmp/${version}-vulns.json"
CRITICAL=$(jq '[.matches[] | select(.vulnerability.severity=="Critical")] | length' \
"/tmp/${version}-vulns.json")
if [ "$CRITICAL" -gt 0 ]; then
echo "ALERT: $CRITICAL critical vulnerabilities in $version"
# Send alert...
fi
doneTesting Your Supply Chain Controls
The controls are only as good as your testing. Build specific tests for each layer:
Test: Can an Unsigned Image Be Deployed?
# Attempt to deploy a known-unsigned image
kubectl run unsigned-test \
--image=nginx:latest \
--namespace=production
# Expected: blocked by admission controller
# grep for rejection message in outputTest: Does Dependency Scanning Catch Vulnerabilities?
# Create a test project with a known-vulnerable dependency
mkdir /tmp/vuln-test
cat > /tmp/vuln-test/requirements.txt << EOF
requests==2.6.0 # CVE-2018-18074
EOF
osv-scanner --lockfile /tmp/vuln-test/requirements.txt
# Expected: vulnerability report for CVE-2018-18074
# Verify CI would fail
if osv-scanner --lockfile /tmp/vuln-test/requirements.txt; then
echo "FAIL: scanner should have failed"
exit 1
else
echo "PASS: scanner correctly detected vulnerability"
fiTest: Is Provenance Verifiable?
# Verify provenance of your latest production image
slsa-verifier verify-image \
ghcr.io/myorg/myapp:v1.2.3 \
--source-uri github.com/myorg/myapp \
--source-tag v1.2.3
# Expected: "Verified SLSA provenance"Test: SBOM Completeness
#!/usr/bin/env python3
"""Test that SBOM contains all expected packages"""
import json
import subprocess
import sys
# Generate fresh SBOM
result = subprocess.run(
['syft', 'myapp:latest', '-o', 'cyclonedx-json'],
capture_output=True, text=True
)
sbom = json.loads(result.stdout)
# Check expected packages are present
expected = {'openssl', 'python3', 'requests'}
found = {c['name'].lower() for c in sbom.get('components', [])}
missing = expected - found
if missing:
print(f"SBOM missing expected packages: {missing}")
sys.exit(1)
print(f"SBOM completeness check passed ({len(found)} packages)")The Minimal Viable Supply Chain Security Program
If you're starting from zero, implement in this order:
Week 1: Enable dependency scanning
- Add Grype or Trivy to CI
- Fail on critical severity
- Don't suppress anything yet (accept the noise)
Week 2: SBOM generation
- Generate SBOMs for all container builds
- Store them as build artifacts
- No enforcement yet
Week 3: Image signing
- Sign all container images with Cosign
- Verify signatures in staging deployment
Week 4: Admission control
- Deploy Sigstore Policy Controller or Kyverno
- Enforce signature verification in non-production first
- Roll out to production after stabilizing
Month 2: SLSA provenance
- Add slsa-github-generator to release workflows
- Aim for SLSA 2 initially
- Add slsa-verifier to deployment pipeline
Month 3+: Continuous monitoring
- Scheduled rescanning of historical SBOMs
- Alerting on new critical CVEs in deployed versions
- VEX statements for false positive management
Common Mistakes
Signing without verifying: Sign images in CI, but verify nothing at deploy time. Signing is useless without enforcement.
Suppressing everything: Teams get overwhelmed by scanner noise and suppress all findings. Audit suppression lists quarterly.
Tag pinning without SHA pinning: Tagging containers with v1.2.3 isn't immutable. Pin to SHA digests for production.
Trusting the build environment unconditionally: If your CI environment is compromised, signed artifacts can still be malicious. Defense in depth is necessary.
One-time scanning: Scanning at build time only. Vulnerabilities disclosed after your build are invisible until the next build. Schedule regular rescans.
Supply chain security is not a product you buy once — it's a practice you build incrementally. Start with the highest-signal controls (dependency scanning, image signing) and layer in more sophisticated controls over time. The goal isn't perfection; it's making attacks measurably harder and detectable earlier.