Skip to content

ci: add C4 SAST SCA and DAST workflow #1

ci: add C4 SAST SCA and DAST workflow

ci: add C4 SAST SCA and DAST workflow #1

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/