Skip to content

Commit 959f73a

Browse files
authored
Merge pull request #7 from r2WillDev/feature/c3-trivy-sca
Add C3 SAST and SCA pipeline with Trivy
2 parents 8f66f19 + dd2efae commit 959f73a

6 files changed

Lines changed: 681 additions & 1 deletion

File tree

.github/workflows/c3-sast-sca.yml

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
name: C3 - SAST and SCA Pipeline
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- dev
8+
9+
env:
10+
SCENARIO: C3-sast-sca
11+
RESULTS_FILE: analysis/raw/c3_sast_sca_run.csv
12+
IMAGE_NAME: tcc-devsecops-api:local
13+
CLUSTER_NAME: tcc-devsecops
14+
SONAR_PROJECT_SETTINGS: ci/config/sonar-project.properties
15+
TRIVY_SUMMARY_FILE: analysis/raw/c3_trivy_summary_run.csv
16+
TRIVY_CONFIG: ci/config/trivy.yaml
17+
TRIVY_VERSION: v0.71.0
18+
TRIVY_REPORTS_DIR: analysis/reports/trivy
19+
20+
jobs:
21+
c3-sast-sca:
22+
name: C3 baseline plus SonarQube SAST and Trivy SCA
23+
runs-on: self-hosted
24+
25+
steps:
26+
- name: Checkout repository
27+
uses: actions/checkout@v5
28+
29+
- name: Cleanup previous kind cluster container
30+
run: |
31+
docker rm -f "${CLUSTER_NAME}-control-plane" 2>/dev/null || true
32+
33+
- name: Prepare results files
34+
run: |
35+
mkdir -p analysis/raw
36+
mkdir -p "$TRIVY_REPORTS_DIR"
37+
38+
echo "WORKFLOW_START_SECONDS=$(date +%s)" >> "$GITHUB_ENV"
39+
echo "WORKFLOW_START_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_ENV"
40+
41+
echo "scenario,run_id,run_number,commit_sha,step_name,start_timestamp,end_timestamp,duration_seconds,status" > "$RESULTS_FILE"
42+
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"
43+
44+
- name: Setup Python
45+
uses: actions/setup-python@v6
46+
with:
47+
python-version: "3.12"
48+
49+
- name: Make measurement script executable
50+
run: chmod +x ci/scripts/measure_step.sh
51+
52+
- name: Install Python dependencies
53+
run: |
54+
./ci/scripts/measure_step.sh "install_dependencies" "$RESULTS_FILE" \
55+
python -m pip install -r app/requirements.txt
56+
57+
- name: Run unit tests
58+
run: |
59+
./ci/scripts/measure_step.sh "unit_tests" "$RESULTS_FILE" \
60+
python -m pytest app -v
61+
62+
- name: Run SonarQube SAST analysis
63+
env:
64+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
65+
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
66+
run: |
67+
SONARQUBE_NETWORK=$(docker inspect tcc-sonarqube --format '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}' | head -n 1)
68+
echo "Using SonarQube Docker network: $SONARQUBE_NETWORK"
69+
70+
./ci/scripts/measure_step.sh "sonarqube_sast" "$RESULTS_FILE" \
71+
docker run --rm \
72+
--network "$SONARQUBE_NETWORK" \
73+
-e SONAR_HOST_URL="$SONAR_HOST_URL" \
74+
-e SONAR_TOKEN="$SONAR_TOKEN" \
75+
-v "$GITHUB_WORKSPACE:/usr/src" \
76+
sonarsource/sonar-scanner-cli \
77+
-Dproject.settings="$SONAR_PROJECT_SETTINGS"
78+
79+
- name: Build Docker image
80+
run: |
81+
./ci/scripts/measure_step.sh "docker_build" "$RESULTS_FILE" \
82+
docker build -t "$IMAGE_NAME" ./app
83+
84+
- name: Install Trivy
85+
run: |
86+
mkdir -p "$HOME/.local/bin"
87+
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b "$HOME/.local/bin" "$TRIVY_VERSION"
88+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
89+
export PATH="$HOME/.local/bin:$PATH"
90+
trivy --version
91+
92+
- name: Mark SCA start
93+
run: |
94+
echo "SCA_START_SECONDS=$(date +%s)" >> "$GITHUB_ENV"
95+
echo "SCA_START_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_ENV"
96+
97+
- name: Run Trivy filesystem scan
98+
run: |
99+
./ci/scripts/measure_step.sh "sca_fs" "$RESULTS_FILE" \
100+
trivy fs \
101+
--config "$TRIVY_CONFIG" \
102+
--format json \
103+
--output "$TRIVY_REPORTS_DIR/trivy-fs-report.json" \
104+
--exit-code 0 \
105+
./app
106+
107+
- name: Run Trivy image scan
108+
run: |
109+
./ci/scripts/measure_step.sh "sca_image" "$RESULTS_FILE" \
110+
trivy image \
111+
--config "$TRIVY_CONFIG" \
112+
--format json \
113+
--output "$TRIVY_REPORTS_DIR/trivy-image-report.json" \
114+
--exit-code 0 \
115+
"$IMAGE_NAME"
116+
117+
- name: Generate Trivy SARIF reports
118+
run: |
119+
trivy convert \
120+
--format sarif \
121+
--output "$TRIVY_REPORTS_DIR/trivy-fs-report.sarif" \
122+
"$TRIVY_REPORTS_DIR/trivy-fs-report.json"
123+
124+
trivy convert \
125+
--format sarif \
126+
--output "$TRIVY_REPORTS_DIR/trivy-image-report.sarif" \
127+
"$TRIVY_REPORTS_DIR/trivy-image-report.json"
128+
129+
- name: Append SCA total duration
130+
if: always()
131+
run: |
132+
SCA_END_TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
133+
SCA_END_SECONDS="$(date +%s)"
134+
SCA_DURATION_SECONDS=$((SCA_END_SECONDS - SCA_START_SECONDS))
135+
echo "${SCENARIO},${GITHUB_RUN_ID},${GITHUB_RUN_NUMBER},${GITHUB_SHA},sca_total,${SCA_START_TIMESTAMP},${SCA_END_TIMESTAMP},${SCA_DURATION_SECONDS},success" >> "$RESULTS_FILE"
136+
137+
- name: Count Trivy vulnerabilities by severity
138+
if: always()
139+
run: |
140+
python - <<'PY'
141+
import csv
142+
import json
143+
import os
144+
from pathlib import Path
145+
146+
scenario = os.getenv("SCENARIO", "C3-sast-sca")
147+
run_id = os.getenv("GITHUB_RUN_ID", "local")
148+
run_number = os.getenv("GITHUB_RUN_NUMBER", "local")
149+
commit_sha = os.getenv("GITHUB_SHA", "local")
150+
summary_file = Path(os.getenv("TRIVY_SUMMARY_FILE", "analysis/raw/c3_trivy_summary_run.csv"))
151+
152+
reports = [
153+
("filesystem", "fs", Path("analysis/reports/trivy/trivy-fs-report.json")),
154+
("image", "image", Path("analysis/reports/trivy/trivy-image-report.json")),
155+
]
156+
157+
rows = []
158+
159+
for target, scan_type, report_path in reports:
160+
counts = {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0}
161+
162+
if report_path.exists():
163+
with report_path.open("r", encoding="utf-8") as f:
164+
data = json.load(f)
165+
166+
for result in data.get("Results", []):
167+
for vuln in result.get("Vulnerabilities", []) or []:
168+
severity = vuln.get("Severity", "").upper()
169+
if severity in counts:
170+
counts[severity] += 1
171+
172+
total = sum(counts.values())
173+
174+
rows.append([
175+
scenario,
176+
run_id,
177+
run_number,
178+
commit_sha,
179+
target,
180+
scan_type,
181+
counts["LOW"],
182+
counts["MEDIUM"],
183+
counts["HIGH"],
184+
counts["CRITICAL"],
185+
total,
186+
])
187+
188+
with summary_file.open("a", newline="", encoding="utf-8") as f:
189+
writer = csv.writer(f)
190+
writer.writerows(rows)
191+
192+
print("Trivy vulnerability summary:")
193+
for row in rows:
194+
print(row)
195+
PY
196+
197+
- name: Install kind
198+
run: |
199+
mkdir -p "$HOME/.local/bin"
200+
curl -Lo "$HOME/.local/bin/kind" https://kind.sigs.k8s.io/dl/v0.30.0/kind-linux-amd64
201+
chmod +x "$HOME/.local/bin/kind"
202+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
203+
export PATH="$HOME/.local/bin:$PATH"
204+
kind version
205+
206+
- name: Create local Kubernetes cluster
207+
run: |
208+
kind create cluster --name "$CLUSTER_NAME" --config infra/kind/kind-cluster.yaml
209+
kubectl cluster-info
210+
kubectl get nodes
211+
212+
- name: Deploy to Kubernetes
213+
run: |
214+
./ci/scripts/measure_step.sh "kubernetes_deploy" "$RESULTS_FILE" \
215+
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'
216+
217+
- name: Smoke test Kubernetes deployment
218+
run: |
219+
./ci/scripts/measure_step.sh "kubernetes_smoke_test" "$RESULTS_FILE" \
220+
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'
221+
222+
- name: Append workflow total duration
223+
if: always()
224+
run: |
225+
WORKFLOW_END_TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
226+
WORKFLOW_END_SECONDS="$(date +%s)"
227+
WORKFLOW_DURATION_SECONDS=$((WORKFLOW_END_SECONDS - WORKFLOW_START_SECONDS))
228+
echo "${SCENARIO},${GITHUB_RUN_ID},${GITHUB_RUN_NUMBER},${GITHUB_SHA},workflow_total,${WORKFLOW_START_TIMESTAMP},${WORKFLOW_END_TIMESTAMP},${WORKFLOW_DURATION_SECONDS},success" >> "$RESULTS_FILE"
229+
230+
- name: Show collected metrics
231+
if: always()
232+
run: |
233+
echo "Collected C3 SAST + SCA metrics:"
234+
cat "$RESULTS_FILE"
235+
echo ""
236+
echo "Collected Trivy vulnerability summary:"
237+
cat "$TRIVY_SUMMARY_FILE"
238+
239+
- name: Write GitHub Actions summary
240+
if: always()
241+
run: |
242+
echo "## C3 - SAST and SCA Pipeline" >> "$GITHUB_STEP_SUMMARY"
243+
echo "" >> "$GITHUB_STEP_SUMMARY"
244+
echo "Scenario: $SCENARIO" >> "$GITHUB_STEP_SUMMARY"
245+
echo "" >> "$GITHUB_STEP_SUMMARY"
246+
247+
echo "### Collected timing metrics" >> "$GITHUB_STEP_SUMMARY"
248+
echo "" >> "$GITHUB_STEP_SUMMARY"
249+
echo '```csv' >> "$GITHUB_STEP_SUMMARY"
250+
cat "$RESULTS_FILE" >> "$GITHUB_STEP_SUMMARY"
251+
echo '```' >> "$GITHUB_STEP_SUMMARY"
252+
253+
echo "" >> "$GITHUB_STEP_SUMMARY"
254+
echo "### Trivy vulnerability summary" >> "$GITHUB_STEP_SUMMARY"
255+
echo "" >> "$GITHUB_STEP_SUMMARY"
256+
echo '```csv' >> "$GITHUB_STEP_SUMMARY"
257+
cat "$TRIVY_SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY"
258+
echo '```' >> "$GITHUB_STEP_SUMMARY"
259+
260+
- name: Upload C3 artifacts
261+
if: always()
262+
uses: actions/upload-artifact@v6
263+
with:
264+
name: c3-sast-sca-artifacts
265+
path: |
266+
analysis/raw/c3_sast_sca_run.csv
267+
analysis/raw/c3_trivy_summary_run.csv
268+
analysis/reports/trivy/

analysis/raw/c3_sast_sca.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/c3_trivy_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,trivy_target,trivy_scan_type,vuln_low,vuln_medium,vuln_high,vuln_critical,total_vulnerabilities

ci/config/trivy.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
timeout: 10m
2+
3+
scan:
4+
scanners:
5+
- vuln
6+
skip-dirs:
7+
- .git
8+
- .venv
9+
- venv
10+
- __pycache__
11+
- analysis
12+
- docs
13+
14+
pkg:
15+
types:
16+
- os
17+
- library
18+
19+
severity:
20+
- LOW
21+
- MEDIUM
22+
- HIGH
23+
- CRITICAL
24+
25+
vulnerability:
26+
ignore-unfixed: false
27+
28+
exit-code: 0

ci/scripts/measure_step.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env bash
1+
#!/usr/bin/env bash
22
set -euo pipefail
33

44
STEP_NAME="$1"

0 commit comments

Comments
 (0)