ci: add conditional DAST execution to C5 workflow #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: C5 - Optimized DevSecOps Pipeline | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| run_zap: | |
| description: "Run OWASP ZAP DAST scan" | |
| required: true | |
| default: "true" | |
| type: choice | |
| options: | |
| - "true" | |
| - "false" | |
| push: | |
| branches: | |
| - feature/c5-optimized-pipeline | |
| - dev | |
| env: | |
| SCENARIO: C5-optimized | |
| RESULTS_FILE: analysis/raw/c5_optimized_run.csv | |
| IMAGE_NAME: tcc-devsecops-api:c5 | |
| CLUSTER_NAME: tcc-devsecops | |
| SONAR_PROJECT_SETTINGS: ci/config/sonar-project.properties | |
| TRIVY_SUMMARY_FILE: analysis/raw/c5_trivy_summary_run.csv | |
| TRIVY_CONFIG: ci/config/trivy.yaml | |
| TRIVY_VERSION: v0.71.0 | |
| TRIVY_REPORTS_DIR: analysis/reports/trivy | |
| ZAP_REPORTS_DIR: analysis/reports/zap | |
| ZAP_SUMMARY_FILE: analysis/raw/c5_zap_summary_run.csv | |
| ZAP_TARGET_URL: http://host.docker.internal:8000 | |
| RUN_ZAP: ${{ github.event_name == 'workflow_dispatch' && inputs.run_zap || 'true' }} | |
| jobs: | |
| c5-optimized: | |
| name: C5 optimized pipeline with SonarQube SAST, Trivy SCA and OWASP ZAP DAST | |
| runs-on: self-hosted | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| - name: Cleanup previous kind cluster container | |
| run: | | |
| docker rm -f "${CLUSTER_NAME}-control-plane" 2>/dev/null || true | |
| - name: Prepare results files | |
| run: | | |
| mkdir -p analysis/raw | |
| mkdir -p "$TRIVY_REPORTS_DIR" | |
| mkdir -p "$ZAP_REPORTS_DIR" | |
| echo "WORKFLOW_START_SECONDS=$(date +%s)" >> "$GITHUB_ENV" | |
| echo "WORKFLOW_START_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_ENV" | |
| echo "scenario,run_id,run_number,commit_sha,step_name,start_timestamp,end_timestamp,duration_seconds,status" > "$RESULTS_FILE" | |
| echo "scenario,run_id,run_number,commit_sha,trivy_target,trivy_scan_type,vuln_low,vuln_medium,vuln_high,vuln_critical,total_vulnerabilities" > "$TRIVY_SUMMARY_FILE" | |
| echo "scenario,run_id,run_number,commit_sha,zap_target_url,zap_info_alerts,zap_low_alerts,zap_medium_alerts,zap_high_alerts,zap_total_alerts" > "$ZAP_SUMMARY_FILE" | |
| - name: Setup Python with pip cache | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: "pip" | |
| cache-dependency-path: "app/requirements.txt" | |
| - name: Make measurement script executable | |
| run: chmod +x ci/scripts/measure_step.sh | |
| - name: Install Python dependencies | |
| run: | | |
| ./ci/scripts/measure_step.sh "install_dependencies" "$RESULTS_FILE" \ | |
| python -m pip install -r app/requirements.txt | |
| - name: Run unit tests | |
| run: | | |
| ./ci/scripts/measure_step.sh "unit_tests" "$RESULTS_FILE" \ | |
| python -m pytest app -v | |
| - name: Run SonarQube SAST analysis | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} | |
| run: | | |
| SONARQUBE_NETWORK=$(docker inspect tcc-sonarqube --format '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}' | head -n 1) | |
| echo "Using SonarQube Docker network: $SONARQUBE_NETWORK" | |
| ./ci/scripts/measure_step.sh "sonarqube_sast" "$RESULTS_FILE" \ | |
| docker run --rm \ | |
| --network "$SONARQUBE_NETWORK" \ | |
| -e SONAR_HOST_URL="$SONAR_HOST_URL" \ | |
| -e SONAR_TOKEN="$SONAR_TOKEN" \ | |
| -v "$GITHUB_WORKSPACE:/usr/src" \ | |
| sonarsource/sonar-scanner-cli \ | |
| -Dproject.settings="$SONAR_PROJECT_SETTINGS" | |
| - name: Build Docker image | |
| run: | | |
| ./ci/scripts/measure_step.sh "docker_build" "$RESULTS_FILE" \ | |
| docker build -t "$IMAGE_NAME" ./app | |
| - name: Cache Trivy vulnerability database | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/trivy | |
| key: ${{ runner.os }}-trivy-${{ env.TRIVY_VERSION }}-${{ hashFiles('ci/config/trivy.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-trivy-${{ env.TRIVY_VERSION }}- | |
| - name: Install Trivy | |
| run: | | |
| mkdir -p "$HOME/.local/bin" | |
| curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b "$HOME/.local/bin" "$TRIVY_VERSION" | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| export PATH="$HOME/.local/bin:$PATH" | |
| trivy --version | |
| - name: Mark SCA start | |
| run: | | |
| echo "SCA_START_SECONDS=$(date +%s)" >> "$GITHUB_ENV" | |
| echo "SCA_START_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_ENV" | |
| - name: Run Trivy filesystem scan | |
| run: | | |
| ./ci/scripts/measure_step.sh "sca_fs" "$RESULTS_FILE" \ | |
| trivy fs \ | |
| --config "$TRIVY_CONFIG" \ | |
| --format json \ | |
| --output "$TRIVY_REPORTS_DIR/trivy-fs-report.json" \ | |
| --exit-code 0 \ | |
| ./app | |
| - name: Run Trivy image scan | |
| run: | | |
| ./ci/scripts/measure_step.sh "sca_image" "$RESULTS_FILE" \ | |
| trivy image \ | |
| --config "$TRIVY_CONFIG" \ | |
| --format json \ | |
| --output "$TRIVY_REPORTS_DIR/trivy-image-report.json" \ | |
| --exit-code 0 \ | |
| "$IMAGE_NAME" | |
| - name: Generate Trivy SARIF reports | |
| run: | | |
| trivy convert \ | |
| --format sarif \ | |
| --output "$TRIVY_REPORTS_DIR/trivy-fs-report.sarif" \ | |
| "$TRIVY_REPORTS_DIR/trivy-fs-report.json" | |
| trivy convert \ | |
| --format sarif \ | |
| --output "$TRIVY_REPORTS_DIR/trivy-image-report.sarif" \ | |
| "$TRIVY_REPORTS_DIR/trivy-image-report.json" | |
| - name: Append SCA total duration | |
| if: always() | |
| run: | | |
| SCA_END_TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" | |
| SCA_END_SECONDS="$(date +%s)" | |
| SCA_DURATION_SECONDS=$((SCA_END_SECONDS - SCA_START_SECONDS)) | |
| echo "${SCENARIO},${GITHUB_RUN_ID},${GITHUB_RUN_NUMBER},${GITHUB_SHA},sca_total,${SCA_START_TIMESTAMP},${SCA_END_TIMESTAMP},${SCA_DURATION_SECONDS},success" >> "$RESULTS_FILE" | |
| - name: Count Trivy vulnerabilities by severity | |
| if: always() | |
| run: | | |
| python - <<'PY' | |
| import csv | |
| import json | |
| import os | |
| from pathlib import Path | |
| scenario = os.getenv("SCENARIO", "C5-optimized") | |
| run_id = os.getenv("GITHUB_RUN_ID", "local") | |
| run_number = os.getenv("GITHUB_RUN_NUMBER", "local") | |
| commit_sha = os.getenv("GITHUB_SHA", "local") | |
| summary_file = Path(os.getenv("TRIVY_SUMMARY_FILE", "analysis/raw/c5_trivy_summary_run.csv")) | |
| reports = [ | |
| ("filesystem", "fs", Path("analysis/reports/trivy/trivy-fs-report.json")), | |
| ("image", "image", Path("analysis/reports/trivy/trivy-image-report.json")), | |
| ] | |
| rows = [] | |
| for target, scan_type, report_path in reports: | |
| counts = {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0} | |
| if report_path.exists(): | |
| with report_path.open("r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| for result in data.get("Results", []): | |
| for vuln in result.get("Vulnerabilities", []) or []: | |
| severity = vuln.get("Severity", "").upper() | |
| if severity in counts: | |
| counts[severity] += 1 | |
| total = sum(counts.values()) | |
| rows.append([ | |
| scenario, | |
| run_id, | |
| run_number, | |
| commit_sha, | |
| target, | |
| scan_type, | |
| counts["LOW"], | |
| counts["MEDIUM"], | |
| counts["HIGH"], | |
| counts["CRITICAL"], | |
| total, | |
| ]) | |
| with summary_file.open("a", newline="", encoding="utf-8") as f: | |
| writer = csv.writer(f) | |
| writer.writerows(rows) | |
| print("Trivy vulnerability summary:") | |
| for row in rows: | |
| print(row) | |
| PY | |
| - name: Install kind | |
| run: | | |
| mkdir -p "$HOME/.local/bin" | |
| curl -Lo "$HOME/.local/bin/kind" https://kind.sigs.k8s.io/dl/v0.30.0/kind-linux-amd64 | |
| chmod +x "$HOME/.local/bin/kind" | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| export PATH="$HOME/.local/bin:$PATH" | |
| kind version | |
| - name: Create local Kubernetes cluster | |
| run: | | |
| kind create cluster --name "$CLUSTER_NAME" --config infra/kind/kind-cluster.yaml | |
| kubectl cluster-info | |
| kubectl get nodes | |
| - name: Deploy to Kubernetes | |
| run: | | |
| ./ci/scripts/measure_step.sh "kubernetes_deploy" "$RESULTS_FILE" \ | |
| bash -c 'kind load docker-image "$IMAGE_NAME" --name "$CLUSTER_NAME" && kubectl apply -f infra/k8s/namespace.yaml && kubectl apply -f infra/k8s/deployment.yaml && kubectl apply -f infra/k8s/service.yaml && kubectl set image deployment/tcc-api tcc-api="$IMAGE_NAME" -n app && kubectl rollout status deployment/tcc-api -n app --timeout=120s' | |
| - name: Smoke test Kubernetes deployment | |
| run: | | |
| ./ci/scripts/measure_step.sh "kubernetes_smoke_test" "$RESULTS_FILE" \ | |
| bash -c 'kubectl port-forward svc/tcc-api 8000:80 -n app > /tmp/port-forward.log 2>&1 & PF_PID=$!; sleep 5; curl -fsS http://localhost:8000/health; kill $PF_PID' | |
| - name: Start application port-forward for DAST | |
| if: env.RUN_ZAP == 'true' | |
| run: | | |
| kubectl port-forward --address 0.0.0.0 svc/tcc-api 8000:80 -n app > /tmp/c5-zap-port-forward.log 2>&1 & | |
| echo "PORT_FORWARD_PID=$!" >> "$GITHUB_ENV" | |
| sleep 5 | |
| - name: Verify application before DAST | |
| if: env.RUN_ZAP == 'true' | |
| run: | | |
| ./ci/scripts/measure_step.sh "dast_health_check" "$RESULTS_FILE" \ | |
| curl -fsS http://localhost:8000/health | |
| - name: Run OWASP ZAP baseline scan | |
| if: env.RUN_ZAP == 'true' | |
| run: | | |
| ./ci/scripts/measure_step.sh "dast_zap_baseline" "$RESULTS_FILE" \ | |
| docker run --rm \ | |
| --add-host=host.docker.internal:host-gateway \ | |
| -v "$GITHUB_WORKSPACE/$ZAP_REPORTS_DIR:/zap/wrk/:rw" \ | |
| ghcr.io/zaproxy/zaproxy:stable \ | |
| zap-baseline.py \ | |
| -t "$ZAP_TARGET_URL" \ | |
| -m 1 \ | |
| -T 5 \ | |
| -I \ | |
| -r zap-baseline-report.html \ | |
| -J zap-baseline-report.json \ | |
| -w zap-baseline-report.md | |
| - name: Record skipped DAST metrics | |
| if: env.RUN_ZAP != 'true' | |
| run: | | |
| SKIP_TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" | |
| echo "${SCENARIO},${GITHUB_RUN_ID},${GITHUB_RUN_NUMBER},${GITHUB_SHA},dast_health_check,${SKIP_TIMESTAMP},${SKIP_TIMESTAMP},0,skipped" >> "$RESULTS_FILE" | |
| echo "${SCENARIO},${GITHUB_RUN_ID},${GITHUB_RUN_NUMBER},${GITHUB_SHA},dast_zap_baseline,${SKIP_TIMESTAMP},${SKIP_TIMESTAMP},0,skipped" >> "$RESULTS_FILE" | |
| - name: Count OWASP ZAP alerts by severity | |
| if: always() && env.RUN_ZAP == 'true' | |
| run: | | |
| python - <<'PY' | |
| import csv | |
| import json | |
| import os | |
| from pathlib import Path | |
| scenario = os.getenv("SCENARIO", "C5-optimized") | |
| run_id = os.getenv("GITHUB_RUN_ID", "local") | |
| run_number = os.getenv("GITHUB_RUN_NUMBER", "local") | |
| commit_sha = os.getenv("GITHUB_SHA", "local") | |
| target_url = os.getenv("ZAP_TARGET_URL", "unknown") | |
| summary_file = Path(os.getenv("ZAP_SUMMARY_FILE", "analysis/raw/c5_zap_summary_run.csv")) | |
| report_file = Path(os.getenv("ZAP_REPORTS_DIR", "analysis/reports/zap")) / "zap-baseline-report.json" | |
| counts = { | |
| "Informational": 0, | |
| "Low": 0, | |
| "Medium": 0, | |
| "High": 0, | |
| } | |
| if report_file.exists(): | |
| with report_file.open("r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| for site in data.get("site", []): | |
| for alert in site.get("alerts", []): | |
| risk = alert.get("riskdesc", "").split(" ")[0] | |
| if risk in counts: | |
| counts[risk] += 1 | |
| else: | |
| print(f"ZAP JSON report not found: {report_file}") | |
| total = sum(counts.values()) | |
| row = [ | |
| scenario, | |
| run_id, | |
| run_number, | |
| commit_sha, | |
| target_url, | |
| counts["Informational"], | |
| counts["Low"], | |
| counts["Medium"], | |
| counts["High"], | |
| total, | |
| ] | |
| with summary_file.open("a", newline="", encoding="utf-8") as f: | |
| writer = csv.writer(f) | |
| writer.writerow(row) | |
| print("OWASP ZAP alert summary:") | |
| print(row) | |
| PY | |
| - name: Stop application port-forward | |
| if: always() | |
| run: | | |
| if [ -n "${PORT_FORWARD_PID:-}" ]; then | |
| kill "$PORT_FORWARD_PID" 2>/dev/null || true | |
| fi | |
| cat /tmp/c5-zap-port-forward.log 2>/dev/null || true | |
| - name: Append workflow total duration | |
| if: always() | |
| run: | | |
| WORKFLOW_END_TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" | |
| WORKFLOW_END_SECONDS="$(date +%s)" | |
| WORKFLOW_DURATION_SECONDS=$((WORKFLOW_END_SECONDS - WORKFLOW_START_SECONDS)) | |
| echo "${SCENARIO},${GITHUB_RUN_ID},${GITHUB_RUN_NUMBER},${GITHUB_SHA},workflow_total,${WORKFLOW_START_TIMESTAMP},${WORKFLOW_END_TIMESTAMP},${WORKFLOW_DURATION_SECONDS},success" >> "$RESULTS_FILE" | |
| - name: Show collected metrics | |
| if: always() | |
| run: | | |
| echo "Collected C5 optimized metrics:" | |
| cat "$RESULTS_FILE" | |
| echo "" | |
| echo "Collected Trivy vulnerability summary:" | |
| cat "$TRIVY_SUMMARY_FILE" | |
| echo "" | |
| echo "Collected OWASP ZAP DAST summary:" | |
| cat "$ZAP_SUMMARY_FILE" | |
| - name: Write GitHub Actions summary | |
| if: always() | |
| run: | | |
| echo "## C5 - Optimized DevSecOps Pipeline" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Scenario: $SCENARIO" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "### Collected timing metrics" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```csv' >> "$GITHUB_STEP_SUMMARY" | |
| cat "$RESULTS_FILE" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "### Trivy vulnerability summary" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```csv' >> "$GITHUB_STEP_SUMMARY" | |
| cat "$TRIVY_SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "### OWASP ZAP DAST summary" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```csv' >> "$GITHUB_STEP_SUMMARY" | |
| cat "$ZAP_SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload C5 artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: C5-optimized-artifacts | |
| path: | | |
| analysis/raw/c5_optimized_run.csv | |
| analysis/raw/c5_trivy_summary_run.csv | |
| analysis/raw/c5_zap_summary_run.csv | |
| analysis/reports/trivy/ | |
| analysis/reports/zap/ | |