Security Automation

Automating security checks as part of CI/CD — SAST (static analysis), DAST (dynamic scanning), dependency auditing, container scanning, and secrets detection.

Automating security checks as part of CI/CD. SAST (static analysis), DAST (dynamic scanning), dependency auditing, container scanning, and secrets detection. Shift-left security means catching vulnerabilities before they reach production.


Security Pipeline Layers

Pre-commit
  └── secrets detection (detect-secrets, gitleaks)
  └── SAST lint (ruff/bandit rules)

Pull Request
  └── SAST (Semgrep, Bandit, CodeQL)
  └── Dependency scan (Trivy, safety, npm audit)
  └── Container image scan (Trivy)
  └── IaC scan (checkov, tfsec)

Staging deploy
  └── DAST (ZAP baseline scan)
  └── API security scan

Production
  └── Continuous penetration testing (Bug bounty, scheduled pen test)
  └── WAF rules (AWS WAF, Cloudflare)
  └── Runtime protection (Falco on Kubernetes)

Secrets Detection

# detect-secrets — baseline approach
detect-secrets scan > .secrets.baseline
detect-secrets audit .secrets.baseline

# pre-commit hook (in .pre-commit-config.yaml)
- repo: https://github.com/Yelp/detect-secrets
  rev: v1.4.0
  hooks:
  - id: detect-secrets
    args: ['--baseline', '.secrets.baseline']

# gitleaks — scan entire git history
docker run -v "${PWD}:/path" zricethezav/gitleaks:latest \
  detect --source="/path" --report-format=json --report-path=/path/gitleaks-report.json

# GitHub — secret scanning is automatic in public repos; enable for private via Settings

SAST — Semgrep

# .github/workflows/semgrep.yaml
- name: Semgrep SAST
  uses: semgrep/semgrep-action@v1
  with:
    config: >-
      p/owasp-top-ten
      p/python
      p/django
      p/jwt
      p/sql-injection
  env:
    SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
# Local scan
semgrep --config "p/owasp-top-ten" src/

# Custom rule example
# .semgrep/rules.yaml
rules:
- id: hardcoded-token
  pattern: |
    $VAR = "sk-..."
  message: "Possible hardcoded API token in $VAR"
  severity: ERROR
  languages: [python]

Dependency Scanning — Trivy

# Scan Python dependencies
trivy fs --security-checks vuln --format table .

# Scan Docker image
trivy image --severity HIGH,CRITICAL myregistry/myapp:latest

# Fail CI if CRITICAL vulnerabilities found
trivy image --exit-code 1 --severity CRITICAL myregistry/myapp:latest

# SBOM generation (Software Bill of Materials)
trivy image --format cyclonedx --output sbom.json myregistry/myapp:latest
# GitHub Actions
- name: Trivy vulnerability scan
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myregistry/myapp:${{ github.sha }}
    format: sarif
    output: trivy-results.sarif
    exit-code: '1'
    ignore-unfixed: true
    severity: CRITICAL,HIGH

- name: Upload to GitHub Security tab
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: trivy-results.sarif

IaC Scanning — Checkov

pip install checkov

# Scan Terraform
checkov -d terraform/ --framework terraform --soft-fail

# Scan Kubernetes manifests
checkov -d k8s/ --framework kubernetes --output json

# Scan Dockerfile
checkov -f Dockerfile --framework dockerfile

# GitHub Actions
- name: Checkov IaC scan
  uses: bridgecrewio/checkov-action@master
  with:
    directory: terraform/
    framework: terraform
    soft_fail: true
    output_format: github_failed_only

DAST — OWASP ZAP in CI

# API scan against OpenAPI spec
- name: ZAP API Scan
  uses: zaproxy/action-api-scan@v0.7.0
  with:
    target: 'https://staging.myapp.com/api/openapi.json'
    format: openapi
    fail_action: true
    cmd_options: '-I'   # ignore warning alerts
  env:
    ZAP_AUTH_HEADER: 'Authorization'
    ZAP_AUTH_HEADER_VALUE: 'Bearer ${{ secrets.STAGING_API_TOKEN }}'

