# GitLab CI/CD Pipeline for OWASP ZAP Security Scanning # Add this to your .gitlab-ci.yml file stages: - security - report variables: ZAP_IMAGE: "zaproxy/zap-stable:latest" STAGING_URL: "https://staging.example.com" REPORTS_DIR: "security-reports" # Baseline scan for all merge requests zap_baseline_scan: stage: security image: docker:latest services: - docker:dind script: - mkdir -p $REPORTS_DIR - | docker run --rm \ -v $(pwd)/$REPORTS_DIR:/zap/wrk/:rw \ $ZAP_IMAGE \ zap-baseline.py \ -t $STAGING_URL \ -r /zap/wrk/baseline-report.html \ -J /zap/wrk/baseline-report.json \ -w /zap/wrk/baseline-report.md \ || true - echo "Baseline scan completed" artifacts: when: always paths: - $REPORTS_DIR/ reports: junit: $REPORTS_DIR/baseline-report.xml expire_in: 1 week only: - merge_requests - develop - main tags: - docker # Full active scan (manual trigger for staging) zap_full_scan: stage: security image: docker:latest services: - docker:dind script: - mkdir -p $REPORTS_DIR - | docker run --rm \ -v $(pwd)/$REPORTS_DIR:/zap/wrk/:rw \ -v $(pwd)/.zap:/zap/config/:ro \ $ZAP_IMAGE \ zap-full-scan.py \ -t $STAGING_URL \ -c /zap/config/rules.tsv \ -r /zap/wrk/full-scan-report.html \ -J /zap/wrk/full-scan-report.json \ -x /zap/wrk/full-scan-report.xml \ || true # Check for high-risk findings - | if command -v jq &> /dev/null; then HIGH_COUNT=$(jq '[.site[].alerts[] | select(.risk == "High")] | length' $REPORTS_DIR/full-scan-report.json) echo "High risk findings: $HIGH_COUNT" if [ "$HIGH_COUNT" -gt 0 ]; then echo "❌ Security scan failed: $HIGH_COUNT high-risk vulnerabilities" exit 1 fi fi artifacts: when: always paths: - $REPORTS_DIR/ expire_in: 4 weeks only: - develop when: manual allow_failure: false tags: - docker # API security scan zap_api_scan: stage: security image: docker:latest services: - docker:dind script: - mkdir -p $REPORTS_DIR - | if [ -f "openapi.yaml" ]; then docker run --rm \ -v $(pwd)/$REPORTS_DIR:/zap/wrk/:rw \ -v $(pwd):/zap/specs/:ro \ $ZAP_IMAGE \ zap-api-scan.py \ -t $STAGING_URL \ -f openapi \ -d /zap/specs/openapi.yaml \ -r /zap/wrk/api-scan-report.html \ -J /zap/wrk/api-scan-report.json \ || true else echo "OpenAPI specification not found, skipping API scan" fi artifacts: when: always paths: - $REPORTS_DIR/ expire_in: 1 week only: - merge_requests - develop allow_failure: true tags: - docker # Authenticated scan (requires test credentials) zap_authenticated_scan: stage: security image: python:3.11-slim before_script: - apt-get update && apt-get install -y docker.io script: - mkdir -p $REPORTS_DIR - | python3 scripts/zap_auth_scanner.py \ --target $STAGING_URL \ --auth-type form \ --login-url $STAGING_URL/login \ --username $TEST_USERNAME \ --password-env TEST_PASSWORD \ --output $REPORTS_DIR/authenticated-scan-report.html artifacts: when: always paths: - $REPORTS_DIR/ expire_in: 4 weeks only: - develop when: manual tags: - docker # Security gate - check thresholds security_gate: stage: report image: alpine:latest before_script: - apk add --no-cache jq script: - | if [ -f "$REPORTS_DIR/baseline-report.json" ]; then HIGH_COUNT=$(jq '[.site[].alerts[] | select(.risk == "High")] | length' $REPORTS_DIR/baseline-report.json) MEDIUM_COUNT=$(jq '[.site[].alerts[] | select(.risk == "Medium")] | length' $REPORTS_DIR/baseline-report.json) echo "===================================" echo "Security Scan Results" echo "===================================" echo "High risk findings: $HIGH_COUNT" echo "Medium risk findings: $MEDIUM_COUNT" echo "===================================" # Fail on high-risk findings if [ "$HIGH_COUNT" -gt 0 ]; then echo "❌ Build failed: High-risk vulnerabilities detected" exit 1 fi # Warn on medium-risk findings above threshold if [ "$MEDIUM_COUNT" -gt 10 ]; then echo "⚠️ Warning: $MEDIUM_COUNT medium-risk findings (threshold: 10)" fi echo "✅ Security gate passed" else echo "No scan report found, skipping security gate" fi dependencies: - zap_baseline_scan only: - merge_requests - develop - main # Generate consolidated report generate_report: stage: report image: alpine:latest before_script: - apk add --no-cache jq curl script: - | echo "# Security Scan Report" > $REPORTS_DIR/summary.md echo "" >> $REPORTS_DIR/summary.md echo "**Scan Date:** $(date)" >> $REPORTS_DIR/summary.md echo "**Target:** $STAGING_URL" >> $REPORTS_DIR/summary.md echo "" >> $REPORTS_DIR/summary.md echo "## Findings Summary" >> $REPORTS_DIR/summary.md echo "" >> $REPORTS_DIR/summary.md if [ -f "$REPORTS_DIR/baseline-report.json" ]; then echo "| Risk Level | Count |" >> $REPORTS_DIR/summary.md echo "|------------|-------|" >> $REPORTS_DIR/summary.md jq -r '.site[].alerts[] | .risk' $REPORTS_DIR/baseline-report.json | \ sort | uniq -c | awk '{print "| " $2 " | " $1 " |"}' >> $REPORTS_DIR/summary.md fi cat $REPORTS_DIR/summary.md artifacts: when: always paths: - $REPORTS_DIR/summary.md expire_in: 4 weeks dependencies: - zap_baseline_scan only: - merge_requests - develop - main