Skip to content

Security Scanning in CI/CD

Wire security scanners into your pipelines to catch code-level vulnerabilities (SAST), runtime behaviour (DAST), and risks in containers and IaC.

  • SAST: Semgrep, CodeQL
  • DAST: OWASP ZAP, Nikto
  • Containers/IaC: Trivy, Grype, Checkov, kube-score

GitHub Actions: Example Workflow

name: security-scan
on:
  pull_request:
    branches: [ main ]

jobs:
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: semgrep/semgrep-action@v1
        with:
          config: "p/ci"
          generateSarif: true
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif

  containers:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .
      - name: Trivy scan
        uses: aquasecurity/trivy-action@0.20.0
        with:
          image-ref: myapp:${{ github.sha }}
          severity: HIGH,CRITICAL
          format: sarif
          output: trivy.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy.sarif

  iac:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Checkov IaC scan
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: .
          output_format: sarif
          output_file_path: checkov.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: checkov.sarif

Approval Policies

  • Require zero CRITICAL vulnerabilities before merging
  • Block deployments when policy-as-code fails (OPA/Gatekeeper, Kyverno)

DAST with OWASP ZAP

docker run -t owasp/zap2docker-stable zap-baseline.py \
  -t https://staging.myapp.example.com \
  -r zap-report.html --minlevel WARN

Best Practices

  • Run scans both on PRs and on releases
  • Export SARIF and surface alerts in the Security tab
  • Use ephemeral (isolated) runners for scans
  • Automate exceptions with an expiry date (never open-ended)