Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 381 additions & 0 deletions .github/workflows/c4-sast-sca-dast.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,381 @@
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 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
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/
1 change: 1 addition & 0 deletions analysis/raw/c4_sast_sca_dast.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
scenario,run_id,run_number,commit_sha,step_name,start_timestamp,end_timestamp,duration_seconds,status
1 change: 1 addition & 0 deletions analysis/raw/c4_zap_summary.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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
1 change: 1 addition & 0 deletions ci/config/zap-rules.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# ZAP rules file for C4 MVP. Keep this file minimal to avoid biasing the experiment.
Loading
Loading