0 / 12 steps complete
Advanced AI-Assisted DevSecOps

L19: DevSecOps — Secure CI/CD Pipeline

Build a security-integrated CI/CD pipeline using GitHub Actions, Semgrep (SAST), OWASP Dependency-Check (SCA), and OWASP ZAP (DAST). Learn to shift security left — catching vulnerabilities in code before they reach production — and write custom Semgrep rules for your application stack.

GitHub Actions Semgrep OWASP ZAP Docker Python/Node.js Kali Linux
Phase 1: Vulnerable Application Setup
1 Set up vulnerable test application

Create a deliberately vulnerable Python Flask application to practice scanning against:

mkdir ~/devsecops-lab && cd ~/devsecops-lab git init mkdir src tests cat > src/app.py << 'EOF' # DELIBERATELY VULNERABLE APP — FOR LAB USE ONLY from flask import Flask, request, render_template_string, redirect import sqlite3, subprocess, os, pickle, hashlib app = Flask(__name__) app.secret_key = "hardcoded_secret_key_12345" # VULN: CWE-798 DB = '/tmp/lab.db' def get_db(): conn = sqlite3.connect(DB) return conn @app.route('/login', methods=['POST']) def login(): user = request.form['username'] pwd = request.form['password'] # VULN: SQL Injection (CWE-89) conn = get_db() query = f"SELECT * FROM users WHERE username='{user}' AND password='{pwd}'" result = conn.execute(query).fetchone() return "ok" if result else "fail" @app.route('/render') def render(): # VULN: Server-Side Template Injection (CWE-94) template = request.args.get('template', '') return render_template_string(template) @app.route('/ping') def ping(): # VULN: Command Injection (CWE-78) host = request.args.get('host', 'localhost') result = subprocess.run(f"ping -c 1 {host}", shell=True, capture_output=True, text=True) return result.stdout @app.route('/load') def load(): # VULN: Insecure Deserialization (CWE-502) data = request.args.get('data', '') obj = pickle.loads(bytes.fromhex(data)) return str(obj) @app.route('/hash') def weak_hash(): # VULN: Weak cryptography (CWE-327) data = request.args.get('data', '') return hashlib.md5(data.encode()).hexdigest() if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) EOF cat > requirements.txt << 'EOF' flask==2.0.1 requests==2.26.0 cryptography==3.3.0 PyYAML==5.3.1 Pillow==8.2.0 EOF echo "Vulnerable app created — DO NOT deploy outside lab environment"
2 Run Semgrep SAST scan on source code
pip3 install semgrep cd ~/devsecops-lab # Run Semgrep with auto-rules (OWASP Top 10 + security audit) semgrep scan \ --config=p/owasp-top-ten \ --config=p/python \ --config=p/secrets \ --json \ --output=reports/semgrep_results.json \ src/ # Human-readable output semgrep scan \ --config=p/owasp-top-ten \ --config=p/python \ --config=p/secrets \ src/ 2>&1 | tee reports/semgrep_readable.txt echo "Semgrep findings:" python3 -c " import json with open('reports/semgrep_results.json') as f: results = json.load(f) findings = results.get('results', []) print(f'Total findings: {len(findings)}') by_severity = {} for f in findings: sev = f.get('extra', {}).get('severity', 'INFO') by_severity[sev] = by_severity.get(sev, 0) + 1 for sev, count in sorted(by_severity.items()): print(f' {sev}: {count}') "
3 Write custom Semgrep rules for your stack
mkdir -p ~/devsecops-lab/semgrep_rules cat > ~/devsecops-lab/semgrep_rules/flask_security.yml << 'EOF' rules: - id: flask-debug-mode patterns: - pattern: | app.run(..., debug=True, ...) message: > Flask debug mode is enabled. This exposes the Werkzeug debugger, which allows arbitrary code execution. Never enable in production. languages: [python] severity: ERROR metadata: cwe: CWE-94 owasp: A05:2021 - Security Misconfiguration mitre: T1190 - id: flask-hardcoded-secret patterns: - pattern: | $APP.secret_key = "..." - pattern: | $APP.secret_key = '...' message: > Hardcoded Flask secret key detected. Use environment variables instead: app.secret_key = os.environ.get('SECRET_KEY') languages: [python] severity: ERROR metadata: cwe: CWE-798 owasp: A02:2021 - Cryptographic Failures - id: sqlite-string-format patterns: - pattern: | $CONN.execute(f"...{$VAR}...") - pattern: | $CONN.execute("..." + $VAR + "...") message: > String formatting in SQL query — potential SQL injection. Use parameterized queries: conn.execute("SELECT * WHERE x=?", (value,)) languages: [python] severity: ERROR metadata: cwe: CWE-89 owasp: A03:2021 - Injection mitre: T1190 - id: subprocess-shell-true patterns: - pattern: | subprocess.run(..., shell=True, ...) - pattern: | subprocess.call(..., shell=True, ...) - pattern: | os.system($X) message: > Command executed with shell=True or os.system — potential command injection if user input is included. Use shell=False and pass args as list. languages: [python] severity: ERROR metadata: cwe: CWE-78 owasp: A03:2021 - Injection mitre: T1059.006 - id: pickle-deserialization patterns: - pattern: | pickle.loads($DATA) - pattern: | pickle.load($FILE) message: > Deserialization of untrusted data with pickle. Pickle can execute arbitrary code. Use JSON or msgpack instead. languages: [python] severity: ERROR metadata: cwe: CWE-502 owasp: A08:2021 - Software and Data Integrity Failures - id: weak-hash-md5 patterns: - pattern: hashlib.md5(...) - pattern: hashlib.sha1(...) message: > MD5 and SHA1 are cryptographically weak. Use SHA-256 or better: hashlib.sha256(data).hexdigest() languages: [python] severity: WARNING metadata: cwe: CWE-327 owasp: A02:2021 - Cryptographic Failures EOF # Test custom rules semgrep --config ~/devsecops-lab/semgrep_rules/flask_security.yml src/
Phase 2: Software Composition Analysis (SCA)
4 Run OWASP Dependency-Check for vulnerable libraries

