# DevSecOps in CI/CD Comprehensive guide to integrating security into CI/CD pipelines with SAST, DAST, SCA, and security gates. ## Table of Contents - [Shift-Left Security](#shift-left-security) - [SAST (Static Application Security Testing)](#sast-static-application-security-testing) - [DAST (Dynamic Application Security Testing)](#dast-dynamic-application-security-testing) - [SCA (Software Composition Analysis)](#sca-software-composition-analysis) - [Container Security](#container-security) - [Secret Scanning](#secret-scanning) - [Security Gates & Quality Gates](#security-gates--quality-gates) - [Compliance & License Scanning](#compliance--license-scanning) --- ## Shift-Left Security **Core principle:** Integrate security testing early in the development lifecycle, not just before production. **Security testing stages in CI/CD:** ``` Commit → SAST → Unit Tests → SCA → Build → Container Scan → Deploy to Test → DAST → Production ↓ ↓ ↓ ↓ ↓ Secret Code Dependency Docker Dynamic Security Scan Analysis Vuln Check Image Scan App Testing Gates ``` **Benefits:** - Find vulnerabilities early (cheaper to fix) - Faster feedback to developers - Reduce security debt - Prevent vulnerable code from reaching production --- ## SAST (Static Application Security Testing) Analyzes source code, bytecode, or binaries for security vulnerabilities without executing the application. ### Tools by Language | Language | Tools | GitHub Actions | GitLab CI | |----------|-------|----------------|-----------| | **Multi-language** | CodeQL, Semgrep, SonarQube | ✅ | ✅ | | **JavaScript/TypeScript** | ESLint (security plugins), NodeJsScan | ✅ | ✅ | | **Python** | Bandit, Pylint, Safety | ✅ | ✅ | | **Go** | Gosec, GoSec Scanner | ✅ | ✅ | | **Java** | SpotBugs, FindSecBugs, PMD | ✅ | ✅ | | **C#/.NET** | Security Code Scan, Roslyn Analyzers | ✅ | ✅ | ### CodeQL (GitHub) **GitHub Actions:** ```yaml name: CodeQL Analysis on: push: branches: [main, develop] pull_request: branches: [main] schedule: - cron: '0 2 * * 1' # Weekly scan jobs: analyze: name: Analyze Code runs-on: ubuntu-latest timeout-minutes: 30 permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ['javascript', 'python'] steps: - uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} queries: security-extended - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" ``` **Key features:** - Supports 10+ languages - Deep semantic analysis - Low false positive rate - Integrates with GitHub Security tab - Custom query support ### Semgrep **GitHub Actions:** ```yaml - name: Run Semgrep uses: returntocorp/semgrep-action@v1 with: config: >- p/security-audit p/owasp-top-ten p/cwe-top-25 ``` **GitLab CI:** ```yaml semgrep: stage: test image: returntocorp/semgrep script: - semgrep --config=auto --sarif --output=semgrep.sarif . artifacts: reports: sast: semgrep.sarif ``` **Benefits:** - Fast (runs in seconds) - Highly customizable rules - Multi-language support - CI-native design ### Language-Specific SAST **Python - Bandit:** ```yaml # GitHub Actions - name: Run Bandit run: | pip install bandit bandit -r src/ -f json -o bandit-report.json bandit -r src/ --exit-zero -ll # Only high severity fails build # GitLab CI bandit: stage: test image: python:3.11 script: - pip install bandit - bandit -r src/ -ll -f gitlab > bandit-report.json artifacts: reports: sast: bandit-report.json ``` **JavaScript - ESLint Security Plugin:** ```yaml # GitHub Actions - name: Run ESLint Security run: | npm install eslint-plugin-security npx eslint . --plugin=security --format=json --output-file=eslint-security.json ``` **Go - Gosec:** ```yaml # GitHub Actions - name: Run Gosec uses: securego/gosec@master with: args: '-fmt sarif -out gosec.sarif ./...' # GitLab CI gosec: stage: test image: securego/gosec:latest script: - gosec -fmt json -out gosec-report.json ./... artifacts: reports: sast: gosec-report.json ``` ### SonarQube/SonarCloud **GitHub Actions:** ```yaml - name: SonarCloud Scan uses: SonarSource/sonarcloud-github-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} with: args: > -Dsonar.projectKey=my-project -Dsonar.organization=my-org -Dsonar.sources=src -Dsonar.tests=tests -Dsonar.python.coverage.reportPaths=coverage.xml ``` **GitLab CI:** ```yaml sonarqube: stage: test image: sonarsource/sonar-scanner-cli:latest script: - sonar-scanner -Dsonar.projectKey=$CI_PROJECT_NAME -Dsonar.sources=src -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_TOKEN ``` --- ## DAST (Dynamic Application Security Testing) Tests running applications for vulnerabilities by simulating attacks. ### OWASP ZAP **Full scan workflow (GitHub Actions):** ```yaml name: DAST Scan on: schedule: - cron: '0 3 * * 1' # Weekly scan workflow_dispatch: jobs: dast: runs-on: ubuntu-latest services: app: image: myapp:latest ports: - 8080:8080 steps: - name: Wait for app to start run: | timeout 60 bash -c 'until curl -f http://localhost:8080/health; do sleep 2; done' - name: ZAP Baseline Scan uses: zaproxy/action-baseline@v0.10.0 with: target: 'http://localhost:8080' rules_file_name: '.zap/rules.tsv' fail_action: true - name: Upload ZAP report if: always() uses: actions/upload-artifact@v4 with: name: zap-report path: report_html.html ``` **GitLab CI:** ```yaml dast: stage: test image: owasp/zap2docker-stable services: - name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA alias: testapp script: # Baseline scan - zap-baseline.py -t http://testapp:8080 -r zap-report.html -J zap-report.json artifacts: when: always paths: - zap-report.html - zap-report.json reports: dast: zap-report.json only: - schedules - main ``` **ZAP scan types:** 1. **Baseline Scan** (Fast, ~1-2 min) ```bash zap-baseline.py -t https://staging.example.com -r report.html ``` - Passive scanning only - No active attacks - Good for PR checks 2. **Full Scan** (Comprehensive, 10-60 min) ```bash zap-full-scan.py -t https://staging.example.com -r report.html ``` - Active + Passive scanning - Attempts exploits - Use on staging only 3. **API Scan** ```bash zap-api-scan.py -t https://api.example.com/openapi.json -f openapi -r report.html ``` - For REST APIs - OpenAPI/Swagger support ### Other DAST Tools **Nuclei:** ```yaml - name: Run Nuclei uses: projectdiscovery/nuclei-action@main with: target: https://staging.example.com templates: cves,vulnerabilities,exposures ``` **Nikto (Web server scanner):** ```yaml nikto: stage: dast image: sullo/nikto script: - nikto -h http://testapp:8080 -Format json -output nikto-report.json ``` --- ## SCA (Software Composition Analysis) Identifies vulnerabilities in third-party dependencies and libraries. ### Dependency Scanning **GitHub Dependabot (Built-in):** ```yaml # .github/dependabot.yml version: 2 updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 10 - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" ``` **GitHub Actions - Dependency Review:** ```yaml - name: Dependency Review uses: actions/dependency-review-action@v4 with: fail-on-severity: high ``` **npm audit:** ```yaml - name: npm audit run: | npm audit --audit-level=high # Or with audit-ci for better control npx audit-ci --high ``` **pip-audit (Python):** ```yaml - name: Python Security Check run: | pip install pip-audit pip-audit --requirement requirements.txt --format json --output pip-audit.json ``` **Snyk:** ```yaml # GitHub Actions - name: Run Snyk uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: args: --severity-threshold=high --fail-on=all # GitLab CI snyk: stage: test image: snyk/snyk:node script: - snyk test --severity-threshold=high --json-file-output=snyk-report.json artifacts: reports: dependency_scanning: snyk-report.json ``` **OWASP Dependency-Check:** ```yaml - name: OWASP Dependency Check run: | wget https://github.com/jeremylong/DependencyCheck/releases/download/v8.4.0/dependency-check-8.4.0-release.zip unzip dependency-check-8.4.0-release.zip ./dependency-check/bin/dependency-check.sh \ --scan . \ --format JSON \ --out dependency-check-report.json \ --failOnCVSS 7 ``` ### GitLab Dependency Scanning (Built-in) ```yaml include: - template: Security/Dependency-Scanning.gitlab-ci.yml dependency_scanning: variables: DS_EXCLUDED_PATHS: "test/,tests/,spec/,vendor/" ``` --- ## Container Security ### Image Scanning **Trivy (Comprehensive):** ```yaml # GitHub Actions - name: Run Trivy uses: aquasecurity/trivy-action@master with: image-ref: myapp:${{ github.sha }} format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' exit-code: '1' - name: Upload to Security tab uses: github/codeql-action/upload-sarif@v3 if: always() with: sarif_file: 'trivy-results.sarif' # GitLab CI trivy: stage: test image: aquasec/trivy:latest script: - trivy image --severity HIGH,CRITICAL --format json --output trivy-report.json $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - trivy image --severity HIGH,CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA artifacts: reports: container_scanning: trivy-report.json ``` **Grype:** ```yaml - name: Scan with Grype uses: anchore/scan-action@v3 with: image: myapp:latest fail-build: true severity-cutoff: high output-format: sarif - name: Upload Grype results uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ${{ steps.scan.outputs.sarif }} ``` **Clair:** ```yaml clair: stage: scan image: arminc/clair-scanner:latest script: - clair-scanner --ip $(hostname -i) myapp:latest ``` ### SBOM (Software Bill of Materials) **Syft:** ```yaml - name: Generate SBOM uses: anchore/sbom-action@v0 with: image: myapp:${{ github.sha }} format: spdx-json output-file: sbom.spdx.json - name: Upload SBOM uses: actions/upload-artifact@v4 with: name: sbom path: sbom.spdx.json ``` **CycloneDX:** ```yaml - name: Generate CycloneDX SBOM run: | npm install -g @cyclonedx/cyclonedx-npm cyclonedx-npm --output-file sbom.json ``` --- ## Secret Scanning ### Pre-commit Prevention **TruffleHog:** ```yaml # GitHub Actions - name: TruffleHog Scan uses: trufflesecurity/trufflehog@main with: path: ./ base: ${{ github.event.repository.default_branch }} head: HEAD # GitLab CI trufflehog: stage: test image: trufflesecurity/trufflehog:latest script: - trufflehog filesystem . --json --fail > trufflehog-report.json ``` **Gitleaks:** ```yaml # GitHub Actions - name: Gitleaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GitLab CI gitleaks: stage: test image: zricethezav/gitleaks:latest script: - gitleaks detect --source . --report-format json --report-path gitleaks-report.json ``` **GitGuardian:** ```yaml - name: GitGuardian scan uses: GitGuardian/ggshield-action@master env: GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }} ``` ### GitHub Secret Scanning (Native) Enable in: **Settings → Code security and analysis → Secret scanning** - Automatic detection - Partner patterns (AWS, Azure, GCP, etc.) - Push protection (prevents commits with secrets) --- ## Security Gates & Quality Gates ### Fail Pipeline on Security Issues **Threshold-based gates:** ```yaml security-gate: stage: gate script: # Check vulnerability count - | CRITICAL=$(jq '.vulnerabilities | map(select(.severity=="CRITICAL")) | length' trivy-report.json) HIGH=$(jq '.vulnerabilities | map(select(.severity=="HIGH")) | length' trivy-report.json) echo "Critical: $CRITICAL, High: $HIGH" if [ "$CRITICAL" -gt 0 ]; then echo "❌ CRITICAL vulnerabilities found!" exit 1 fi if [ "$HIGH" -gt 5 ]; then echo "❌ Too many HIGH vulnerabilities: $HIGH" exit 1 fi ``` **SonarQube Quality Gate:** ```yaml - name: Check Quality Gate run: | STATUS=$(curl -u $SONAR_TOKEN: "$SONAR_HOST/api/qualitygates/project_status?projectKey=$PROJECT_KEY" | jq -r '.projectStatus.status') if [ "$STATUS" != "OK" ]; then echo "Quality gate failed: $STATUS" exit 1 fi ``` ### Manual Approval for Production **GitHub Actions:** ```yaml deploy-production: runs-on: ubuntu-latest needs: [sast, dast, container-scan] environment: name: production # Requires manual approval in Settings → Environments steps: - run: echo "Deploying to production" ``` **GitLab CI:** ```yaml deploy:production: stage: deploy needs: [sast, dast, container_scanning] script: - ./deploy.sh production when: manual only: - main ``` --- ## Compliance & License Scanning ### License Compliance **FOSSology:** ```yaml license-scan: stage: compliance image: fossology/fossology:latest script: - fossology --scan ./src ``` **License Finder:** ```yaml - name: Check Licenses run: | gem install license_finder license_finder --decisions-file .license_finder.yml ``` **npm license checker:** ```yaml - name: License Check run: | npx license-checker --production --onlyAllow "MIT;Apache-2.0;BSD-3-Clause;ISC" ``` ### Policy as Code **Open Policy Agent (OPA):** ```yaml policy-check: stage: gate image: openpolicyagent/opa:latest script: - opa test policies/ - opa eval --data policies/ --input violations.json "data.security.allow" ``` --- ## Complete DevSecOps Pipeline **Comprehensive example (GitHub Actions):** ```yaml name: DevSecOps Pipeline on: [push, pull_request] jobs: # Stage 1: Secret Scanning secret-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: trufflesecurity/trufflehog@main # Stage 2: SAST sast: runs-on: ubuntu-latest needs: secret-scan steps: - uses: actions/checkout@v4 - uses: github/codeql-action/init@v3 - uses: github/codeql-action/autobuild@v3 - uses: github/codeql-action/analyze@v3 # Stage 3: SCA sca: runs-on: ubuntu-latest needs: secret-scan steps: - uses: actions/checkout@v4 - run: npm audit --audit-level=high - uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} # Stage 4: Build & Container Scan build-scan: runs-on: ubuntu-latest needs: [sast, sca] steps: - uses: actions/checkout@v4 - run: docker build -t myapp:${{ github.sha }} . - uses: aquasecurity/trivy-action@master with: image-ref: myapp:${{ github.sha }} exit-code: '1' # Stage 5: DAST dast: runs-on: ubuntu-latest needs: build-scan if: github.ref == 'refs/heads/main' steps: - uses: zaproxy/action-baseline@v0.10.0 with: target: 'https://staging.example.com' # Stage 6: Security Gate security-gate: runs-on: ubuntu-latest needs: [sast, sca, build-scan, dast] steps: - run: echo "All security checks passed!" - run: echo "Ready for deployment" # Stage 7: Deploy deploy: runs-on: ubuntu-latest needs: security-gate environment: production steps: - run: echo "Deploying to production" ``` --- ## Best Practices ### 1. Fail Fast - Run secret scanning first - Run SAST early in pipeline - Block PRs with critical vulnerabilities ### 2. Balance Speed vs Security - SAST/SCA on every PR (fast) - Container scanning after build - DAST on schedules or staging only (slow) ### 3. Prioritize Findings **Focus on:** - Critical/High severity - Exploitable vulnerabilities - Direct dependencies (not transitive) - Public-facing components ### 4. Developer Experience - Clear error messages - Link to remediation guidance - Don't overwhelm with noise - Use quality gates, not just fail/pass ### 5. Continuous Improvement - Track security debt over time - Set SLAs for vulnerability remediation - Regular tool evaluation - Security training for developers ### 6. Reporting & Metrics **Track:** - Mean Time to Remediate (MTTR) - Vulnerability backlog - False positive rate - Coverage (% of code scanned) ```yaml - name: Generate Security Report run: | echo "## Security Scan Summary" >> $GITHUB_STEP_SUMMARY echo "- SAST: ✅ Passed" >> $GITHUB_STEP_SUMMARY echo "- SCA: ⚠️ 3 vulnerabilities" >> $GITHUB_STEP_SUMMARY echo "- Container: ✅ Passed" >> $GITHUB_STEP_SUMMARY echo "- DAST: 🔄 Scheduled" >> $GITHUB_STEP_SUMMARY ``` --- ## Tool Comparison | Category | Tool | Speed | Accuracy | Cost | Best For | |----------|------|-------|----------|------|----------| | **SAST** | CodeQL | Medium | High | Free (GH) | Deep analysis | | | Semgrep | Fast | Medium | Free/Paid | Custom rules | | | SonarQube | Medium | High | Free/Paid | Quality + Security | | **DAST** | OWASP ZAP | Medium | High | Free | Web apps | | | Burp Suite | Slow | High | Paid | Professional | | **SCA** | Snyk | Fast | High | Free/Paid | Easy integration | | | Dependabot | Fast | Medium | Free (GH) | Auto PRs | | **Container** | Trivy | Fast | High | Free | Fast scans | | | Grype | Fast | High | Free | SBOM support | | **Secrets** | TruffleHog | Fast | High | Free/Paid | Git history | | | GitGuardian | Fast | High | Paid | Real-time | --- ## Security Scanning Schedule **Recommended frequency:** | Scan Type | PR | Main Branch | Schedule | Notes | |-----------|----|-----------|-----------| ------| | Secret Scanning | ✅ Every | ✅ Every | - | Fast, critical | | SAST | ✅ Every | ✅ Every | - | Fast, essential | | SCA | ✅ Every | ✅ Every | Weekly | Check dependencies | | Linting | ✅ Every | ✅ Every | - | Very fast | | Container Scan | ❌ No | ✅ Every | - | After build | | DAST Baseline | ❌ No | ✅ Every | - | Medium speed | | DAST Full | ❌ No | ❌ No | Weekly | Very slow | | Penetration Test | ❌ No | ❌ No | Quarterly | Manual | --- ## Security Checklist - [ ] Secret scanning enabled and running - [ ] SAST configured for all languages used - [ ] Dependency scanning (SCA) enabled - [ ] Container images scanned before deployment - [ ] DAST running on staging environment - [ ] Security findings triaged in issue tracker - [ ] Quality gates prevent vulnerable deployments - [ ] SBOM generated for releases - [ ] Security scan results tracked over time - [ ] Vulnerability remediation SLAs defined - [ ] Security training for developers - [ ] Incident response plan documented