# Full site scan
- name: ZAP Baseline Scan
  uses: zaproxy/action-baseline@v0.12.0
  with:
    target: 'https://staging.myapp.com'
    rules_file_name: '.zap/rules.tsv'
    cmd_options: '-a'   # include alpha rules
# .zap/rules.tsv — customise which rules fail CI
# Format: rule_id TAB action (IGNORE/WARN/FAIL)
10016	IGNORE    # Web Browser XSS Protection Not Enabled (deprecated header)
10035	WARN      # Strict-Transport-Security header not set (staging-only)
40012	FAIL      # Cross Site Scripting Reflected
40014	FAIL      # Cross Site Scripting Persistent

Falco — Runtime Security (Kubernetes)

# Falco rule — alert on container writing to /etc
- rule: Write below etc
  desc: Detect writes to /etc
  condition: >
    open_write and container and fd.name startswith /etc
    and not proc.name in (known_etc_writers)
  output: >
    File opened for writing below /etc
    (user=%user.name command=%proc.cmdline file=%fd.name container=%container.id)
  priority: ERROR
  tags: [filesystem, mitre_persistence]

Automated Security Test Cases

# tests/security/test_auth.py
def test_jwt_expiry_rejected(client):
    expired_token = create_jwt(expires_delta=timedelta(seconds=-1))
    response = client.get("/api/me", headers={"Authorization": f"Bearer {expired_token}"})
    assert response.status_code == 401

def test_csrf_protection_active(client, session_cookie):
    # POST without CSRF token must be rejected
    response = client.post("/api/profile", cookies={"session": session_cookie})
    assert response.status_code in (400, 403)

def test_rate_limit_enforced(client):
    for _ in range(100):
        client.post("/api/auth/login", json={"email": "test@test.com", "password": "wrong"})
    response = client.post("/api/auth/login", json={"email": "test@test.com", "password": "wrong"})
    assert response.status_code == 429

Common Failure Cases

Secrets detection baseline goes stale and stops catching new secrets Why: detect-secrets compares against .secrets.baseline; once a real secret is committed and added to the baseline to silence the alert, it is permanently ignored. Detect: .secrets.baseline grows over time with entries that are marked is_secret: false without a comment explaining why. Fix: treat every baseline entry as a finding to investigate; rotate any credential that was ever committed, then add it to the baseline only after rotation.

DAST scan authenticates but loses the session mid-scan Why: ZAP's authenticated scan uses a login script that obtains a token; if the token expires during a long scan, subsequent requests return 401 and ZAP reports false negatives (pages appear to return errors, not vulnerabilities). Detect: ZAP scan log shows a spike in 401 responses after the first few minutes; coverage drops sharply for auth-protected routes. Fix: configure a token refresh script in ZAP or set ZAP_AUTH_HEADER_VALUE to a long-lived staging token; verify post-scan that authenticated routes were actually reached.

Trivy exits 0 on ignore-unfixed: true even when CRITICAL CVEs exist with available fixes Why: ignore-unfixed suppresses all unfixed vulnerabilities regardless of severity; if a fix ships to the base image but the image has not been rebuilt, the CVE now has a fix but the flag still suppresses it. Detect: run Trivy without ignore-unfixed in a scheduled job and diff against the gated CI run to find newly fixable CVEs. Fix: use ignore-unfixed: false in the gated CI job and a separate scheduled job with ignore-unfixed: true to track upstream-blocked items separately.

Semgrep custom rules match too broadly and create alert fatigue Why: rules using overly generic patterns (e.g., any string assignment) produce hundreds of findings per run; engineers start ignoring the entire output. Detect: PR pipelines show 50+ Semgrep findings; engineers add # nosemgrep comments indiscriminately. Fix: scope custom rules with metavariable-regex constraints and set severity: WARNING rather than ERROR until the rule is tuned; track suppression comment growth as a metric.

Connections

tqa-hub · qa/security-testing-qa · cloud/cloud-security · security/guardrails · cloud/github-actions · cloud/kubernetes · cs-fundamentals/auth-patterns

Open Questions

  • What is the most common failure mode when implementing this at scale?
  • How does this testing approach need to adapt for distributed or microservice architectures?