Scan your requirements.txt for known CVEs in dependencies:

# Install OWASP Dependency-Check (Java required) sudo apt install -y default-jre wget -O /tmp/dc.zip \ https://github.com/jeremylong/DependencyCheck/releases/download/v9.0.7/dependency-check-9.0.7-release.zip unzip /tmp/dc.zip -d /opt/ export PATH=$PATH:/opt/dependency-check/bin # Run SCA scan on Python dependencies dependency-check.sh \ --project "DevSecOps Lab" \ --scan ~/devsecops-lab/requirements.txt \ --format HTML \ --format JSON \ --out ~/devsecops-lab/reports/ \ --enableExperimental # Alternative: pip-audit (simpler, Python-native) pip3 install pip-audit pip-audit -r ~/devsecops-lab/requirements.txt \ --format json > ~/devsecops-lab/reports/pip_audit.json python3 -c " import json with open('reports/pip_audit.json') as f: data = json.load(f) vulns = data.get('vulnerabilities', []) print(f'Vulnerable packages: {len(set(v[\"package\"] for v in vulns))}') for v in vulns: print(f' {v[\"package\"]} {v[\"installed_version\"]}: {v[\"id\"]}') "
5 Scan Docker images for CVEs with Trivy
# Create Dockerfile for the lab app cat > ~/devsecops-lab/Dockerfile << 'EOF' FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY src/ . EXPOSE 5000 CMD ["python", "app.py"] EOF # Build the image docker build -t devsecops-lab:latest ~/devsecops-lab/ # Install Trivy (container image scanner) wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - echo "deb https://aquasecurity.github.io/trivy-repo/deb generic main" | \ sudo tee -a /etc/apt/sources.list.d/trivy.list sudo apt update && sudo apt install -y trivy # Scan the Docker image trivy image \ --format json \ --output ~/devsecops-lab/reports/trivy_image.json \ devsecops-lab:latest trivy image --severity HIGH,CRITICAL devsecops-lab:latest # Scan base OS packages too trivy image --scanners vuln,config,secret devsecops-lab:latest
Phase 3: Dynamic Analysis (DAST) with OWASP ZAP
6 Run OWASP ZAP baseline scan against running app
# Start the vulnerable app cd ~/devsecops-lab && python3 src/app.py & APP_PID=$! sleep 3 # Wait for app to start # Run OWASP ZAP baseline scan (Docker — easiest method) docker run --rm \ --network="host" \ -v ~/devsecops-lab/reports:/zap/wrk:rw \ ghcr.io/zaproxy/zaproxy:stable \ zap-baseline.py \ -t http://localhost:5000 \ -r zap_baseline_report.html \ -J zap_baseline_report.json \ -l WARN # Full active scan (finds more, takes longer) docker run --rm \ --network="host" \ -v ~/devsecops-lab/reports:/zap/wrk:rw \ ghcr.io/zaproxy/zaproxy:stable \ zap-full-scan.py \ -t http://localhost:5000 \ -r zap_full_report.html \ -J zap_full_report.json kill $APP_PID 2>/dev/null echo "ZAP scan complete — see reports/zap_*.html"
7 Analyze ZAP findings and CVSS scoring
python3 << 'EOF' import json try: with open('reports/zap_full_report.json') as f: data = json.load(f) except FileNotFoundError: print("ZAP report not found — run Step 6 first") exit() alerts = data.get('site', [{}])[0].get('alerts', []) print(f"Total ZAP alerts: {len(alerts)}") # Risk breakdown risk_levels = {'High': [], 'Medium': [], 'Low': [], 'Informational': []} for alert in alerts: risk = alert.get('riskdesc', 'Informational').split()[0] risk_levels.get(risk, risk_levels['Informational']).append(alert) for risk, items in risk_levels.items(): print(f"\n[{risk.upper()}] ({len(items)} findings)") for item in items[:3]: print(f" - {item.get('alert', '')}") print(f" CWE: {item.get('cweid', 'N/A')} | WASC: {item.get('wascid', 'N/A')}") print(f" URL: {item.get('instances', [{}])[0].get('uri', 'N/A')}") print(f" Solution: {item.get('solution','')[:100]}") EOF
Phase 4: GitHub Actions CI/CD Pipeline
8 Build GitHub Actions security pipeline
mkdir -p ~/devsecops-lab/.github/workflows cat > ~/devsecops-lab/.github/workflows/devsecops.yml << 'EOF' name: DevSecOps Security Pipeline on: push: branches: [main, develop] pull_request: branches: [main] jobs: # SAST — Static Analysis sast: name: SAST - Semgrep runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Semgrep SAST uses: semgrep/semgrep-action@v1 with: config: >- p/owasp-top-ten p/python p/secrets env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} - name: Upload SARIF results uses: github/codeql-action/upload-sarif@v3 with: sarif_file: semgrep.sarif # SCA — Dependency Scanning sca: name: SCA - Dependency Audit runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: pip-audit run: | pip install pip-audit pip-audit -r requirements.txt \ --format cyclonedx-json \ --output dependency-report.json || true - name: Upload dependency report uses: actions/upload-artifact@v4 with: name: dependency-report path: dependency-report.json # Container Scanning container: name: Container - Trivy Scan runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build Docker image run: docker build -t app:${{ github.sha }} . - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: image-ref: 'app:${{ github.sha }}' format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' exit-code: '1' # Fail pipeline on CRITICAL/HIGH - name: Upload Trivy SARIF uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' # DAST — Dynamic Testing dast: name: DAST - OWASP ZAP runs-on: ubuntu-latest needs: [sast, sca] # Only run DAST if SAST/SCA pass steps: - uses: actions/checkout@v4 - name: Start application run: | pip install -r requirements.txt python src/app.py & sleep 5 # Wait for startup - name: ZAP Baseline Scan uses: zaproxy/action-baseline@v0.11.0 with: target: 'http://localhost:5000' fail_action: true artifact_name: 'zap-results' allow_issue_writing: false # Security gate — aggregate and enforce thresholds security-gate: name: Security Gate runs-on: ubuntu-latest needs: [sast, sca, container, dast] steps: - name: Evaluate security posture run: | echo "All security scans passed!" echo "SAST: Semgrep — passed" echo "SCA: pip-audit — passed" echo "Container: Trivy — passed" echo "DAST: ZAP — passed" echo "Deployment approved." EOF echo "GitHub Actions pipeline created at .github/workflows/devsecops.yml"
9 Fix vulnerabilities and verify with re-scan

