Skip to content

Commit 76a0ad3

Browse files
authored
Merge pull request #8 from r2WillDev/feature/c4-zap-dast
Add C4 OWASP ZAP DAST pipeline
2 parents 2a87d63 + 00bbdcc commit 76a0ad3

5 files changed

Lines changed: 801 additions & 0 deletions

File tree

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
name: C4 - SAST SCA and DAST Pipeline
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- feature/c4-zap-dast
8+
- dev
9+
10+
env:
11+
SCENARIO: C4-sast-sca-dast
12+
RESULTS_FILE: analysis/raw/c4_sast_sca_dast_run.csv
13+
IMAGE_NAME: tcc-devsecops-api:c4
14+
CLUSTER_NAME: tcc-devsecops
15+
SONAR_PROJECT_SETTINGS: ci/config/sonar-project.properties
16+
TRIVY_SUMMARY_FILE: analysis/raw/c4_trivy_summary_run.csv
17+
TRIVY_CONFIG: ci/config/trivy.yaml
18+
TRIVY_VERSION: v0.71.0
19+
TRIVY_REPORTS_DIR: analysis/reports/trivy
20+
ZAP_REPORTS_DIR: analysis/reports/zap
21+
ZAP_SUMMARY_FILE: analysis/raw/c4_zap_summary_run.csv
22+
ZAP_TARGET_URL: http://host.docker.internal:8000
23+
24+
jobs:
25+
c4-sast-sca-dast:
26+
name: C4 baseline plus SonarQube SAST, Trivy SCA and OWASP ZAP DAST
27+
runs-on: self-hosted
28+
29+
steps:
30+
- name: Checkout repository
31+
uses: actions/checkout@v5
32+
33+
- name: Cleanup previous kind cluster container
34+
run: |
35+
docker rm -f "${CLUSTER_NAME}-control-plane" 2>/dev/null || true
36+
37+
- name: Prepare results files
38+
run: |
39+
mkdir -p analysis/raw
40+
mkdir -p "$TRIVY_REPORTS_DIR"
41+
mkdir -p "$ZAP_REPORTS_DIR"
42+
43+
echo "WORKFLOW_START_SECONDS=$(date +%s)" >> "$GITHUB_ENV"
44+
echo "WORKFLOW_START_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_ENV"
45+
46+
echo "scenario,run_id,run_number,commit_sha,step_name,start_timestamp,end_timestamp,duration_seconds,status" > "$RESULTS_FILE"
47+
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"
48+
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"
49+
50+
- name: Setup Python
51+
uses: actions/setup-python@v6
52+
with:
53+
python-version: "3.12"
54+
55+
- name: Make measurement script executable
56+
run: chmod +x ci/scripts/measure_step.sh
57+
58+
- name: Install Python dependencies
59+
run: |
60+
./ci/scripts/measure_step.sh "install_dependencies" "$RESULTS_FILE" \
61+
python -m pip install -r app/requirements.txt
62+
63+
- name: Run unit tests
64+
run: |
65+
./ci/scripts/measure_step.sh "unit_tests" "$RESULTS_FILE" \
66+
python -m pytest app -v
67+
68+
- name: Run SonarQube SAST analysis
69+
env:
70+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
71+
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
72+
run: |
73+
SONARQUBE_NETWORK=$(docker inspect tcc-sonarqube --format '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}' | head -n 1)
74+
echo "Using SonarQube Docker network: $SONARQUBE_NETWORK"
75+
76+
./ci/scripts/measure_step.sh "sonarqube_sast" "$RESULTS_FILE" \
77+
docker run --rm \
78+
--network "$SONARQUBE_NETWORK" \
79+
-e SONAR_HOST_URL="$SONAR_HOST_URL" \
80+
-e SONAR_TOKEN="$SONAR_TOKEN" \
81+
-v "$GITHUB_WORKSPACE:/usr/src" \
82+
sonarsource/sonar-scanner-cli \
83+
-Dproject.settings="$SONAR_PROJECT_SETTINGS"
84+
85+
- name: Build Docker image
86+
run: |
87+
./ci/scripts/measure_step.sh "docker_build" "$RESULTS_FILE" \
88+
docker build -t "$IMAGE_NAME" ./app
89+
90+
- name: Install Trivy
91+
run: |
92+
mkdir -p "$HOME/.local/bin"
93+
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b "$HOME/.local/bin" "$TRIVY_VERSION"
94+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
95+
export PATH="$HOME/.local/bin:$PATH"
96+
trivy --version
97+
98+
- name: Mark SCA start
99+
run: |
100+
echo "SCA_START_SECONDS=$(date +%s)" >> "$GITHUB_ENV"
101+
echo "SCA_START_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_ENV"
102+
103+
- name: Run Trivy filesystem scan
104+
run: |
105+
./ci/scripts/measure_step.sh "sca_fs" "$RESULTS_FILE" \
106+
trivy fs \
107+
--config "$TRIVY_CONFIG" \
108+
--format json \
109+
--output "$TRIVY_REPORTS_DIR/trivy-fs-report.json" \
110+
--exit-code 0 \
111+
./app
112+
113+
- name: Run Trivy image scan
114+
run: |
115+
./ci/scripts/measure_step.sh "sca_image" "$RESULTS_FILE" \
116+
trivy image \
117+
--config "$TRIVY_CONFIG" \
118+
--format json \
119+
--output "$TRIVY_REPORTS_DIR/trivy-image-report.json" \
120+
--exit-code 0 \
121+
"$IMAGE_NAME"
122+
123+
- name: Generate Trivy SARIF reports
124+
run: |
125+
trivy convert \
126+
--format sarif \
127+
--output "$TRIVY_REPORTS_DIR/trivy-fs-report.sarif" \
128+
"$TRIVY_REPORTS_DIR/trivy-fs-report.json"
129+
130+
trivy convert \
131+
--format sarif \
132+
--output "$TRIVY_REPORTS_DIR/trivy-image-report.sarif" \
133+
"$TRIVY_REPORTS_DIR/trivy-image-report.json"
134+
135+
- name: Append SCA total duration
136+
if: always()
137+
run: |
138+
SCA_END_TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
139+
SCA_END_SECONDS="$(date +%s)"
140+
SCA_DURATION_SECONDS=$((SCA_END_SECONDS - SCA_START_SECONDS))
141+
echo "${SCENARIO},${GITHUB_RUN_ID},${GITHUB_RUN_NUMBER},${GITHUB_SHA},sca_total,${SCA_START_TIMESTAMP},${SCA_END_TIMESTAMP},${SCA_DURATION_SECONDS},success" >> "$RESULTS_FILE"
142+
143+
- name: Count Trivy vulnerabilities by severity
144+
if: always()
145+
run: |
146+
python - <<'PY'
147+
import csv
148+
import json
149+
import os
150+
from pathlib import Path
151+
152+
scenario = os.getenv("SCENARIO", "C4-sast-sca-dast")
153+
run_id = os.getenv("GITHUB_RUN_ID", "local")
154+
run_number = os.getenv("GITHUB_RUN_NUMBER", "local")
155+
commit_sha = os.getenv("GITHUB_SHA", "local")
156+
summary_file = Path(os.getenv("TRIVY_SUMMARY_FILE", "analysis/raw/c4_trivy_summary_run.csv"))
157+
158+
reports = [
159+
("filesystem", "fs", Path("analysis/reports/trivy/trivy-fs-report.json")),
160+
("image", "image", Path("analysis/reports/trivy/trivy-image-report.json")),
161+
]
162+
163+
rows = []
164+
165+
for target, scan_type, report_path in reports:
166+
counts = {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0}
167+
168+
if report_path.exists():
169+
with report_path.open("r", encoding="utf-8") as f:
170+
data = json.load(f)
171+
172+
for result in data.get("Results", []):
173+
for vuln in result.get("Vulnerabilities", []) or []:
174+
severity = vuln.get("Severity", "").upper()
175+
if severity in counts:
176+
counts[severity] += 1
177+
178+
total = sum(counts.values())
179+
180+
rows.append([
181+
scenario,
182+
run_id,
183+
run_number,
184+
commit_sha,
185+
target,
186+
scan_type,
187+
counts["LOW"],
188+
counts["MEDIUM"],
189+
counts["HIGH"],
190+
counts["CRITICAL"],
191+
total,
192+
])
193+
194+
with summary_file.open("a", newline="", encoding="utf-8") as f:
195+
writer = csv.writer(f)
196+
writer.writerows(rows)
197+
198+
print("Trivy vulnerability summary:")
199+
for row in rows:
200+
print(row)
201+
PY
202+
203+
- name: Install kind
204+
run: |
205+
mkdir -p "$HOME/.local/bin"
206+
curl -Lo "$HOME/.local/bin/kind" https://kind.sigs.k8s.io/dl/v0.30.0/kind-linux-amd64
207+
chmod +x "$HOME/.local/bin/kind"
208+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
209+
export PATH="$HOME/.local/bin:$PATH"
210+
kind version
211+
212+
- name: Create local Kubernetes cluster
213+
run: |
214+
kind create cluster --name "$CLUSTER_NAME" --config infra/kind/kind-cluster.yaml
215+
kubectl cluster-info
216+
kubectl get nodes
217+
218+
- name: Deploy to Kubernetes
219+
run: |
220+
./ci/scripts/measure_step.sh "kubernetes_deploy" "$RESULTS_FILE" \
221+
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'
222+
223+
- name: Smoke test Kubernetes deployment
224+
run: |
225+
./ci/scripts/measure_step.sh "kubernetes_smoke_test" "$RESULTS_FILE" \
226+
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'
227+
228+
- name: Start application port-forward for DAST
229+
run: |
230+
kubectl port-forward --address 0.0.0.0 svc/tcc-api 8000:80 -n app > /tmp/c4-zap-port-forward.log 2>&1 &
231+
echo "PORT_FORWARD_PID=$!" >> "$GITHUB_ENV"
232+
sleep 5
233+
234+
- name: Verify application before DAST
235+
run: |
236+
./ci/scripts/measure_step.sh "dast_health_check" "$RESULTS_FILE" \
237+
curl -fsS http://localhost:8000/health
238+
239+
- name: Run OWASP ZAP baseline scan
240+
run: |
241+
./ci/scripts/measure_step.sh "dast_zap_baseline" "$RESULTS_FILE" \
242+
docker run --rm \
243+
--add-host=host.docker.internal:host-gateway \
244+
-v "$GITHUB_WORKSPACE/$ZAP_REPORTS_DIR:/zap/wrk/:rw" \
245+
ghcr.io/zaproxy/zaproxy:stable \
246+
zap-baseline.py \
247+
-t "$ZAP_TARGET_URL" \
248+
-m 1 \
249+
-T 5 \
250+
-I \
251+
-r zap-baseline-report.html \
252+
-J zap-baseline-report.json \
253+
-w zap-baseline-report.md
254+
255+
- name: Count OWASP ZAP alerts by severity
256+
if: always()
257+
run: |
258+
python - <<'PY'
259+
import csv
260+
import json
261+
import os
262+
from pathlib import Path
263+
264+
scenario = os.getenv("SCENARIO", "C4-sast-sca-dast")
265+
run_id = os.getenv("GITHUB_RUN_ID", "local")
266+
run_number = os.getenv("GITHUB_RUN_NUMBER", "local")
267+
commit_sha = os.getenv("GITHUB_SHA", "local")
268+
target_url = os.getenv("ZAP_TARGET_URL", "unknown")
269+
270+
summary_file = Path(os.getenv("ZAP_SUMMARY_FILE", "analysis/raw/c4_zap_summary_run.csv"))
271+
report_file = Path(os.getenv("ZAP_REPORTS_DIR", "analysis/reports/zap")) / "zap-baseline-report.json"
272+
273+
counts = {
274+
"Informational": 0,
275+
"Low": 0,
276+
"Medium": 0,
277+
"High": 0,
278+
}
279+
280+
if report_file.exists():
281+
with report_file.open("r", encoding="utf-8") as f:
282+
data = json.load(f)
283+
284+
for site in data.get("site", []):
285+
for alert in site.get("alerts", []):
286+
risk = alert.get("riskdesc", "").split(" ")[0]
287+
if risk in counts:
288+
counts[risk] += 1
289+
else:
290+
print(f"ZAP JSON report not found: {report_file}")
291+
292+
total = sum(counts.values())
293+
294+
row = [
295+
scenario,
296+
run_id,
297+
run_number,
298+
commit_sha,
299+
target_url,
300+
counts["Informational"],
301+
counts["Low"],
302+
counts["Medium"],
303+
counts["High"],
304+
total,
305+
]
306+
307+
with summary_file.open("a", newline="", encoding="utf-8") as f:
308+
writer = csv.writer(f)
309+
writer.writerow(row)
310+
311+
print("OWASP ZAP alert summary:")
312+
print(row)
313+
PY
314+
315+
- name: Stop application port-forward
316+
if: always()
317+
run: |
318+
if [ -n "${PORT_FORWARD_PID:-}" ]; then
319+
kill "$PORT_FORWARD_PID" 2>/dev/null || true
320+
fi
321+
cat /tmp/c4-zap-port-forward.log 2>/dev/null || true
322+
323+
- name: Append workflow total duration
324+
if: always()
325+
run: |
326+
WORKFLOW_END_TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
327+
WORKFLOW_END_SECONDS="$(date +%s)"
328+
WORKFLOW_DURATION_SECONDS=$((WORKFLOW_END_SECONDS - WORKFLOW_START_SECONDS))
329+
echo "${SCENARIO},${GITHUB_RUN_ID},${GITHUB_RUN_NUMBER},${GITHUB_SHA},workflow_total,${WORKFLOW_START_TIMESTAMP},${WORKFLOW_END_TIMESTAMP},${WORKFLOW_DURATION_SECONDS},success" >> "$RESULTS_FILE"
330+
331+
- name: Show collected metrics
332+
if: always()
333+
run: |
334+
echo "Collected C4 SAST + SCA + DAST metrics:"
335+
cat "$RESULTS_FILE"
336+
echo ""
337+
echo "Collected Trivy vulnerability summary:"
338+
cat "$TRIVY_SUMMARY_FILE"
339+
echo ""
340+
echo "Collected OWASP ZAP DAST summary:"
341+
cat "$ZAP_SUMMARY_FILE"
342+
343+
- name: Write GitHub Actions summary
344+
if: always()
345+
run: |
346+
echo "## C4 - SAST, SCA and DAST Pipeline" >> "$GITHUB_STEP_SUMMARY"
347+
echo "" >> "$GITHUB_STEP_SUMMARY"
348+
echo "Scenario: $SCENARIO" >> "$GITHUB_STEP_SUMMARY"
349+
echo "" >> "$GITHUB_STEP_SUMMARY"
350+
351+
echo "### Collected timing metrics" >> "$GITHUB_STEP_SUMMARY"
352+
echo "" >> "$GITHUB_STEP_SUMMARY"
353+
echo '```csv' >> "$GITHUB_STEP_SUMMARY"
354+
cat "$RESULTS_FILE" >> "$GITHUB_STEP_SUMMARY"
355+
echo '```' >> "$GITHUB_STEP_SUMMARY"
356+
357+
echo "" >> "$GITHUB_STEP_SUMMARY"
358+
echo "### Trivy vulnerability summary" >> "$GITHUB_STEP_SUMMARY"
359+
echo "" >> "$GITHUB_STEP_SUMMARY"
360+
echo '```csv' >> "$GITHUB_STEP_SUMMARY"
361+
cat "$TRIVY_SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY"
362+
echo '```' >> "$GITHUB_STEP_SUMMARY"
363+
364+
echo "" >> "$GITHUB_STEP_SUMMARY"
365+
echo "### OWASP ZAP DAST summary" >> "$GITHUB_STEP_SUMMARY"
366+
echo "" >> "$GITHUB_STEP_SUMMARY"
367+
echo '```csv' >> "$GITHUB_STEP_SUMMARY"
368+
cat "$ZAP_SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY"
369+
echo '```' >> "$GITHUB_STEP_SUMMARY"
370+
371+
- name: Upload C4 artifacts
372+
if: always()
373+
uses: actions/upload-artifact@v6
374+
with:
375+
name: c4-sast-sca-dast-artifacts
376+
path: |
377+
analysis/raw/c4_sast_sca_dast_run.csv
378+
analysis/raw/c4_trivy_summary_run.csv
379+
analysis/raw/c4_zap_summary_run.csv
380+
analysis/reports/trivy/
381+
analysis/reports/zap/

analysis/raw/c4_sast_sca_dast.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
scenario,run_id,run_number,commit_sha,step_name,start_timestamp,end_timestamp,duration_seconds,status

analysis/raw/c4_zap_summary.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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

ci/config/zap-rules.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# ZAP rules file for C4 MVP. Keep this file minimal to avoid biasing the experiment.

0 commit comments

Comments
 (0)