forms security scanning from a bottleneck into a precision tool.
Core Solution
A robust container security strategy requires a three-tier architecture: SBOM generation, multi-stage scanning, and policy enforcement. This solution uses Syft for SBOM generation and Trivy for vulnerability scanning, integrated into a CI/CD pipeline with TypeScript-based triage logic.
Architecture Decisions
- SBOM as First-Class Artifact: The SBOM is generated during the build and stored alongside the container image. This enables retrospective scanning and supply chain attestation.
- Layered Scanning:
- Pre-commit: Linting of Dockerfiles to prevent insecure patterns (e.g.,
RUN curl | bash).
- CI/CD: SBOM generation and initial vulnerability scan with fail thresholds.
- Registry: Continuous scanning of stored images to catch new CVEs.
- Risk-Based Triage: Scanning results are enriched with EPSS scores. Only vulnerabilities exceeding a risk threshold block promotion.
Step-by-Step Implementation
1. SBOM Generation
Use Syft to generate an SPDX-formatted SBOM. This format is widely supported and machine-readable.
syft -o spdx-json ./my-app:latest > sbom.spdx.json
2. Vulnerability Scanning with Trivy
Run Trivy against the image, outputting results in SARIF format for integration with security dashboards, and a JSON report for programmatic processing.
trivy image \
--format json \
--output trivy-results.json \
--exit-code 1 \
--severity HIGH,CRITICAL \
--ignore-unfixed \
my-app:latest
3. Programmatic Triage with TypeScript
Raw scan results often contain noise. A TypeScript utility can parse the SBOM and scan results to filter vulnerabilities based on EPSS and context. This script can be part of your deployment automation or a serverless function that validates images before promotion.
// triage-engine.ts
import fs from 'fs';
interface Vulnerability {
id: string;
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
epss_score?: number;
cvss_v3?: number;
fixed_version?: string;
}
interface ScanResult {
Results: Array<{
Target: string;
Vulnerabilities: Vulnerability[];
}>;
}
// Configuration for risk thresholds
const RISK_THRESHOLD = {
CRITICAL_EPSS: 0.5, // Block if Critical and EPSS > 50%
HIGH_EPSS: 0.8, // Block if High and EPSS > 80%
};
export function triageScanResults(scanPath: string): { blocked: boolean; actionableVulns: Vulnerability[] } {
const raw = fs.readFileSync(scanPath, 'utf-8');
const results: ScanResult = JSON.parse(raw);
const actionableVulns: Vulnerability[] = [];
results.Results.forEach(target => {
target.Vulnerabilities.forEach(vuln => {
// Filter logic: Combine severity with exploitability probability
const isCriticalActionable = vuln.severity === 'CRITICAL' &&
(vuln.epss_score || 0) > RISK_THRESHOLD.CRITICAL_EPSS;
const isHighActionable = vuln.severity === 'HIGH' &&
(vuln.epss_score || 0) > RISK_THRESHOLD.HIGH_EPSS;
if (isCriticalActionable || isHighActionable) {
actionableVulns.push(vuln);
}
});
});
return {
blocked: actionableVulns.length > 0,
actionableVulns
};
}
// Usage in deployment pipeline
const { blocked, actionableVulns } = triageScanResults('./trivy-results.json');
if (blocked) {
console.error(`Deployment blocked: ${actionableVulns.length} exploitable vulnerabilities detected.`);
process.exit(1);
}
4. Policy Enforcement with OPA
For Kubernetes deployments, enforce policies using Open Policy Agent (OPA) or Kyverno. This prevents vulnerable images from running, even if CI/CD is bypassed.
# kyverno-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-image-scan
spec:
validationFailureAction: Enforce
rules:
- name: validate-scan-result
match:
resources:
kinds:
- Pod
validate:
message: "Image has critical vulnerabilities."
pattern:
metadata:
annotations:
trivy-image-scan: "pass"
Pitfall Guide
Common Mistakes
-
Scanning Only the latest Tag:
- Issue: Production deployments use immutable tags (e.g., SHA digests). Scanning
latest provides no assurance for the deployed artifact.
- Fix: Scan the specific digest or tag being promoted. Use
trivy image myapp@sha256:....
-
Ignoring EPSS Data:
- Issue: Prioritizing based solely on CVSS leads to patching theoretical vulnerabilities while ignoring actively exploited ones with lower scores.
- Fix: Integrate EPSS scores to focus on vulnerabilities with a high probability of exploitation.
-
Bloating Images with Dev Tools:
- Issue: Including debug tools, shells, or package managers in production images increases the attack surface and scan surface.
- Fix: Use multi-stage builds and distroless or scratch base images. Remove all unnecessary binaries.
-
Treating Base Images as Secure:
- Issue: Assuming "official" or "alpine" images are safe. Base images frequently contain outdated libraries.
- Fix: Pin base images to specific digests. Scan base images independently before use. Automate base image updates.
-
Alert Fatigue from Low Severity:
- Issue: Blocking builds for LOW or MEDIUM vulnerabilities slows delivery and encourages teams to disable security.
- Fix: Configure scanners to fail only on HIGH/CRITICAL or use risk scoring to determine block thresholds.
-
Missing Runtime Context:
- Issue: A vulnerability in a library that is never loaded or called at runtime is not exploitable.
- Fix: Use static analysis tools that trace code paths to determine if vulnerable functions are actually reachable.
-
Not Updating Scanner Databases:
- Issue: Running scanners with stale vulnerability databases misses recent CVEs.
- Fix: Ensure CI/CD jobs fetch the latest vulnerability database before scanning. Use
trivy image --download-db-only in a separate job if needed.
Best Practices
- Immutable Scanning: Scan the exact artifact that is deployed. Store scan results linked to the image digest.
- Shift-Left Linting: Use
hadolint in pre-commit hooks to catch insecure Dockerfile patterns early.
- Automated Remediation: Integrate tools like Dependabot or Renovate to automatically create PRs for vulnerable dependencies.
- Non-Root Enforcement: Ensure containers run as non-root users. Scanners should flag images running as root.
- SBOM Distribution: Include the SBOM in the container image metadata or registry annotations for downstream consumers.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Early-Stage Startup | Build-time CLI Scan | Fast integration, low overhead, immediate feedback. | Low |
| Regulated Enterprise | SBOM + Continuous + OPA | Auditability, compliance requirements, defense-in-depth. | Medium-High |
| High-Velocity Microservices | SBOM + EPSS Risk Scoring | Reduces noise, maintains deployment speed, focuses on real risk. | Medium |
| Serverless/Edge Functions | Lightweight Static Scan | Minimal build time impact, sufficient for ephemeral workloads. | Low |
| Legacy Monolith Containers | Continuous Registry Scan | Hard to modify build pipeline; continuous scan catches drift. | Medium |
Configuration Template
GitHub Actions Workflow with SBOM and Trivy:
name: Container Security Scan
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build Image
run: docker build -t myapp:${{ github.sha }} .
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: myapp:${{ github.sha }}
format: spdx-json
output-file: sbom.spdx.json
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
- name: Run Trivy Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
exit-code: '1'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
Quick Start Guide
-
Install Tools:
brew install aquasecurity/trivy/trivy syft
# Or use curl for Linux
-
Scan Local Image:
trivy image myapp:latest --severity HIGH,CRITICAL
-
Generate SBOM:
syft -o spdx-json myapp:latest > sbom.json
-
Add to CI:
Copy the GitHub Actions template above into .github/workflows/security.yml. Adjust image names and severity thresholds.
-
Set Exit Code:
Ensure your CI configuration uses --exit-code 1 to fail the pipeline on critical vulnerabilities. Monitor the first few runs to tune thresholds and avoid false positives.
Container security scanning is not a destination but a continuous discipline. By implementing SBOM-driven workflows, risk-based prioritization, and policy enforcement, you transform scanning from a compliance checkbox into a measurable reduction of production risk. Focus on actionable threats, automate remediation, and maintain visibility across the entire container lifecycle.