Fix the SQL injection and command injection vulnerabilities found by Semgrep:

cat > ~/devsecops-lab/src/app_fixed.py << 'EOF' # FIXED version of the vulnerable app from flask import Flask, request import sqlite3, subprocess, os app = Flask(__name__) # FIX: Secret from environment variable app.secret_key = os.environ.get('SECRET_KEY', os.urandom(32)) def get_db(): return sqlite3.connect('/tmp/lab.db') @app.route('/login', methods=['POST']) def login(): user = request.form.get('username', '') pwd = request.form.get('password', '') # FIX: Parameterized query — no SQL injection conn = get_db() result = conn.execute( "SELECT * FROM users WHERE username=? AND password=?", (user, pwd)).fetchone() return "ok" if result else "fail" @app.route('/ping') def ping(): host = request.args.get('host', '') # FIX: Validate input, no shell=True, args as list import re if not re.match(r'^[a-zA-Z0-9.-]+$', host): return "Invalid host", 400 result = subprocess.run( ['ping', '-c', '1', host], capture_output=True, text=True, timeout=5) return result.stdout @app.route('/hash') def strong_hash(): import hashlib data = request.args.get('data', '') # FIX: Use SHA-256 instead of MD5 return hashlib.sha256(data.encode()).hexdigest() EOF # Verify fixed code with Semgrep semgrep --config p/owasp-top-ten \ --config ~/devsecops-lab/semgrep_rules/flask_security.yml \ ~/devsecops-lab/src/app_fixed.py echo "Fixed version should show fewer/no findings"
10 Add secret scanning with gitleaks
# Install gitleaks (detects secrets in git history and current code) docker pull zricethezav/gitleaks:latest # Scan the git repository for secrets cd ~/devsecops-lab docker run --rm \ -v $(pwd):/path \ zricethezav/gitleaks:latest detect \ --source /path \ --report-format json \ --report-path /path/reports/gitleaks_report.json \ --verbose # Also install pre-commit hooks to catch secrets before they're committed pip3 install pre-commit cat > ~/devsecops-lab/.pre-commit-config.yaml << 'EOF' repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.18.0 hooks: - id: gitleaks - repo: https://github.com/pycqa/bandit rev: 1.7.5 hooks: - id: bandit args: ["-ll"] - repo: https://github.com/Yelp/detect-secrets rev: v1.4.0 hooks: - id: detect-secrets args: ["--baseline", ".secrets.baseline"] EOF cd ~/devsecops-lab && pre-commit install echo "Pre-commit hooks installed — will scan on every git commit"
11 Map all findings to OWASP Top 10 & MITRE
VulnerabilityTool DetectedOWASPCWE
SQL InjectionSemgrepA03:2021 InjectionCWE-89
Command InjectionSemgrep + ZAPA03:2021 InjectionCWE-78
Hardcoded SecretSemgrep + GitleaksA02:2021 CryptoCWE-798
Weak Hash (MD5)SemgrepA02:2021 CryptoCWE-327
Insecure DeserializationSemgrepA08:2021 IntegrityCWE-502
Vulnerable Dependenciespip-audit/TrivyA06:2021 ComponentsCVE-specific
12 Document pipeline and produce DevSecOps report

Lab Findings

MetricValue
Semgrep findings (total)
Vulnerable dependencies
Container CVEs (CRITICAL)
ZAP findings (HIGH)
Secrets found in code
Vulnerabilities after fix

Next: Lab L20 — Full SOC Simulation — AI Analyst

Put it all together in a full SOC simulation with live alerts, triage, and AI-assisted response.

Start L20 →
AI DevSecOps Engineer

Enter your Anthropic API key to activate the AI analyst:

Quick Prompts: