Series Navigation
← Part 10: SAST, DAST, SCA and Secrets Management
→ Part 12: Threat Modeling — STRIDE, PASTA and Attack Trees
The DevSecOps Security Gates
DevSecOps Pipeline Security Gates:
Developer Machine (pre-commit)
├── detect-secrets (prevent secret commits)
├── gitleaks (detect leaked credentials)
└── lint / format
Pull Request (CI)
├── SAST (Semgrep, Bandit, ESLint-security)
├── SCA (npm audit, pip-audit, Snyk)
├── IaC scan (Checkov, tfsec)
└── Secret scan (truffleHog)
Build Stage
├── Container scan (Trivy)
├── SBOM generation
└── Image signing (Cosign)
Staging Environment
├── DAST (OWASP ZAP)
└── Penetration test (automated)
Production Deploy
├── Policy check (OPA Gatekeeper)
└── Runtime security (Falco)
Production Monitoring
├── SIEM alerts
├── GuardDuty findings
└── Anomaly detection
Complete GitLab CI/CD Security Pipeline
# .gitlab-ci.yml
stages:
- pre-check
- sast
- sca
- build
- dast
- deploy
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
TRIVY_SEVERITY: HIGH,CRITICAL
SEMGREP_RULES: p/owasp-top-ten,p/python
# ── Stage: Pre-check ─────────────────────────────────────────────────────────
secret-detection:
stage: pre-check
image: trufflesecurity/trufflehog:latest
script:
- trufflehog git file://. --only-verified --fail
allow_failure: false # BLOCK PR on secret detection
# ── Stage: SAST ──────────────────────────────────────────────────────────────
semgrep:
stage: sast
image: returntocorp/semgrep
script:
- semgrep ci
--config "$SEMGREP_RULES"
--error # exit 1 on findings
--sarif-output semgrep.sarif
artifacts:
paths: [semgrep.sarif]
reports:
sast: semgrep.sarif
when: always
bandit:
stage: sast
image: python:3.11
script:
- pip install bandit
- bandit -r src/ -ll -f json -o bandit-report.json
- python3 -c "
import json, sys
data = json.load(open('bandit-report.json'))
highs = [i for i in data['results'] if i['issue_severity'] == 'HIGH']
print(f'High severity: {len(highs)}')
if highs: sys.exit(1)
"
artifacts:
paths: [bandit-report.json]
when: always
iac-scan:
stage: sast
image: bridgecrew/checkov:latest
script:
- checkov
-d infrastructure/
--framework terraform cloudformation
--output json > checkov-results.json
--soft-fail false
artifacts:
paths: [checkov-results.json]
when: always
# ── Stage: SCA ───────────────────────────────────────────────────────────────
dependency-scan-python:
stage: sca
image: python:3.11
script:
- pip install pip-audit
- pip-audit --format json > pip-audit-report.json
- python3 -c "
import json, sys
data = json.load(open('pip-audit-report.json'))
vulns = [v for d in data.get('dependencies',[]) for v in d.get('vulns',[])]
criticals = [v for v in vulns if v.get('fix_versions')]
print(f'Vulnerabilities: {len(vulns)}, fixable: {len(criticals)}')
if criticals: sys.exit(1)
"
artifacts:
paths: [pip-audit-report.json]
dependency-scan-node:
stage: sca
image: node:18
script:
- npm ci
- npm audit --audit-level=high --json > npm-audit.json || true
- |
HIGH=$(cat npm-audit.json | python3 -c "
import json,sys; d=json.load(sys.stdin)
print(d.get('metadata',{}).get('vulnerabilities',{}).get('high',0))
")
CRITICAL=$(cat npm-audit.json | python3 -c "
import json,sys; d=json.load(sys.stdin)
print(d.get('metadata',{}).get('vulnerabilities',{}).get('critical',0))
")
echo "High: $HIGH, Critical: $CRITICAL"
if [ "$CRITICAL" -gt "0" ]; then exit 1; fi
# ── Stage: Build ─────────────────────────────────────────────────────────────
build-image:
stage: build
image: docker:latest
services: [docker:dind]
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
trivy-scan:
stage: build
image: aquasec/trivy:latest
needs: [build-image]
script:
- trivy image
--exit-code 1
--severity $TRIVY_SEVERITY
--format sarif
--output trivy-results.sarif
$DOCKER_IMAGE
artifacts:
paths: [trivy-results.sarif]
reports:
container_scanning: trivy-results.sarif
when: always
sign-image:
stage: build
needs: [trivy-scan] # only sign if scan passes
image: gcr.io/projectsigstore/cosign:latest
script:
# Sign image with keyless signing (OIDC + Sigstore Rekor transparency log)
- cosign sign --yes $DOCKER_IMAGE
# ── Stage: DAST ──────────────────────────────────────────────────────────────
zap-baseline:
stage: dast
image: owasp/zap2docker-stable
needs: [] # run in parallel with build stage
script:
- zap-baseline.py
-t $STAGING_URL
-r zap-report.html
-J zap-report.json
-I # don't fail on warnings, only errors
artifacts:
paths: [zap-report.html, zap-report.json]
when: always
# ── Stage: Deploy ─────────────────────────────────────────────────────────────
deploy-production:
stage: deploy
needs: [semgrep, bandit, trivy-scan, zap-baseline] # all must pass
environment: production
script:
- kubectl set image deployment/app app=$DOCKER_IMAGE
- kubectl rollout status deployment/app --timeout=5m
rules:
- if: $CI_COMMIT_BRANCH == "main"
GitHub Actions Security Pipeline
# .github/workflows/security.yml
name: Security Pipeline
on: [push, pull_request]
permissions:
contents: read # restrict default permissions
security-events: write # for SARIF uploads
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: p/owasp-top-ten p/python
- name: Run Bandit
run: |
pip install bandit
bandit -r src/ -ll -f sarif -o bandit.sarif
continue-on-error: true
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: bandit.sarif
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for secret scanning
- name: TruffleHog Scan
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v3
with:
fail-on-severity: high
container-security:
runs-on: ubuntu-latest
needs: []
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t ${{ github.repository }}:${{ github.sha }} .
- name: Run Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ github.repository }}:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
exit-code: '1'
- name: Upload Trivy SARIF
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: trivy-results.sarif
Security as Code — Open Policy Agent (OPA)
# Kubernetes admission policy using OPA Gatekeeper
# Deny pods running as root
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := sprintf("Container '%v' must set runAsNonRoot: true", [container.name])
}
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
container.securityContext.allowPrivilegeEscalation == true
msg := sprintf("Container '%v' must not allow privilege escalation", [container.name])
}
deny[msg] {
input.request.kind.kind == "Pod"
not input.request.object.spec.securityContext.runAsNonRoot
msg := "Pod must set runAsNonRoot at pod level"
}
# Block images without digest (must be immutable)
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not contains(container.image, "@sha256:")
not endswith(container.image, ":latest") # also block :latest
msg := sprintf("Container '%v' must use image digest", [container.name])
}
Security Metrics to Track
Key DevSecOps Metrics:
│
├── Mean Time to Remediate (MTTR) — how fast are vulns fixed?
├── Vulnerability Density — vulns per 1000 lines of code
├── Security Debt — backlog of known unfixed vulns
├── Critical Vuln Aging — how old are critical findings?
├── Pipeline Security Gate Pass Rate— how many PRs blocked?
├── False Positive Rate — how noisy are your tools?
└── Mean Time to Detect (MTTD) — how fast do we find issues?
What's Next
In Part 12 we threat model like a professional — STRIDE methodology, attack trees, data flow diagrams, and how to run threat modeling sessions that actually prevent real attacks.
Discussion
Loading...Leave a Comment
All comments are reviewed before appearing. No links please.