# Complete DevSecOps Security Scanning Pipeline for GitLab CI # SAST, DAST, SCA, Container Scanning, Secret Scanning stages: - secret-scan - sast - sca - build - container-scan - dast - compliance - report variables: SECURE_LOG_LEVEL: "info" # Enable Auto DevOps security scanners SAST_EXCLUDED_PATHS: "spec, test, tests, tmp" SCAN_KUBERNETES_MANIFESTS: "false" # Stage 1: Secret Scanning secret-scan:trufflehog: stage: secret-scan image: trufflesecurity/trufflehog:latest script: - trufflehog filesystem . --json --fail > trufflehog-report.json || true - | if [ -s trufflehog-report.json ]; then echo "❌ Secrets detected!" cat trufflehog-report.json exit 1 fi artifacts: when: always paths: - trufflehog-report.json expire_in: 30 days allow_failure: false secret-scan:gitleaks: stage: secret-scan image: zricethezav/gitleaks:latest script: - gitleaks detect --source . --report-format json --report-path gitleaks-report.json artifacts: when: always paths: - gitleaks-report.json expire_in: 30 days allow_failure: true # Stage 2: SAST (Static Application Security Testing) sast:semgrep: stage: sast image: returntocorp/semgrep script: - semgrep scan --config=auto --sarif --output=semgrep.sarif . - semgrep scan --config=p/owasp-top-ten --json --output=semgrep-owasp.json . artifacts: reports: sast: semgrep.sarif paths: - semgrep.sarif - semgrep-owasp.json expire_in: 30 days allow_failure: false sast:nodejs: stage: sast image: node:20-alpine script: - npm install -g eslint eslint-plugin-security - eslint . --plugin=security --format=json --output-file=eslint-security.json || true artifacts: paths: - eslint-security.json expire_in: 30 days only: exists: - package.json allow_failure: true sast:python: stage: sast image: python:3.11-alpine script: - pip install bandit - bandit -r . -f json -o bandit-report.json -ll || true - bandit -r . -ll # Fail on high severity artifacts: reports: sast: bandit-report.json paths: - bandit-report.json expire_in: 30 days only: exists: - requirements.txt allow_failure: false sast:go: stage: sast image: securego/gosec:latest script: - gosec -fmt json -out gosec-report.json ./... || true - gosec ./... # Fail on findings artifacts: reports: sast: gosec-report.json paths: - gosec-report.json expire_in: 30 days only: exists: - go.mod allow_failure: false # GitLab built-in SAST include: - template: Security/SAST.gitlab-ci.yml sast: variables: SAST_EXCLUDED_ANALYZERS: "" # Stage 3: SCA (Software Composition Analysis) sca:npm-audit: stage: sca image: node:20-alpine before_script: - npm ci script: - npm audit --audit-level=moderate --json > npm-audit.json || true - npm audit --audit-level=high # Fail on high severity artifacts: paths: - npm-audit.json expire_in: 30 days only: exists: - package.json allow_failure: false sca:python: stage: sca image: python:3.11-alpine script: - pip install pip-audit - pip-audit --requirement requirements.txt --format json --output pip-audit.json || true - pip-audit --requirement requirements.txt --vulnerability-service osv # Fail on vulns artifacts: paths: - pip-audit.json expire_in: 30 days only: exists: - requirements.txt allow_failure: false # GitLab built-in Dependency Scanning include: - template: Security/Dependency-Scanning.gitlab-ci.yml dependency_scanning: variables: DS_EXCLUDED_PATHS: "test/,tests/,spec/,vendor/" # Stage 4: Build build:app: stage: build image: node:20-alpine script: - npm ci - npm run build artifacts: paths: - dist/ expire_in: 7 days only: - branches - merge_requests build:docker: stage: build image: docker:24-cli services: - docker:24-dind variables: IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA before_script: - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY script: - docker build --tag $IMAGE_TAG . - docker push $IMAGE_TAG - echo "IMAGE_TAG=$IMAGE_TAG" > build.env artifacts: reports: dotenv: build.env only: - branches - merge_requests # Stage 5: Container Security Scanning container:trivy: stage: container-scan image: aquasec/trivy:latest needs: [build:docker] dependencies: - build:docker variables: GIT_STRATEGY: none script: # Scan for vulnerabilities - trivy image --severity HIGH,CRITICAL --format json --output trivy-report.json $IMAGE_TAG - trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGE_TAG artifacts: reports: container_scanning: trivy-report.json paths: - trivy-report.json expire_in: 30 days allow_failure: false container:grype: stage: container-scan image: anchore/grype:latest needs: [build:docker] dependencies: - build:docker variables: GIT_STRATEGY: none before_script: - echo $CI_REGISTRY_PASSWORD | grype registry login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY script: - grype $IMAGE_TAG --fail-on high --output json --file grype-report.json artifacts: paths: - grype-report.json expire_in: 30 days allow_failure: true container:sbom: stage: container-scan image: anchore/syft:latest needs: [build:docker] dependencies: - build:docker variables: GIT_STRATEGY: none before_script: - echo $CI_REGISTRY_PASSWORD | syft registry login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY script: - syft $IMAGE_TAG -o spdx-json > sbom.spdx.json - syft $IMAGE_TAG -o cyclonedx-json > sbom.cyclonedx.json artifacts: paths: - sbom.spdx.json - sbom.cyclonedx.json expire_in: 90 days only: - main - tags # GitLab built-in Container Scanning include: - template: Security/Container-Scanning.gitlab-ci.yml container_scanning: needs: [build:docker] dependencies: - build:docker variables: CS_IMAGE: $IMAGE_TAG GIT_STRATEGY: none # Stage 6: DAST (Dynamic Application Security Testing) dast:zap-baseline: stage: dast image: owasp/zap2docker-stable needs: [build:docker] services: - name: $IMAGE_TAG alias: testapp script: # Wait for app to be ready - sleep 10 # Run baseline scan - zap-baseline.py -t http://testapp:8080 -r zap-baseline-report.html -J zap-baseline.json artifacts: when: always paths: - zap-baseline-report.html - zap-baseline.json expire_in: 30 days only: - main - schedules allow_failure: true dast:zap-full: stage: dast image: owasp/zap2docker-stable script: # Full scan on staging environment - zap-full-scan.py -t https://staging.example.com -r zap-full-report.html -J zap-full.json artifacts: when: always paths: - zap-full-report.html - zap-full.json reports: dast: zap-full.json expire_in: 30 days only: - schedules # Run on schedule only (slow) allow_failure: true # GitLab built-in DAST include: - template: DAST.gitlab-ci.yml dast: variables: DAST_WEBSITE: https://staging.example.com DAST_FULL_SCAN_ENABLED: "false" only: - schedules - main # Stage 7: License Compliance license:check: stage: compliance image: node:20-alpine needs: [build:app] script: - npm ci - npm install -g license-checker - license-checker --production --onlyAllow "MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD" --json --out license-report.json artifacts: paths: - license-report.json expire_in: 30 days only: exists: - package.json allow_failure: false # GitLab built-in License Scanning include: - template: Security/License-Scanning.gitlab-ci.yml license_scanning: only: - main - merge_requests # Stage 8: Security Report & Gate security:gate: stage: report image: alpine:latest needs: - secret-scan:trufflehog - sast:semgrep - sca:npm-audit - container:trivy - license:check before_script: - apk add --no-cache jq curl script: - | echo "===================================" echo "🔒 Security Gate Evaluation" echo "===================================" GATE_PASSED=true # Check Trivy results if [ -f trivy-report.json ]; then CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' trivy-report.json) HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="HIGH")] | length' trivy-report.json) echo "Container Vulnerabilities:" echo " - Critical: $CRITICAL" echo " - High: $HIGH" if [ "$CRITICAL" -gt 0 ]; then echo "❌ CRITICAL vulnerabilities found in container" GATE_PASSED=false fi if [ "$HIGH" -gt 10 ]; then echo "⚠️ Too many HIGH vulnerabilities: $HIGH" GATE_PASSED=false fi fi # Final gate decision if [ "$GATE_PASSED" = true ]; then echo "" echo "✅ Security gate PASSED" echo "All security checks completed successfully" exit 0 else echo "" echo "❌ Security gate FAILED" echo "Critical security issues detected" exit 1 fi allow_failure: false security:report: stage: report image: alpine:latest needs: - security:gate when: always script: - | cat << EOF > security-report.md # Security Scan Report - $(date +%Y-%m-%d) ## Summary - **Project:** $CI_PROJECT_NAME - **Branch:** $CI_COMMIT_BRANCH - **Commit:** $CI_COMMIT_SHORT_SHA - **Pipeline:** $CI_PIPELINE_URL - **Scan Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC") ## Scans Performed 1. ✅ Secret Scanning (TruffleHog, Gitleaks) 2. ✅ SAST (Semgrep, Language-specific) 3. ✅ SCA (npm audit, pip-audit) 4. ✅ Container Scanning (Trivy, Grype) 5. ✅ SBOM Generation (Syft) 6. ✅ License Compliance ## Security Gate Status: See job logs for details ## Artifacts - Trivy container scan report - Semgrep SAST report - SBOM (SPDX & CycloneDX) - License compliance report - ZAP DAST report (if applicable) ## Next Steps 1. Review all security findings in artifacts 2. Address critical and high severity vulnerabilities 3. Update dependencies with known vulnerabilities 4. Re-run pipeline to verify fixes EOF cat security-report.md artifacts: paths: - security-report.md expire_in: 90 days # Workflow rules workflow: rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH == "main"' - if: '$CI_COMMIT_BRANCH == "develop"' - if: '$CI_PIPELINE_SOURCE == "schedule"' - if: '$CI_COMMIT_TAG' # Interruptible jobs for MR pipelines .interruptible: interruptible: true secret-scan:trufflehog: extends: .interruptible sast:semgrep: extends: .interruptible sca:npm-audit: extends: .interruptible