Security scanning is not a single tool or a single stage. A comprehensive approach covers four phases: static analysis of your source code (SAST), scanning of your dependencies for known vulnerabilities, scanning of your built container images, and runtime monitoring of your deployed containers. Each phase catches different categories of vulnerabilities, and relying on any single phase leaves significant blind spots.

This guide walks through building a practical, automated security scanning pipeline that covers all four phases, with concrete CI/CD integration examples and policy enforcement patterns.

The Four Phases of Security Scanning

Phase What It Scans What It Catches Tools
SAST Your source code Code-level vulnerabilities (SQL injection, XSS, hardcoded secrets) Semgrep, CodeQL, gosec
Dependency scanning Libraries and packages Known CVEs in dependencies Trivy, Grype, npm audit
Image scanning Container images OS package CVEs, misconfigurations, secrets in layers Trivy, Grype, Snyk
Runtime scanning Running containers Anomalous behavior, exploit attempts, policy violations Falco, Tracee, Sysdig

Phase 1: Static Application Security Testing (SAST)

Semgrep

Semgrep is an open-source static analysis tool that supports 30+ languages. It uses pattern-matching rules that are easy to write and understand, unlike regex-based tools.

# Install Semgrep
pip install semgrep

# Run with default rules for your language
semgrep --config auto .

# Run with specific rule sets
semgrep --config p/security-audit .
semgrep --config p/owasp-top-ten .
semgrep --config p/docker .

# Output in JSON for pipeline processing
semgrep --config auto --json --output results.json .

# Run only on changed files (faster in CI)
git diff --name-only HEAD~1 | xargs semgrep --config auto

Semgrep rules are YAML files with a pattern-matching syntax. You can write custom rules for your codebase:

# .semgrep/custom-rules.yml
rules:
  - id: hardcoded-password
    patterns:
      - pattern: |
          password = "..."
      - pattern-not: |
          password = ""
      - pattern-not: |
          password = "changeme"
    message: "Hardcoded password detected"
    severity: ERROR
    languages: [python, javascript, go]

  - id: docker-socket-mount
    pattern: |
      volumes:
        - ...
        - /var/run/docker.sock:/var/run/docker.sock
        - ...
    message: "Docker socket mounted - ensure this is necessary and secured"
    severity: WARNING
    languages: [yaml]

  - id: no-resource-limits
    pattern: |
      services:
        ...:
          image: ...
    pattern-not: |
      services:
        ...:
          deploy:
            resources:
              limits: ...
    message: "Container has no resource limits defined"
    severity: WARNING
    languages: [yaml]

Language-Specific SAST

# Go: gosec
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...

# Python: bandit
pip install bandit
bandit -r . -f json -o bandit-results.json

# JavaScript/TypeScript: eslint-plugin-security
npm install eslint-plugin-security --save-dev
# Add to .eslintrc: plugins: ["security"]

Phase 2: Dependency Scanning

Trivy for Dependencies

# Install Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

# Scan filesystem for dependency vulnerabilities
trivy fs --scanners vuln .

# Scan specific lock files
trivy fs --scanners vuln ./go.sum
trivy fs --scanners vuln ./package-lock.json
trivy fs --scanners vuln ./requirements.txt

# Output in table format (human-readable)
trivy fs --scanners vuln --severity HIGH,CRITICAL .

# Output in JSON (for pipeline processing)
trivy fs --scanners vuln --format json --output trivy-deps.json .

# Fail the build on critical vulnerabilities
trivy fs --scanners vuln --severity CRITICAL --exit-code 1 .

Grype for Dependencies

# Install Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

# Scan a directory
grype dir:.

# Scan specific lock files
grype sbom:./sbom.spdx.json

# Generate an SBOM first (with Syft), then scan
syft . -o spdx-json > sbom.spdx.json
grype sbom:sbom.spdx.json

