ci: add C4 SAST SCA and DAST 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: C4 - SAST SCA and DAST Pipeline | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: | |
| - feature/c4-zap-dast | |
| - dev | |
| env: | |
| SCENARIO: C4-sast-sca-dast | |
| RESULTS_FILE: analysis/raw/c4_sast_sca_dast_run.csv | |
| IMAGE_NAME: tcc-devsecops-api:c4 | |
| CLUSTER_NAME: tcc-devsecops | |
| SONAR_PROJECT_SETTINGS: ci/config/sonar-project.properties | |
| TRIVY_SUMMARY_FILE: analysis/raw/c4_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/c4_zap_summary_run.csv | |
| ZAP_TARGET_URL: http://host.docker.internal:8000 | |
| jobs: | |
| c4-sast-sca-dast: | |
| name: C4 baseline plus 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 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - 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: 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", "C4-sast-sca-dast") | |
| 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/c4_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 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 | |
| run: | | |
| kubectl port-forward --address 0.0.0.0 svc/tcc-api 8000:80 -n app > /tmp/c4-zap-port-forward.log 2>&1 & | |
| echo "PORT_FORWARD_PID=$!" >> "$GITHUB_ENV" | |
| sleep 5 | |
| - name: Verify application before DAST | |
| run: | | |
| ./ci/scripts/measure_step.sh "dast_health_check" "$RESULTS_FILE" \ | |
| curl -fsS http://localhost:8000/health | |
| - name: Run OWASP ZAP baseline scan | |
| 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: Count OWASP ZAP alerts by severity | |
| if: always() | |
| run: | | |
| python - <<'PY' | |
| import csv | |
| import json | |
| import os | |
| from pathlib import Path | |
| scenario = os.getenv("SCENARIO", "C4-sast-sca-dast") | |
| 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/c4_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/c4-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 C4 SAST + SCA + DAST 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 "## C4 - SAST, SCA and DAST 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 C4 artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: c4-sast-sca-dast-artifacts | |
| path: | | |
| analysis/raw/c4_sast_sca_dast_run.csv | |
| analysis/raw/c4_trivy_summary_run.csv | |
| analysis/raw/c4_zap_summary_run.csv | |
| analysis/reports/trivy/ | |
| analysis/reports/zap/ |