Skip to content
Unverified — AI-generated content. Help verify this page

Security Scanning

Why Security Scanning in CI/CD Exists

In 2017, Equifax suffered a data breach affecting 147 million people. The root cause: an unpatched Apache Struts vulnerability (CVE-2017-5638) that had a fix available for two months before the breach. A dependency scanner in the CI pipeline would have flagged this vulnerability on day one.

Security scanning in CI/CD — often called "shift left security" — moves vulnerability detection from production incident response to the development pipeline. Instead of discovering vulnerabilities after deployment (or after a breach), teams catch them before code ever reaches production.

The Problem Landscape

Cost of Late Detection

The cost of fixing a vulnerability increases exponentially the later it's found:

Stage FoundRelative CostExample
Development (IDE)1xDeveloper fixes before commit
CI Pipeline5xBuild fails, developer investigates
Staging20xTest environment compromised
Production (pre-exploit)50xEmergency patch, coordinated deploy
Production (post-exploit)100-1000xBreach response, legal, regulatory

First Principles

Defense in Depth

No single scanning tool catches everything. A defense-in-depth strategy layers multiple scanners:

CVSS Scoring Model

The Common Vulnerability Scoring System (CVSS) provides a standardized severity rating:

CVSS Score=round(min([Impact+Exploitability],10))

Where Impact subscore depends on Confidentiality, Integrity, and Availability impact:

ISS=1[(1C)×(1I)×(1A)]Impact={6.42×ISSif Scope Unchanged7.52×[ISS0.029]3.25×[ISS0.02]15if Scope Changed
CVSS RangeSeverityCI/CD Action
0.0NoneInformational
0.1 - 3.9LowLog, don't block
4.0 - 6.9MediumWarn, flag for review
7.0 - 8.9HighBlock merge without override
9.0 - 10.0CriticalBlock pipeline, alert security team

The SLSA Framework

Supply-chain Levels for Software Artifacts (SLSA, pronounced "salsa") defines four levels of supply chain security:

LevelRequirementsProtects Against
SLSA 1Build process documentedAd-hoc builds
SLSA 2Build service, signed provenanceTampered builds
SLSA 3Hardened build platform, non-falsifiable provenanceCompromised build
SLSA 4Two-person review, hermetic buildsInsider threats

Core Mechanics

SAST (Static Application Security Testing)

SAST analyzes source code without executing it, looking for patterns that indicate vulnerabilities:

Common SAST findings:

VulnerabilityCWEExample
SQL InjectionCWE-89query("SELECT * FROM users WHERE id = " + userId)
XSSCWE-79innerHTML = userInput
Path TraversalCWE-22readFile("/data/" + userPath)
SSRFCWE-918fetch(userProvidedUrl)
Hardcoded SecretsCWE-798const API_KEY = "sk-live-abc123"
Insecure DeserializationCWE-502JSON.parse(untrustedData) used unsafely

SAST tools by language:

LanguageToolIntegrationFalse Positive Rate
TypeScript/JSSemgrepCLI, CILow (~10%)
TypeScript/JSESLint SecuritynpmVery Low (~5%)
GogosecCLI, CILow (~15%)
PythonBanditCLI, CIMedium (~20%)
JavaSpotBugs + FindSecBugsMaven/GradleMedium (~25%)
Multi-languageSonarQubeServer + CIMedium (~20%)
Multi-languageCodeQLGitHub nativeLow (~10%)

Semgrep Rules for TypeScript

yaml
# .semgrep/custom-rules.yml
rules:
  - id: no-sql-injection
    patterns:
      - pattern: |
          $DB.query(`... ${$USER_INPUT} ...`)
      - pattern-not: |
          $DB.query(`... $${$PARAM} ...`, [$VALUES])
    message: |
      Possible SQL injection. Use parameterized queries instead.
    severity: ERROR
    languages: [typescript, javascript]
    metadata:
      cwe: "CWE-89"
      confidence: HIGH

  - id: no-ssrf
    patterns:
      - pattern: |
          fetch($URL)
      - pattern-where-python: |
          # URL comes from user input
          "req." in str(vars["$URL"]) or "params" in str(vars["$URL"])
    message: |
      Possible SSRF. Validate and allowlist URLs before fetching.
    severity: WARNING
    languages: [typescript, javascript]
    metadata:
      cwe: "CWE-918"

  - id: no-eval
    pattern: eval(...)
    message: |
      eval() is dangerous. Use safer alternatives.
    severity: ERROR
    languages: [typescript, javascript]
    metadata:
      cwe: "CWE-95"

  - id: no-hardcoded-jwt-secret
    patterns:
      - pattern: |
          jwt.sign($PAYLOAD, "...")
      - pattern: |
          jwt.verify($TOKEN, "...")
    message: |
      Hardcoded JWT secret. Use environment variable instead.
    severity: ERROR
    languages: [typescript, javascript]
    metadata:
      cwe: "CWE-798"

SCA (Software Composition Analysis) / Dependency Scanning

SCA tools analyze your dependency tree against vulnerability databases:

typescript
// scripts/dependency-audit.ts
import { execSync } from 'child_process';

interface Vulnerability {
  id: string;
  severity: 'critical' | 'high' | 'medium' | 'low';
  package: string;
  version: string;
  fixedIn: string | null;
  cwe: string[];
  cvss: number;
  description: string;
  exploitable: boolean;
}

interface AuditResult {
  vulnerabilities: Vulnerability[];
  totalDependencies: number;
  directDependencies: number;
  transitiveDepth: number;
}

interface PolicyDecision {
  action: 'pass' | 'warn' | 'fail';
  reason: string;
  vulnerabilities: Vulnerability[];
}

function evaluatePolicy(audit: AuditResult): PolicyDecision {
  const critical = audit.vulnerabilities.filter(v => v.severity === 'critical');
  const high = audit.vulnerabilities.filter(v => v.severity === 'high');
  const exploitable = audit.vulnerabilities.filter(v => v.exploitable);

  // Policy: Block on critical or exploitable high
  if (critical.length > 0) {
    return {
      action: 'fail',
      reason: `${critical.length} critical vulnerabilities found`,
      vulnerabilities: critical,
    };
  }

  if (exploitable.filter(v => v.severity === 'high').length > 0) {
    return {
      action: 'fail',
      reason: `${exploitable.length} exploitable high-severity vulnerabilities`,
      vulnerabilities: exploitable,
    };
  }

  // Warn on high vulnerabilities with available fixes
  const fixableHigh = high.filter(v => v.fixedIn !== null);
  if (fixableHigh.length > 0) {
    return {
      action: 'warn',
      reason: `${fixableHigh.length} high vulnerabilities with available fixes`,
      vulnerabilities: fixableHigh,
    };
  }

  return {
    action: 'pass',
    reason: 'No policy violations',
    vulnerabilities: [],
  };
}

// Transitive dependency risk analysis
function analyzeDependencyRisk(audit: AuditResult): {
  riskScore: number;
  factors: string[];
} {
  const factors: string[] = [];
  let riskScore = 0;

  // Deep dependency chains increase risk
  if (audit.transitiveDepth > 10) {
    riskScore += 20;
    factors.push(`Deep dependency chain (${audit.transitiveDepth} levels)`);
  }

  // High ratio of transitive to direct deps
  const ratio = audit.totalDependencies / audit.directDependencies;
  if (ratio > 20) {
    riskScore += 15;
    factors.push(`High transitive ratio (${ratio.toFixed(1)}x)`);
  }

  // Vulnerability density
  const vulnDensity = audit.vulnerabilities.length / audit.totalDependencies;
  if (vulnDensity > 0.05) {
    riskScore += 25;
    factors.push(`High vulnerability density (${(vulnDensity * 100).toFixed(1)}%)`);
  }

  // Any critical or exploitable vulnerabilities
  const criticalCount = audit.vulnerabilities.filter(
    v => v.severity === 'critical' || v.exploitable
  ).length;
  riskScore += criticalCount * 10;

  return { riskScore: Math.min(riskScore, 100), factors };
}

Container Scanning with Trivy

Trivy is the de facto standard for container vulnerability scanning:

yaml
# .github/workflows/container-scan.yml
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:scan .

      # Comprehensive Trivy scan
      - name: Scan image vulnerabilities
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:scan'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

      # Scan for secrets in image
      - name: Scan for secrets
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:scan'
          scanners: 'secret'
          severity: 'CRITICAL,HIGH,MEDIUM'
          exit-code: '1'

      # Scan for misconfigurations
      - name: Scan Dockerfile
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          scan-ref: '.'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

      # Scan filesystem (IaC files)
      - name: Scan IaC
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: 'infrastructure/'
          scanners: 'misconfig'
          severity: 'CRITICAL,HIGH'

      # Upload to GitHub Security tab
      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

Trivy configuration for production:

yaml
# trivy.yaml
severity:
  - CRITICAL
  - HIGH

vulnerability:
  type:
    - os
    - library

ignorefile: .trivyignore
cache-dir: /tmp/trivy-cache

# Ignore unfixed vulnerabilities (no patch available)
ignore-unfixed: false

# Custom policy
misconfig:
  policy-namespaces:
    - custom

  scanners:
    - dockerfile
    - kubernetes
    - terraform

# DB update settings
db:
  skip-update: false
  java-db-update: true
# .trivyignore — Accepted risks with justification
# CVE-2023-xxxxx — No patch available, mitigated by WAF rules (expires: 2026-04-01)
CVE-2023-xxxxx

# CVE-2023-yyyyy — Only exploitable in configurations we don't use
CVE-2023-yyyyy

DAST (Dynamic Application Security Testing)

DAST tests the running application by sending crafted requests:

yaml
# .github/workflows/dast.yml
jobs:
  dast:
    runs-on: ubuntu-latest
    services:
      app:
        image: myapp:latest
        ports: ['3000:3000']
        env:
          DATABASE_URL: postgresql://test:test@postgres:5432/test
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: test
    steps:
      - uses: actions/checkout@v4

      - name: Wait for application
        run: |
          for i in $(seq 1 30); do
            curl -s http://localhost:3000/health && exit 0
            sleep 2
          done
          exit 1

      # OWASP ZAP baseline scan
      - name: ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: 'http://localhost:3000'
          rules_file_name: '.zap/rules.tsv'
          cmd_options: '-a -j'

      # Nuclei vulnerability scanner
      - name: Nuclei Scan
        run: |
          docker run --network host projectdiscovery/nuclei:latest \
            -u http://localhost:3000 \
            -severity critical,high \
            -json -o nuclei-results.json

      - name: Upload results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: dast-results
          path: |
            nuclei-results.json
            zap-report.html

Secret Detection

yaml
# .github/workflows/secret-scan.yml
jobs:
  secret-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for git-secrets

      # Gitleaks — scan git history
      - name: Gitleaks scan
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${​{ secrets.GITHUB_TOKEN }}

      # TruffleHog — deep scan
      - name: TruffleHog scan
        run: |
          docker run --rm -v "$PWD:/repo" \
            trufflesecurity/trufflehog:latest \
            filesystem /repo \
            --json \
            --only-verified \
            > trufflehog-results.json

          if [ -s trufflehog-results.json ]; then
            echo "VERIFIED SECRETS FOUND!"
            cat trufflehog-results.json | jq '.SourceMetadata'
            exit 1
          fi

Custom Gitleaks configuration:

toml
# .gitleaks.toml
title = "Custom Gitleaks Config"

[extend]
useDefault = true

[[rules]]
id = "custom-internal-api-key"
description = "Internal API Key Pattern"
regex = '''myorg-api-[a-zA-Z0-9]{32}'''
secretGroup = 0
entropy = 3.5

[[rules]]
id = "custom-jwt-secret"
description = "JWT Secret in config"
regex = '''(?i)jwt[_-]?secret\s*[=:]\s*['"]([^'"]{16,})['"]'''
secretGroup = 1

[allowlist]
description = "Allow test fixtures and documentation"
paths = [
  '''test/fixtures/''',
  '''docs/''',
  '''.*_test\.go$''',
  '''.*\.test\.ts$''',
]

Implementation: Unified Security Pipeline

yaml
# .github/workflows/security.yml
name: Security Pipeline

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'  # Weekly Monday scan

permissions:
  security-events: write
  contents: read

jobs:
  # Secret detection (fastest, run first)
  secrets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${​{ secrets.GITHUB_TOKEN }}

  # SAST — Source code analysis
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/typescript
            p/javascript
            p/owasp-top-ten
            p/xss
            p/sql-injection
            .semgrep/

      - name: CodeQL Analysis
        uses: github/codeql-action/init@v3
        with:
          languages: javascript-typescript

      - name: CodeQL Autobuild
        uses: github/codeql-action/autobuild@v3

      - name: CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:javascript-typescript"

  # Dependency scanning
  dependencies:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: npm audit
        run: |
          npm audit --audit-level=high --json > npm-audit.json || true
          CRITICAL=$(jq '.metadata.vulnerabilities.critical // 0' npm-audit.json)
          HIGH=$(jq '.metadata.vulnerabilities.high // 0' npm-audit.json)
          echo "Critical: $CRITICAL, High: $HIGH"
          if [ "$CRITICAL" -gt 0 ]; then
            echo "Critical vulnerabilities found!"
            exit 1
          fi

      - name: Trivy filesystem scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          severity: 'CRITICAL,HIGH'
          format: 'sarif'
          output: 'trivy-fs.sarif'

      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-fs.sarif'
          category: 'dependency-scan'

  # IaC scanning
  iac:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Trivy IaC scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-iac.sarif'

      - name: Checkov IaC scan
        uses: bridgecrewio/checkov-action@master
        with:
          directory: infrastructure/
          framework: terraform,kubernetes
          output_format: sarif
          output_file_path: checkov-results.sarif
          quiet: true
          soft_fail: false

      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-iac.sarif'
          category: 'iac-scan'

  # Container scanning (only on main or when Dockerfile changes)
  container:
    runs-on: ubuntu-latest
    if: >
      github.ref == 'refs/heads/main' ||
      contains(github.event.pull_request.changed_files, 'Dockerfile')
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:scan .

      - name: Trivy container scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:scan'
          format: 'sarif'
          output: 'trivy-container.sarif'
          severity: 'CRITICAL,HIGH'

      - name: Grype container scan
        uses: anchore/scan-action@v3
        with:
          image: 'myapp:scan'
          severity-cutoff: high
          output-format: sarif
          fail-build: true

      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-container.sarif'
          category: 'container-scan'

  # License compliance
  licenses:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci

      - name: Check licenses
        run: |
          npx license-checker --production --json --out licenses.json

          # Check for copyleft licenses
          COPYLEFT=$(jq -r 'to_entries[] | select(.value.licenses | test("GPL|AGPL|SSPL"; "i")) | .key' licenses.json)
          if [ -n "$COPYLEFT" ]; then
            echo "Copyleft licenses detected:"
            echo "$COPYLEFT"
            exit 1
          fi

  # Security report aggregation
  report:
    needs: [secrets, sast, dependencies, iac, container, licenses]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Security summary
        run: |
          echo "## Security Scan Results" >> "$GITHUB_STEP_SUMMARY"
          echo "| Scanner | Status |" >> "$GITHUB_STEP_SUMMARY"
          echo "|---------|--------|" >> "$GITHUB_STEP_SUMMARY"
          echo "| Secrets | ${​{ needs.secrets.result }} |" >> "$GITHUB_STEP_SUMMARY"
          echo "| SAST | ${​{ needs.sast.result }} |" >> "$GITHUB_STEP_SUMMARY"
          echo "| Dependencies | ${​{ needs.dependencies.result }} |" >> "$GITHUB_STEP_SUMMARY"
          echo "| IaC | ${​{ needs.iac.result }} |" >> "$GITHUB_STEP_SUMMARY"
          echo "| Container | ${​{ needs.container.result }} |" >> "$GITHUB_STEP_SUMMARY"
          echo "| Licenses | ${​{ needs.licenses.result }} |" >> "$GITHUB_STEP_SUMMARY"

Edge Cases & Failure Modes

False Positive Management

Scanner TypeTypical FP RateMitigation
SAST15-30%Custom rules, inline suppressions
SCA10-20%Reachability analysis, VEX
Container5-10%.trivyignore, base image selection
Secret detection20-40%Allowlists, entropy thresholds
IaC scanning10-20%Custom policies, skip rules

Vulnerability Exploitability eXchange (VEX) lets you declare that a detected vulnerability is not exploitable in your context:

json
{
  "@context": "https://openvex.dev/ns",
  "@id": "https://myorg.com/vex/2024-001",
  "author": "security@myorg.com",
  "timestamp": "2026-03-18T00:00:00Z",
  "statements": [
    {
      "vulnerability": "CVE-2024-12345",
      "products": ["pkg:docker/myorg/myapp@sha256:abc123"],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "impact_statement": "The affected function is in a module we don't import"
    }
  ]
}

Scanner Comparison Matrix

CapabilityTrivyGrypeSnykSonarQubeSemgrep
Container scanningYesYesYesNoNo
Filesystem scanningYesYesYesNoNo
IaC scanningYesNoYes (partial)NoYes
Secret detectionYesNoNoNoYes
SASTNoNoYesYesYes
License checkingYesNoYesNoNo
SBOM generationYesYesNoNoNo
CostFreeFreeFree/PaidFree/PaidFree/Paid
CI integrationExcellentGoodExcellentGoodExcellent

Performance Characteristics

Scanner Benchmarks

ScannerScan TypeTime (Small App)Time (Large App)Memory Usage
Trivy (container)Image15-30s60-120s200-500 MB
Trivy (filesystem)Dependencies5-10s20-40s100-200 MB
Grype (container)Image20-40s90-180s300-600 MB
SemgrepSAST10-30s60-300s200-800 MB
CodeQLSAST120-300s600-1800s2-8 GB
GitleaksSecrets5-15s30-120s50-200 MB
npm auditDependencies3-10s10-30s100 MB
CheckovIaC10-20s30-60s200-400 MB

Optimization Strategies

Tsecurity=max(Tsast,Tsca,Tcontainer,Tiac)

Running scanners in parallel reduces total time to the slowest scanner. For a pipeline with SAST (60s), SCA (15s), Container (45s), and IaC (20s):

  • Sequential: 60+15+45+20=140 seconds
  • Parallel: max(60,15,45,20)=60 seconds (57% reduction)

Trivy DB caching:

yaml
# Cache Trivy vulnerability DB
- uses: actions/cache@v4
  with:
    path: /tmp/trivy-cache
    key: trivy-db-${​{ github.run_id }}
    restore-keys: trivy-db-

- name: Scan with cached DB
  env:
    TRIVY_CACHE_DIR: /tmp/trivy-cache
  run: trivy image --severity CRITICAL,HIGH myapp:scan

Mathematical Foundations

Vulnerability Risk Scoring

A more nuanced risk score considers exploitability, asset value, and existing mitigations:

R=CVSS×E×AM

Where:

  • CVSS = base vulnerability score (0-10)
  • E = exploitability factor (0-1): is there a public exploit?
  • A = asset value factor (1-5): how critical is the affected system?
  • M = mitigation factor (1-5): are there compensating controls?
FactorValue 1Value 3Value 5
ExploitabilityNo known exploitPoC availableActive exploitation
Asset ValueInternal toolBusiness appPayment system
MitigationWAF + network isolationPartial controlsNo mitigations

For a CVSS 9.0 vulnerability with active exploitation (E=1.0), in a payment system (A=5), with no mitigations (M=1):

R=9.0×1.0×51=45 (Critical — immediate action)

False Positive Probability

With n independent scanners each having false positive rate fi, the combined false positive rate for a finding flagged by all scanners is:

Fcombined=i=1nfi

If Trivy, Grype, and Snyk each have a 10% false positive rate, a finding flagged by all three has only 0.13=0.1% false positive probability. This is why running multiple scanners improves signal quality.

Real-World War Stories

War Story — The Log4Shell Emergency (CVE-2021-44228)

On December 9, 2021, a critical remote code execution vulnerability in Apache Log4j 2 (Log4Shell, CVSS 10.0) was publicly disclosed. It affected virtually every Java application on earth. Organizations with automated dependency scanning in their CI pipelines identified affected services within hours. Those without it spent weeks manually auditing codebases.

Timeline at a well-prepared organization:

  • Hour 0: CVE published
  • Hour 1: Trivy DB updated with CVE
  • Hour 2: All CI pipelines flagged affected services (12 out of 45 microservices)
  • Hour 4: Patches applied to all 12 services, deployed through standard promotion pipeline
  • Hour 6: All production services patched

Timeline at an unprepared organization:

  • Day 0: CVE published
  • Day 1: Security team begins manual audit
  • Day 3: First services identified as affected
  • Day 5: First patches deployed (manually, with errors)
  • Day 14: All services patched (they hoped)
  • Day 30: Found three more affected services they'd missed

Lesson: Automated dependency scanning isn't overhead — it's your first line of defense when the next Log4Shell drops.

War Story — The Secret in the Docker Layer

A startup engineer committed an AWS access key in their application config, then "fixed" it in the next commit by replacing it with an environment variable reference. The Dockerfile copied the entire application directory including the git history. An attacker pulled the public image, extracted the layers, and found the key in a previous layer's filesystem diff.

Root cause: The .git directory was included in the Docker build context and embedded in a layer.

Triple failure:

  1. No .dockerignore excluding .git
  2. No pre-commit hook for secret detection
  3. No container scanning in CI

Fix: Added .dockerignore (excluding .git, .env, node_modules), Gitleaks pre-commit hook, Trivy container scanning with secret scanning enabled, and rotated all compromised credentials.

Decision Framework

Minimum Viable Security Pipeline

For teams just starting with security scanning:

PriorityToolWhat It CatchesEffort
1Gitleaks (pre-commit)Secrets before they enter history5 min setup
2npm audit / Trivy fsKnown dependency CVEs5 min setup
3Trivy containerOS + library vulnerabilities10 min setup
4SemgrepCode-level vulnerabilities15 min setup
5CheckovIaC misconfigurations15 min setup
6DAST (ZAP)Runtime vulnerabilities30 min setup

When to Block vs. Warn

Advanced Topics

Kubernetes Admission Control

Enforce security policies at deployment time:

yaml
# Kyverno policy — block unscanned images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-image-scan
spec:
  validationFailureAction: enforce
  background: true
  rules:
    - name: check-trivy-scan
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/myorg/*"
                    issuer: "https://token.actions.githubusercontent.com"
          attestations:
            - type: https://trivy.dev/scan/v1
              conditions:
                all:
                  - key: "criticalCount"
                    operator: Equals
                    value: "0"
                  - key: "highCount"
                    operator: LessThanOrEquals
                    value: "5"

SBOM-Driven Vulnerability Management

typescript
// scripts/sbom-monitor.ts — Continuous SBOM monitoring
interface SBOMPackage {
  name: string;
  version: string;
  purl: string;  // Package URL
  licenses: string[];
  supplier: string;
}

interface VulnerabilityAlert {
  cve: string;
  package: string;
  severity: string;
  affectedVersions: string;
  fixedVersion: string | null;
  publishedDate: Date;
}

class SBOMMonitor {
  // Continuously monitor SBOMs against new CVEs
  async checkForNewVulnerabilities(
    sbom: SBOMPackage[]
  ): Promise<VulnerabilityAlert[]> {
    const alerts: VulnerabilityAlert[] = [];

    for (const pkg of sbom) {
      // Query OSV (Open Source Vulnerabilities) database
      const response = await fetch('https://api.osv.dev/v1/query', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          package: { purl: pkg.purl },
        }),
      });

      const data = await response.json() as { vulns?: Array<{
        id: string;
        severity: Array<{ type: string; score: string }>;
        affected: Array<{ ranges: Array<{ events: Array<{ fixed?: string }> }> }>;
        published: string;
      }> };

      for (const vuln of data.vulns ?? []) {
        alerts.push({
          cve: vuln.id,
          package: pkg.name,
          severity: vuln.severity?.[0]?.score ?? 'unknown',
          affectedVersions: pkg.version,
          fixedVersion: vuln.affected?.[0]?.ranges?.[0]?.events
            ?.find(e => e.fixed)?.fixed ?? null,
          publishedDate: new Date(vuln.published),
        });
      }
    }

    return alerts;
  }
}

Zero-Trust CI/CD Pipeline

Security scanning is not a checkbox — it's a continuous discipline. The tools and patterns described here form the foundation of a secure software supply chain, complementing the CI/CD Overview principles and the Artifact Management practices that ensure your build outputs are trustworthy.

"What I cannot create, I do not understand." — Richard Feynman