# Fail on high severity
grype dir:. --fail-on high
Tip: Generate a Software Bill of Materials (SBOM) as part of your build process. Tools like Syft can create SBOMs in SPDX or CycloneDX format. This allows you to retroactively scan deployed software when new CVEs are disclosed, without needing to rebuild.

Phase 3: Container Image Scanning

Trivy Image Scanning

# Scan a Docker image
trivy image nginx:alpine

# Scan a locally built image
docker build -t my-app:latest .
trivy image my-app:latest

# Scan for vulnerabilities AND misconfigurations
trivy image --scanners vuln,misconfig my-app:latest

# Scan for secrets embedded in image layers
trivy image --scanners secret my-app:latest

# Comprehensive scan with all scanners
trivy image --scanners vuln,misconfig,secret --severity HIGH,CRITICAL my-app:latest

# Output as SARIF (for GitHub Security tab)
trivy image --format sarif --output trivy-results.sarif my-app:latest

# Scan with a policy file
trivy image --severity CRITICAL --exit-code 1 my-app:latest

Grype Image Scanning

# Scan a Docker image
grype nginx:alpine

# Scan a local image
grype docker:my-app:latest

# Scan an image from a registry (without pulling)
grype registry:ghcr.io/myorg/my-app:latest

# Scan and fail on high severity
grype docker:my-app:latest --fail-on high

# Output in different formats
grype docker:my-app:latest -o json > grype-results.json
grype docker:my-app:latest -o table
grype docker:my-app:latest -o cyclonedx-json > sbom.json

Comparing Trivy and Grype

Feature Trivy Grype
Vulnerability database NVD, vendor advisories, GitHub NVD, vendor advisories, GitHub
Misconfiguration scanning Yes (Dockerfile, K8s, Terraform) No (use separate tool)
Secret detection Yes No
SBOM generation Yes No (use Syft)
Scan speed Fast Very fast
CI/CD integration GitHub Actions, GitLab CI, Jenkins GitHub Actions, GitLab CI

Recommendation: Use Trivy as your primary scanner for its breadth (vulnerabilities, misconfigurations, secrets in one tool). Use Grype as a secondary scanner for its speed and different vulnerability database. Running both catches more issues than either alone.

Phase 4: Runtime Scanning with Falco

Falco monitors running containers for anomalous behavior by intercepting system calls at the kernel level. It detects activities like unexpected process execution, file system modifications, network connections, and privilege escalation attempts.

# Install Falco
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc | sudo gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" | sudo tee /etc/apt/sources.list.d/falcosecurity.list
sudo apt update && sudo apt install -y falco

# Or run Falco in a Docker container
docker run -d \
  --name falco \
  --privileged \
  -v /var/run/docker.sock:/host/var/run/docker.sock \
  -v /proc:/host/proc:ro \
  -v /boot:/host/boot:ro \
  -v /lib/modules:/host/lib/modules:ro \
  -v /etc:/host/etc:ro \
  falcosecurity/falco:latest

Falco Rules

# /etc/falco/falco_rules.local.yaml

# Detect shell spawned in a container
- rule: Shell Spawned in Container
  desc: A shell was spawned in a container
  condition: >
    spawned_process and container and
    proc.name in (bash, sh, zsh, dash, ksh)
  output: >
    Shell spawned in container
    (user=%user.name container=%container.name
     image=%container.image.repository
     shell=%proc.name parent=%proc.pname
     cmdline=%proc.cmdline)
  priority: WARNING
  tags: [container, shell]

# Detect sensitive file read in container
- rule: Read Sensitive File in Container
  desc: Sensitive file was read inside a container
  condition: >
    open_read and container and
    fd.name in (/etc/shadow, /etc/passwd, /etc/sudoers)
  output: >
    Sensitive file read in container
    (file=%fd.name container=%container.name
     image=%container.image.repository user=%user.name)
  priority: ERROR
  tags: [container, filesystem]

