Skip to content

Commit 5c2896c

Browse files
feat: add detailed SARIF findings to security scan summaries
Add collapsible details sections showing individual findings from CodeQL and Trivy scans in the GitHub Actions Step Summary, with automatic build failure for high-severity security issues. CodeQL improvements: - Display findings table with level, security-severity, rule, location, message - Join results with rule definitions to get security-severity scores - Upload entire codeql-results directory for multi-language support - Fail build on critical/high severity findings (security-severity >= 7.0) Trivy improvements: - Display findings table with level, rule, location, message - Correct SARIF severity mapping (error=CRITICAL, warning=HIGH, note=MEDIUM/LOW) - Fail build on critical/high severity findings Link Checker improvements: - Make results a visible heading with collapsible broken links list - Check report content for broken links instead of unreliable exit codes - Use dynamic branch ref in remap URL General fixes: - Escape pipe characters in messages to prevent markdown table corruption - Use valid HTML structure in summary elements - Use string comparison for exit code checks - Truncate messages at 120 characters for readability
1 parent 7f23ac0 commit 5c2896c

File tree

1 file changed

+110
-24
lines changed

1 file changed

+110
-24
lines changed

.github/workflows/build.yml

Lines changed: 110 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ concurrency:
7272
env:
7373
INPUT_FAIL_ON_ERROR: ${{ inputs.linkcheck_fail_on_error || 'true' }}
7474
INPUT_ISSUE_ON_ERROR: ${{ inputs.linkcheck_create_issue || 'false' }}
75+
# Effective ref for link checker remap: prefer inputs.ref if it looks like a branch name, else fall back to base_ref/ref_name
76+
# Heuristic: use inputs.ref if provided, doesn't contain '/', and doesn't start with 'v' (likely a tag)
77+
EFFECTIVE_REF: ${{ (inputs.ref && !contains(inputs.ref, '/') && !startsWith(inputs.ref, 'v')) && inputs.ref || github.base_ref || github.ref_name }}
7578
MAVEN_VERSION: 3.9.8
7679
JAVA_DISTRO: 'temurin'
7780
JAVA_VERSION_FILE: .java-version
@@ -166,6 +169,25 @@ jobs:
166169
echo "**Language:** $FILENAME" >> $GITHUB_STEP_SUMMARY
167170
echo "- Results found: $RESULTS" >> $GITHUB_STEP_SUMMARY
168171
echo "- Rules checked: $RULES" >> $GITHUB_STEP_SUMMARY
172+
# Show details if there are findings
173+
if [ "$RESULTS" -gt 0 ]; then
174+
echo "" >> $GITHUB_STEP_SUMMARY
175+
echo "<details>" >> $GITHUB_STEP_SUMMARY
176+
echo "<summary>View $RESULTS finding(s)</summary>" >> $GITHUB_STEP_SUMMARY
177+
echo "" >> $GITHUB_STEP_SUMMARY
178+
echo "| Severity | Sec-Sev | Rule | Location | Message |" >> $GITHUB_STEP_SUMMARY
179+
echo "|----------|---------|------|----------|---------|" >> $GITHUB_STEP_SUMMARY
180+
# Join results with rules to get security-severity (which is on rule definitions, not results)
181+
jq -r '
182+
(.runs[0].tool.driver.rules // []) as $driver_rules |
183+
([.runs[0].tool.extensions[]?.rules // []] | add // []) as $ext_rules |
184+
($driver_rules + $ext_rules | map({(.id): (.properties["security-severity"] // null)}) | add // {}) as $severities |
185+
.runs[0].results[] |
186+
"| \(if .level == "error" then "Error" elif .level == "warning" then "Warning" elif .level == "note" then "Note" else .level end) | \($severities[.ruleId] // "N/A") | \(.ruleId // "unknown") | `\(.locations[0].physicalLocation.artifactLocation.uri // "unknown"):\(.locations[0].physicalLocation.region.startLine // "?")` | \(.message.text | gsub("\n"; " ") | gsub("\\|"; "\\\\|") | .[0:120]) |"
187+
' "$sarif" >> $GITHUB_STEP_SUMMARY
188+
echo "" >> $GITHUB_STEP_SUMMARY
189+
echo "</details>" >> $GITHUB_STEP_SUMMARY
190+
fi
169191
fi
170192
done
171193
else
@@ -199,16 +221,28 @@ jobs:
199221
echo "" >> $GITHUB_STEP_SUMMARY
200222
if [ -f "trivy-results.sarif" ]; then
201223
TOTAL=$(jq -r '.runs[0].results | length' trivy-results.sarif 2>/dev/null || echo "0")
202-
# Trivy SARIF level mapping: error=CRITICAL, warning=HIGH, note=MEDIUM/LOW
203-
CRITICAL=$(jq -r '[.runs[0].results[] | select(.level == "error")] | length' trivy-results.sarif 2>/dev/null || echo "0")
204-
HIGH=$(jq -r '[.runs[0].results[] | select(.level == "warning")] | length' trivy-results.sarif 2>/dev/null || echo "0")
205-
MEDIUM_LOW=$(jq -r '[.runs[0].results[] | select(.level == "note")] | length' trivy-results.sarif 2>/dev/null || echo "0")
224+
# Trivy SARIF level mapping (from trivy source): CRITICAL+HIGH -> error, MEDIUM -> warning, LOW+UNKNOWN -> note
225+
CRITICAL_HIGH=$(jq -r '[.runs[0].results[] | select(.level == "error")] | length' trivy-results.sarif 2>/dev/null || echo "0")
226+
MEDIUM=$(jq -r '[.runs[0].results[] | select(.level == "warning")] | length' trivy-results.sarif 2>/dev/null || echo "0")
227+
LOW=$(jq -r '[.runs[0].results[] | select(.level == "note")] | length' trivy-results.sarif 2>/dev/null || echo "0")
206228
echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
207229
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
208-
echo "| :red_circle: Critical | $CRITICAL |" >> $GITHUB_STEP_SUMMARY
209-
echo "| :orange_circle: High | $HIGH |" >> $GITHUB_STEP_SUMMARY
210-
echo "| :yellow_circle: Medium/Low | $MEDIUM_LOW |" >> $GITHUB_STEP_SUMMARY
230+
echo "| :red_circle: Critical/High | $CRITICAL_HIGH |" >> $GITHUB_STEP_SUMMARY
231+
echo "| :orange_circle: Medium | $MEDIUM |" >> $GITHUB_STEP_SUMMARY
232+
echo "| :yellow_circle: Low | $LOW |" >> $GITHUB_STEP_SUMMARY
211233
echo "| **Total** | **$TOTAL** |" >> $GITHUB_STEP_SUMMARY
234+
# Show details if there are findings
235+
if [ "$TOTAL" -gt 0 ]; then
236+
echo "" >> $GITHUB_STEP_SUMMARY
237+
echo "<details>" >> $GITHUB_STEP_SUMMARY
238+
echo "<summary>View $TOTAL finding(s)</summary>" >> $GITHUB_STEP_SUMMARY
239+
echo "" >> $GITHUB_STEP_SUMMARY
240+
echo "| Severity | Rule | Location | Message |" >> $GITHUB_STEP_SUMMARY
241+
echo "|----------|------|----------|---------|" >> $GITHUB_STEP_SUMMARY
242+
jq -r '.runs[0].results[] | "| \(if .level == "error" then "Critical/High" elif .level == "warning" then "Medium" elif .level == "note" then "Low" else .level end) | \(.ruleId // "unknown") | `\(.locations[0].physicalLocation.artifactLocation.uri // "unknown"):\(.locations[0].physicalLocation.region.startLine // "?")` | \(.message.text | gsub("\n"; " ") | gsub("\\|"; "\\\\|") | .[0:120]) |"' trivy-results.sarif >> $GITHUB_STEP_SUMMARY
243+
echo "" >> $GITHUB_STEP_SUMMARY
244+
echo "</details>" >> $GITHUB_STEP_SUMMARY
245+
fi
212246
else
213247
echo "No Trivy results file found." >> $GITHUB_STEP_SUMMARY
214248
fi
@@ -222,14 +256,58 @@ jobs:
222256
if: ${{ !inputs.skip_code_scans && env.UPLOAD_SCAN_SARIF == 'true' }}
223257
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7
224258
with:
225-
sarif_file: codeql-results/java.sarif
259+
sarif_file: codeql-results
226260
category: 'codeql'
227261
- name: Upload Trivy scan results to GitHub Security tab
228262
if: ${{ !inputs.skip_code_scans && env.UPLOAD_SCAN_SARIF == 'true' }}
229263
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7
230264
with:
231265
sarif_file: 'trivy-results.sarif'
232266
category: 'trivy'
267+
- name: Fail on critical/high security findings
268+
if: ${{ !inputs.skip_code_scans }}
269+
run: |
270+
FAILED=false
271+
# Check CodeQL for error level (critical/high) findings
272+
if [ -d "codeql-results" ]; then
273+
for sarif in codeql-results/*.sarif; do
274+
if [ -f "$sarif" ]; then
275+
# Check for error level OR security-severity >= 7.0 (high/critical)
276+
# Note: security-severity is on rule definitions, not results, so we join via ruleId
277+
CODEQL_CRITICAL=$(jq -r '
278+
# Collect security-severity from driver and extension rules
279+
(.runs[0].tool.driver.rules // []) as $driver_rules |
280+
([.runs[0].tool.extensions[]?.rules // []] | add // []) as $ext_rules |
281+
($driver_rules + $ext_rules | map({(.id): (.properties["security-severity"] // "0")}) | add // {}) as $severities |
282+
[.runs[0].results[] | select(
283+
.level == "error" or
284+
(($severities[.ruleId] // "0") | tonumber >= 7.0)
285+
)] | length
286+
' "$sarif" 2>/dev/null || echo "0")
287+
if [ "$CODEQL_CRITICAL" -gt 0 ]; then
288+
echo "::error::CodeQL found $CODEQL_CRITICAL critical/high severity issue(s)"
289+
FAILED=true
290+
fi
291+
fi
292+
done
293+
fi
294+
# Check Trivy for critical/high (error) and medium (warning) findings
295+
# Note: Trivy SARIF mapping: CRITICAL+HIGH -> error, MEDIUM -> warning, LOW -> note
296+
if [ -f "trivy-results.sarif" ]; then
297+
TRIVY_CRITICAL_HIGH=$(jq -r '[.runs[0].results[] | select(.level == "error")] | length' trivy-results.sarif 2>/dev/null || echo "0")
298+
TRIVY_MEDIUM=$(jq -r '[.runs[0].results[] | select(.level == "warning")] | length' trivy-results.sarif 2>/dev/null || echo "0")
299+
if [ "$TRIVY_CRITICAL_HIGH" -gt 0 ]; then
300+
echo "::error::Trivy found $TRIVY_CRITICAL_HIGH critical/high severity issue(s)"
301+
FAILED=true
302+
fi
303+
if [ "$TRIVY_MEDIUM" -gt 0 ]; then
304+
echo "::warning::Trivy found $TRIVY_MEDIUM medium severity issue(s)"
305+
fi
306+
fi
307+
if [ "$FAILED" = true ]; then
308+
echo "::error::Build failed due to critical/high security findings. See summaries above for details."
309+
exit 1
310+
fi
233311
build-website:
234312
name: Website
235313
runs-on: ubuntu-24.04
@@ -276,7 +354,7 @@ jobs:
276354
if: ${{ !inputs.skip_linkcheck }}
277355
uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0
278356
with:
279-
args: --verbose --no-progress --accept 200,206,429,503 './target/staging/**/*.html' --remap "https://github.com/metaschema-framework/liboscal-java/tree/main/ file://${GITHUB_WORKSPACE}/" --remap "https://liboscal-java.metaschema.dev/ file://${GITHUB_WORKSPACE}/target/staging/"
357+
args: --verbose --no-progress --accept 200,206,429,503 './target/staging/**/*.html' --remap "https://github.com/metaschema-framework/liboscal-java/tree/${{ env.EFFECTIVE_REF }}/ file://${GITHUB_WORKSPACE}/" --remap "https://liboscal-java.metaschema.dev/ file://${GITHUB_WORKSPACE}/target/staging/"
280358
format: markdown
281359
output: html-link-report.md
282360
debug: true
@@ -287,19 +365,25 @@ jobs:
287365
- name: Link Checker Summary
288366
if: ${{ !inputs.skip_linkcheck && always() }}
289367
run: |
290-
echo "<details>" >> $GITHUB_STEP_SUMMARY
291-
echo "<summary><h2>Link Checker Results</h2></summary>" >> $GITHUB_STEP_SUMMARY
368+
echo "## Link Checker Results" >> $GITHUB_STEP_SUMMARY
292369
echo "" >> $GITHUB_STEP_SUMMARY
293370
if [ -f "html-link-report.md" ]; then
294-
# Extract summary stats from the report (lychee markdown format uses "- [Status]")
295-
ERRORS=$(grep -c "^- \[Broken\]" html-link-report.md 2>/dev/null || echo "0")
296-
TIMEOUTS=$(grep -c "^- \[Timeout\]" html-link-report.md 2>/dev/null || echo "0")
371+
# Extract summary stats from the report
372+
# Lychee uses [ERROR], [4xx], [5xx] for broken links and [TIMEOUT] for timeouts
373+
# Note: grep -c exits 1 when no matches, so we capture output first then handle exit code
374+
ERRORS=$(grep -cE "^\[ERROR\]|^\[[45][0-9]{2}\]" html-link-report.md 2>/dev/null) || ERRORS=0
375+
TIMEOUTS=$(grep -c "^\[TIMEOUT\]" html-link-report.md 2>/dev/null) || TIMEOUTS=0
297376
if [ "$ERRORS" -gt 0 ]; then
298377
echo ":x: **Found $ERRORS broken link(s)**" >> $GITHUB_STEP_SUMMARY
299378
echo "" >> $GITHUB_STEP_SUMMARY
379+
echo "<details>" >> $GITHUB_STEP_SUMMARY
380+
echo "<summary>View broken links</summary>" >> $GITHUB_STEP_SUMMARY
381+
echo "" >> $GITHUB_STEP_SUMMARY
300382
echo '```' >> $GITHUB_STEP_SUMMARY
301-
grep "^- \[Broken\]" html-link-report.md >> $GITHUB_STEP_SUMMARY
383+
grep -E "^\[ERROR\]|^\[[45][0-9]{2}\]" html-link-report.md >> $GITHUB_STEP_SUMMARY
302384
echo '```' >> $GITHUB_STEP_SUMMARY
385+
echo "" >> $GITHUB_STEP_SUMMARY
386+
echo "</details>" >> $GITHUB_STEP_SUMMARY
303387
elif [ "$TIMEOUTS" -gt 0 ]; then
304388
echo ":warning: **$TIMEOUTS link(s) timed out** (external sites may be slow)" >> $GITHUB_STEP_SUMMARY
305389
else
@@ -309,7 +393,6 @@ jobs:
309393
echo ":warning: No link check report found." >> $GITHUB_STEP_SUMMARY
310394
fi
311395
echo "" >> $GITHUB_STEP_SUMMARY
312-
echo "</details>" >> $GITHUB_STEP_SUMMARY
313396
- name: Upload link check report
314397
if: ${{ !inputs.skip_linkcheck }}
315398
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
@@ -319,7 +402,7 @@ jobs:
319402
retention-days: 5
320403
- name: Create issue if bad links detected
321404
# Only create issues for actual broken links (exit code 2), not timeouts (exit code 1)
322-
if: ${{ !inputs.skip_linkcheck && !cancelled() && steps.linkchecker.outputs.exit_code == 2 && env.INPUT_ISSUE_ON_ERROR == 'true' }}
405+
if: ${{ !inputs.skip_linkcheck && !cancelled() && steps.linkchecker.outputs.exit_code == '2' && env.INPUT_ISSUE_ON_ERROR == 'true' }}
323406
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710
324407
with:
325408
title: Scheduled Check of Website Content Found Bad Hyperlinks
@@ -328,10 +411,13 @@ jobs:
328411
bug
329412
documentation
330413
- name: Fail on link check error
331-
# Exit codes: 0=success, 1=runtime errors/timeouts, 2=broken links found
332-
# Only fail on actual broken links (exit code 2), not timeouts (exit code 1)
333-
if: ${{ !inputs.skip_linkcheck && !cancelled() && steps.linkchecker.outputs.exit_code == 2 && env.INPUT_FAIL_ON_ERROR == 'true' }}
334-
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
335-
with:
336-
script: |
337-
core.setFailed('Link checker detected broken or invalid links, read attached report.')
414+
# Check report for actual broken links (ERROR, 4xx, 5xx), not timeouts
415+
if: ${{ !inputs.skip_linkcheck && !cancelled() && env.INPUT_FAIL_ON_ERROR == 'true' }}
416+
run: |
417+
if [ -f "html-link-report.md" ]; then
418+
ERRORS=$(grep -cE "^\[ERROR\]|^\[[45][0-9]{2}\]" html-link-report.md 2>/dev/null) || ERRORS=0
419+
if [ "$ERRORS" -gt 0 ]; then
420+
echo "::error::Link checker found $ERRORS broken link(s). See report for details."
421+
exit 1
422+
fi
423+
fi

0 commit comments

Comments
 (0)