From 6572666007707fd6c016106b4d36156986bc258c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 8 Jul 2026 22:27:21 +0900 Subject: [PATCH] ci(trivy-fs): print the concrete findings that fail the gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trivy-fs job writes findings only to trivy-results.sarif, so a failing scan logs nothing but "Process completed with exit code 1" — every blocked PR had to inspect the uploaded SARIF to learn what actually failed. Add a read-only `if: failure()` step that parses the generated SARIF and prints each finding (rule id, Trivy severity, file:line, message) directly in the job log. The scan step still owns the pass/fail decision; the gate is unchanged (no severity/exit-code weakening). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P --- .github/workflows/security-scan.yml | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 7a077b05..3b824b7a 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -134,6 +134,54 @@ jobs: format: sarif output: trivy-results.sarif exit-code: "1" + - name: Print Trivy findings that failed the gate + # SARIF-only output means a failing scan otherwise logs nothing but + # "exit code 1". Parse the generated SARIF and print each finding + # (rule id, severity, location, message) so every trivy-fs failure + # shows its concrete reason directly in the job log. Read-only and + # non-gating: the scan step above still owns the pass/fail decision. + if: failure() && hashFiles('trivy-results.sarif') != '' + shell: python3 {0} + run: | + import json, pathlib + + sarif = json.loads(pathlib.Path("trivy-results.sarif").read_text(encoding="utf-8")) + findings = [] + for run in sarif.get("runs", []): + rules = {r["id"]: r for r in run.get("tool", {}).get("driver", {}).get("rules", [])} + for result in run.get("results", []): + rule = rules.get(result.get("ruleId", ""), {}) + severity = rule.get("properties", {}).get("security-severity", "?") + # Trivy encodes a multi-line "Key: value" body in the SARIF + # message text; surface the human-readable summary and Trivy's + # own severity label rather than the leading "Artifact:" line. + lines = (result.get("message", {}).get("text") or "").strip().splitlines() + fields = {} + for entry in lines: + key, sep, value = entry.partition(":") + if sep: + fields[key.strip().lower()] = value.strip() + if fields.get("severity"): + severity = f"{fields['severity']} (security-severity={severity})" + message = fields.get("message") or (lines[0] if lines else result.get("ruleId", "")) + locations = result.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "?") + line = phys.get("region", {}).get("startLine", "?") + where = f"{uri}:{line}" + else: + where = "-" + findings.append((severity, result.get("ruleId", "?"), where, message)) + + if not findings: + print("Trivy failed but produced no SARIF results (secret/vuln scanner error or exit-code without findings).") + else: + print(f"Trivy filesystem scan reported {len(findings)} finding(s):") + for severity, rule_id, where, message in findings: + print(f" [{severity}] {rule_id} {where} — {message}") + print("") + print("Remediate each finding at the shared base (default branch) so open PRs inherit the fix.") - name: Upload Trivy SARIF to code scanning if: always() && hashFiles('trivy-results.sarif') != '' uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2