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 SettingsSAST — 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.sarifIaC 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_onlyDAST — 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 == 429Common 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?
Related reading