# Detect outbound connection from database container
- rule: Unexpected Outbound Connection from Database
  desc: Database container made an outbound network connection
  condition: >
    outbound and container and
    container.image.repository contains "postgres"
  output: >
    Database container made outbound connection
    (container=%container.name ip=%fd.sip port=%fd.sport)
  priority: CRITICAL
  tags: [container, network]

Falco Alerting

# /etc/falco/falco.yaml
# Send alerts to multiple outputs

# Stdout (for container log collection)
stdout_output:
  enabled: true

# File output
file_output:
  enabled: true
  filename: /var/log/falco/events.json
  keep_alive: false

# HTTP webhook
http_output:
  enabled: true
  url: https://hooks.slack.com/services/YOUR/WEBHOOK/URL

# gRPC output (for Falco Sidekick)
grpc:
  enabled: true
  bind_address: "0.0.0.0:5060"

CI/CD Integration: GitHub Actions

# .github/workflows/security-scan.yml
name: Security Scanning Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  sast:
    name: Static Analysis
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Semgrep
        uses: semgrep/semgrep-action@v1
        with:
          config: >-
            p/security-audit
            p/owasp-top-ten
          generateSarif: true

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif

  dependency-scan:
    name: Dependency Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-fs.sarif

  image-scan:
    name: Container Image Scan
    runs-on: ubuntu-latest
    needs: [sast, dependency-scan]
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t my-app:${{ github.sha }} .

      - name: Run Trivy (image)
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'my-app:${{ github.sha }}'
          severity: 'HIGH,CRITICAL'
          format: 'sarif'
          output: 'trivy-image.sarif'
          exit-code: '1'

      - name: Run Grype (image)
        uses: anchore/scan-action@v4
        with:
          image: 'my-app:${{ github.sha }}'
          fail-build: true
          severity-cutoff: high

      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-image.sarif

  generate-sbom:
    name: Generate SBOM
    runs-on: ubuntu-latest
    needs: [image-scan]
    steps:
      - name: Generate SBOM with Syft
        uses: anchore/sbom-action@v0
        with:
          image: 'my-app:${{ github.sha }}'
          format: spdx-json
          artifact-name: sbom.spdx.json

Policy Enforcement

Scanning without enforcement is informational at best. Define clear policies for what blocks a deployment:

# .trivyignore - Accepted vulnerabilities with justification
# CVE-2024-XXXXX: False positive, not exploitable in our configuration
CVE-2024-XXXXX

# CVE-2024-YYYYY: Vendor patch pending, mitigated by WAF rule
CVE-2024-YYYYY
# trivy.yaml - Policy configuration
severity:
  - CRITICAL
  - HIGH

# Block deployments with:
# - Any CRITICAL vulnerability
# - HIGH vulnerabilities older than 30 days
# - Secrets in image layers
# - Running as root without justification
Warning: Start with policy enforcement in "warn" mode before switching to "block" mode. A pipeline that blocks every build on day one due to existing vulnerabilities will be immediately disabled by frustrated developers. Establish a vulnerability baseline, fix what you can, accept what you cannot (with documented justification), and then enforce.

Vulnerability Management Workflow

  1. Scan: Automated scanning in CI/CD and on a schedule for deployed images
  2. Triage: Classify vulnerabilities by severity, exploitability, and relevance to your deployment
  3. Fix: Update dependencies, rebuild base images, or apply mitigations
  4. Accept: Document accepted risks with justification and review dates
  5. Monitor: Re-scan regularly as new CVEs are published
Tip: Container management platforms like usulnet integrate Trivy scanning directly, allowing you to monitor the vulnerability status of all running containers from a single dashboard. This closes the gap between CI/CD-time scanning and production monitoring, ensuring you know when a newly discovered CVE affects your running infrastructure.

A complete security scanning pipeline catches vulnerabilities at every stage of the software lifecycle. SAST catches bugs before they are committed. Dependency scanning catches known CVEs in libraries. Image scanning catches issues in your production artifacts. Runtime scanning catches exploitation attempts in production. No single tool or phase is sufficient alone, but together they provide comprehensive coverage that dramatically reduces your security risk.