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
10 changes: 10 additions & 0 deletions .github/scripts/validate/report.sh
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ fi
if [[ -n "${CODEQL_RESULT:-}" && "${CODEQL_RESULT:-}" != "skipped" && "${CODEQL_RESULT:-}" != "success" ]] || \
[[ -n "${CODEQL_MEDIUMS:-}" && "${CODEQL_MEDIUMS}" != "0" && "${CODEQL_RESULT:-}" != "skipped" ]] || \
[[ -n "${CODEQL_LOWS:-}" && "${CODEQL_LOWS}" != "0" && "${CODEQL_RESULT:-}" != "skipped" ]] || \
[[ -n "${CODEQL_SUPPRESSED:-}" && "${CODEQL_SUPPRESSED}" != "0" && "${CODEQL_RESULT:-}" != "skipped" ]] || \
[[ "${CODEQL_RESULT:-}" == "skipped" && -n "${CODEQL_UNSCANNED_LANGS:-}" ]] || \
[[ "${CODEQL_RESULT:-}" != "skipped" && -n "${CODEQL_RESULT:-}" && -n "${CODEQL_UNSCANNED_LANGS:-}" ]] || \
[[ "${CLAMAV_RESULT:-}" == "failure" ]]; then
Expand Down Expand Up @@ -280,6 +281,15 @@ fi
echo "</details>"
fi

if [[ -n "${CODEQL_SUPPRESSED:-}" && "${CODEQL_SUPPRESSED:-}" != "0" && "${CODEQL_RESULT:-}" != "skipped" ]]; then
echo ""
echo "**${CODEQL_SUPPRESSED} finding(s) suppressed via inline \`codeql[...]\` comment** - requires maintainer review before merging; auto-merge is blocked for this PR."
echo ""
if [[ -f "codeql-suppressed-findings/codeql-suppressed-findings.md" ]]; then
cat "codeql-suppressed-findings/codeql-suppressed-findings.md"
fi
fi

# CodeQL skipped notice (when no scannable files exist but unscannable types were found)
if [[ "${CODEQL_RESULT:-}" == "skipped" && -n "${CODEQL_UNSCANNED_LANGS:-}" ]]; then
UNSCANNED_DISPLAY=$(echo "${CODEQL_UNSCANNED_LANGS}" | tr ',' ' ')
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/auto-merge-updates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ jobs:
has_label "Repo Update" && { echo "::notice::Has 'Repo Update' label - skipping."; OK=false; }
has_label "Invalid" && { echo "::notice::Has 'Invalid' label - skipping."; OK=false; }
has_label "QUARANTINE" && { echo "::notice::Has 'QUARANTINE' label - skipping."; OK=false; }
has_label "CodeQL Suppression Used" && { echo "::notice::PR uses a CodeQL suppression comment - skipping auto-merge, requires maintainer review."; OK=false; }
[[ "$MERGEABLE" == "MERGEABLE" ]] || { echo "::notice::PR not cleanly mergeable (mergeable=$MERGEABLE) - skipping."; OK=false; }

echo "ok=$OK" >> "$GITHUB_OUTPUT"
Expand Down
97 changes: 92 additions & 5 deletions .github/workflows/validate-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ jobs:
codeql_warnings: ${{ steps.status.outputs.codeql_warnings }}
codeql_mediums: ${{ steps.status.outputs.codeql_mediums }}
codeql_lows: ${{ steps.status.outputs.codeql_lows }}
codeql_suppressed: ${{ steps.status.outputs.codeql_suppressed }}
codeql_unscanned_langs: ${{ steps.status.outputs.codeql_unscanned_langs }}
steps:
- name: Checkout PR merge commit for analysis
Expand Down Expand Up @@ -555,6 +556,9 @@ jobs:
run: |
# Only block on security findings with CVSS score >= 7.0 (HIGH or CRITICAL).
# CodeQL stores this in rule properties["security-severity"], not in result.level.
# A result with a non-empty .suppressions array was recognized by CodeQL's own
# engine as validly suppressed (e.g. a correctly-placed inline `codeql[...]`
# comment) - trust that signal and never let it count as blocking/medium/low.
JQ_BLOCKING='
[ .runs[] |
. as $run |
Expand All @@ -564,6 +568,7 @@ jobs:
| from_entries
) as $secmap |
($run.results // [])[] |
select((.suppressions // []) | length == 0) |
((.ruleId // .rule.id // "") | tostring) as $rid |
select((($secmap[$rid] // "0") | tonumber) >= 7.0)
] | length'
Expand All @@ -576,6 +581,7 @@ jobs:
| from_entries
) as $secmap |
($run.results // [])[] |
select((.suppressions // []) | length == 0) |
((.ruleId // .rule.id // "") | tostring) as $rid |
(($secmap[$rid] // "0") | tonumber) as $sev |
select($sev >= 6.0 and $sev < 7.0)
Expand All @@ -589,13 +595,17 @@ jobs:
| from_entries
) as $secmap |
($run.results // [])[] |
select((.suppressions // []) | length == 0) |
((.ruleId // .rule.id // "") | tostring) as $rid |
(($secmap[$rid] // "0") | tonumber) as $sev |
select($sev < 6.0)
] | length'
JQ_SUPPRESSED='
[ .runs[] | (.results // [])[] | select((.suppressions // []) | length > 0) ] | length'
RESULT_COUNT=0
MEDIUM_COUNT=0
LOW_COUNT=0
SUPPRESSED_COUNT=0
TOTAL_COUNT=0
if [[ -d "sarif-results" ]]; then
for f in sarif-results/*.sarif sarif-results/*.sarif.gz; do
Expand All @@ -608,28 +618,32 @@ jobs:
COUNT=$(echo "$CONTENT" | jq "$JQ_BLOCKING")
MED=$(echo "$CONTENT" | jq "$JQ_MEDIUM")
LOW=$(echo "$CONTENT" | jq "$JQ_LOW")
SUPPRESSED=$(echo "$CONTENT" | jq "$JQ_SUPPRESSED")
TOT=$(echo "$CONTENT" | jq '[.runs[] | (.results // [])[]] | length')
echo "File $f: ${COUNT:-0} blocking, ${MED:-0} medium, ${LOW:-0} low, $TOT total"
echo "File $f: ${COUNT:-0} blocking, ${MED:-0} medium, ${LOW:-0} low, ${SUPPRESSED:-0} suppressed, $TOT total"
echo "$CONTENT" | jq -r \
'[ .runs[] | . as $run |
([ (($run.tool.driver.rules // [])[] | {key: (.id // ""), value: ((.properties["security-severity"] // "0") | tostring)}),
((($run.tool.extensions // [])[].rules // [])[] | {key: (.id // ""), value: ((.properties["security-severity"] // "0") | tostring)}) ] | from_entries) as $secmap |
($run.results // [])[] |
select((.suppressions // []) | length == 0) |
((.ruleId // .rule.id // "") | tostring) as $rid |
select((($secmap[$rid] // "0") | tonumber) >= 7.0) |
" [blocking] \($rid) sec-sev=\($secmap[$rid] // "n/a")" ] | .[]' || true
RESULT_COUNT=$((RESULT_COUNT + ${COUNT:-0}))
MEDIUM_COUNT=$((MEDIUM_COUNT + ${MED:-0}))
LOW_COUNT=$((LOW_COUNT + ${LOW:-0}))
SUPPRESSED_COUNT=$((SUPPRESSED_COUNT + ${SUPPRESSED:-0}))
TOTAL_COUNT=$((TOTAL_COUNT + ${TOT:-0}))
done
fi
WARN_COUNT=$(( TOTAL_COUNT > RESULT_COUNT ? TOTAL_COUNT - RESULT_COUNT : 0 ))
echo "Found $RESULT_COUNT high/critical, $MEDIUM_COUNT medium, $LOW_COUNT low, and $WARN_COUNT other CodeQL result(s)"
echo "Found $RESULT_COUNT high/critical, $MEDIUM_COUNT medium, $LOW_COUNT low, $SUPPRESSED_COUNT suppressed, and $WARN_COUNT other CodeQL result(s)"
echo "codeql_errors=$RESULT_COUNT" >> "$GITHUB_OUTPUT"
echo "codeql_warnings=$WARN_COUNT" >> "$GITHUB_OUTPUT"
echo "codeql_mediums=$MEDIUM_COUNT" >> "$GITHUB_OUTPUT"
echo "codeql_lows=$LOW_COUNT" >> "$GITHUB_OUTPUT"
echo "codeql_suppressed=$SUPPRESSED_COUNT" >> "$GITHUB_OUTPUT"

# Build list of external plugin path prefixes so findings links are suppressed
# for files that came from a downloaded ZIP and don't exist in this repo.
Expand Down Expand Up @@ -668,12 +682,13 @@ jobs:
| from_entries
) as $secmap |
($run.results // [])[] |
select((.suppressions // []) | length == 0) |
. as $result |
(($result.ruleId // $result.rule.id // "") | tostring) as $rid |
select((($secmap[$rid] // "0") | tonumber) >= 7.0) |
(.locations[0].physicalLocation.artifactLocation.uri // "?") as $uri |
((.locations[0].physicalLocation.region.startLine // "?") | tostring) as $line |
(.message.text // "no description" | gsub("\n"; " ") | gsub("://"; "\u200b://") | gsub("www\\."; "www\u200b.") | gsub("#(?=[0-9])"; "#\u200b") | gsub("\\[(?<c>[^\\]]+)\\][(][0-9]+[)]"; .c) | gsub("\\["; "&#91;") | gsub("\\]"; "&#93;") | if length > 150 then .[0:150] + "\u2026" else . end) as $msg |
(.message.text // "no description" | gsub("\n"; " ") | gsub("://"; "\u200b://") | gsub("www\\."; "www\u200b.") | gsub("#(?=[0-9])"; "#\u200b") | gsub("\\[(?<c>[^\\]]+)\\][(][0-9]+[)]"; .c) | gsub("\\["; "&#91;") | gsub("\\]"; "&#93;")) as $msg |
([$external_prefixes[] | . as $p | $uri | startswith($p)] | any) as $is_external |
(if ($uri != "?" and $line != "?" and ($is_external | not)) then "[\($uri):\($line)](https://github.com/\($repo)/blob/\($sha)/\($uri)#L\($line))" else "\($uri):\($line)" end) as $loc |
"| `\($rid)` | \($loc) | \($msg) |"'
Expand Down Expand Up @@ -705,13 +720,14 @@ jobs:
| from_entries
) as $secmap |
($run.results // [])[] |
select((.suppressions // []) | length == 0) |
. as $result |
(($result.ruleId // $result.rule.id // "") | tostring) as $rid |
(($secmap[$rid] // "0") | tonumber) as $sev |
select($sev >= 6.0 and $sev < 7.0) |
(.locations[0].physicalLocation.artifactLocation.uri // "?") as $uri |
((.locations[0].physicalLocation.region.startLine // "?") | tostring) as $line |
(.message.text // "no description" | gsub("\n"; " ") | gsub("://"; "\u200b://") | gsub("www\\."; "www\u200b.") | gsub("#(?=[0-9])"; "#\u200b") | gsub("\\[(?<c>[^\\]]+)\\][(][0-9]+[)]"; .c) | gsub("\\["; "&#91;") | gsub("\\]"; "&#93;") | if length > 150 then .[0:150] + "\u2026" else . end) as $msg |
(.message.text // "no description" | gsub("\n"; " ") | gsub("://"; "\u200b://") | gsub("www\\."; "www\u200b.") | gsub("#(?=[0-9])"; "#\u200b") | gsub("\\[(?<c>[^\\]]+)\\][(][0-9]+[)]"; .c) | gsub("\\["; "&#91;") | gsub("\\]"; "&#93;")) as $msg |
([$external_prefixes[] | . as $p | $uri | startswith($p)] | any) as $is_external |
(if ($uri != "?" and $line != "?" and ($is_external | not)) then "[\($uri):\($line)](https://github.com/\($repo)/blob/\($sha)/\($uri)#L\($line))" else "\($uri):\($line)" end) as $loc |
"| `\($rid)` | \($loc) | \($msg) |"'
Expand Down Expand Up @@ -743,19 +759,54 @@ jobs:
| from_entries
) as $secmap |
($run.results // [])[] |
select((.suppressions // []) | length == 0) |
. as $result |
(($result.ruleId // $result.rule.id // "") | tostring) as $rid |
(($secmap[$rid] // "0") | tonumber) as $sev |
select($sev < 6.0) |
(.locations[0].physicalLocation.artifactLocation.uri // "?") as $uri |
((.locations[0].physicalLocation.region.startLine // "?") | tostring) as $line |
(.message.text // "no description" | gsub("\n"; " ") | gsub("://"; "\u200b://") | gsub("www\\."; "www\u200b.") | gsub("#(?=[0-9])"; "#\u200b") | gsub("\\[(?<c>[^\\]]+)\\][(][0-9]+[)]"; .c) | gsub("\\["; "&#91;") | gsub("\\]"; "&#93;") | if length > 150 then .[0:150] + "\u2026" else . end) as $msg |
(.message.text // "no description" | gsub("\n"; " ") | gsub("://"; "\u200b://") | gsub("www\\."; "www\u200b.") | gsub("#(?=[0-9])"; "#\u200b") | gsub("\\[(?<c>[^\\]]+)\\][(][0-9]+[)]"; .c) | gsub("\\["; "&#91;") | gsub("\\]"; "&#93;")) as $msg |
([$external_prefixes[] | . as $p | $uri | startswith($p)] | any) as $is_external |
(if ($uri != "?" and $line != "?" and ($is_external | not)) then "[\($uri):\($line)](https://github.com/\($repo)/blob/\($sha)/\($uri)#L\($line))" else "\($uri):\($line)" end) as $loc |
"| `\($rid)` | \($loc) | \($msg) |"'
done
} > codeql-low-findings.md
fi
# Generate suppressed findings detail file for informational display in the PR comment.
# These were excluded from the blocking/medium/low tables above because CodeQL's own
# engine recognized and applied an inline suppression comment.
if [[ "$SUPPRESSED_COUNT" -gt 0 ]]; then
MERGE_SHA=${MERGE_SHA:-$(git rev-parse HEAD)}
{
echo "| Rule | Location | Description |"
echo "|------|----------|-------------|"
for f in sarif-results/*.sarif sarif-results/*.sarif.gz; do
[[ -f "$f" ]] || continue
if [[ "$f" == *.gz ]]; then
FC=$(gunzip -c "$f")
else
FC=$(cat "$f")
fi
echo "$FC" | jq -r \
--arg repo "$GITHUB_REPOSITORY" \
--arg sha "$MERGE_SHA" \
--argjson external_prefixes "$EXTERNAL_PREFIXES" \
'.runs[] |
. as $run |
($run.results // [])[] |
select((.suppressions // []) | length > 0) |
. as $result |
(($result.ruleId // $result.rule.id // "") | tostring) as $rid |
(.locations[0].physicalLocation.artifactLocation.uri // "?") as $uri |
((.locations[0].physicalLocation.region.startLine // "?") | tostring) as $line |
(.message.text // "no description" | gsub("\n"; " ") | gsub("://"; "\u200b://") | gsub("www\\."; "www\u200b.") | gsub("#(?=[0-9])"; "#\u200b") | gsub("\\[(?<c>[^\\]]+)\\][(][0-9]+[)]"; .c) | gsub("\\["; "&#91;") | gsub("\\]"; "&#93;")) as $msg |
([$external_prefixes[] | . as $p | $uri | startswith($p)] | any) as $is_external |
(if ($uri != "?" and $line != "?" and ($is_external | not)) then "[\($uri):\($line)](https://github.com/\($repo)/blob/\($sha)/\($uri)#L\($line))" else "\($uri):\($line)" end) as $loc |
"| `\($rid)` | \($loc) | \($msg) |"'
done
} > codeql-suppressed-findings.md
fi
ANALYZE_FAILED=false
if [[ "${{ steps.detect-langs.outputs.found }}" == 'true' && "${{ steps.analyze.outcome }}" != "success" && "${{ steps.analyze.outcome }}" != "" ]]; then
ANALYZE_FAILED=true
Expand Down Expand Up @@ -810,6 +861,34 @@ jobs:
path: codeql-low-findings.md
if-no-files-found: ignore

- name: Upload suppressed findings detail for PR comment
if: always()
uses: actions/upload-artifact@v7
with:
name: codeql-suppressed-findings
path: codeql-suppressed-findings.md
if-no-files-found: ignore

- name: Apply/clear CodeQL suppression label
# Purely informational for auto-merge gating (see auto-merge-updates.yml) - never
# fails the job, so a maintainer can still merge by hand after reviewing.
if: always()
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
SUPPRESSED_COUNT: ${{ steps.status.outputs.codeql_suppressed }}
run: |
gh label create "CodeQL Suppression Used" \
--color "FBCA04" \
--description "PR relies on an inline CodeQL suppression comment - requires maintainer review" \
--repo "$GH_REPO" 2>/dev/null || true
if [[ "${SUPPRESSED_COUNT:-0}" -gt 0 ]]; then
gh pr edit "$PR_NUMBER" --add-label "CodeQL Suppression Used" --repo "$GH_REPO"
else
gh pr edit "$PR_NUMBER" --remove-label "CodeQL Suppression Used" --repo "$GH_REPO" 2>/dev/null || true
fi

- name: Fail job if CodeQL found high/error/critical issues
if: always() && steps.status.outputs.codeql_status == 'failure'
run: exit 1
Expand Down Expand Up @@ -1189,6 +1268,13 @@ jobs:
path: codeql-low-findings
continue-on-error: true

- name: Download CodeQL suppressed findings detail
uses: actions/download-artifact@v8
with:
name: codeql-suppressed-findings
path: codeql-suppressed-findings
continue-on-error: true

- name: Download ClamAV findings detail
uses: actions/download-artifact@v8
with:
Expand All @@ -1207,6 +1293,7 @@ jobs:
CODEQL_WARNINGS: ${{ needs.codeql-analyze.outputs.codeql_warnings }}
CODEQL_MEDIUMS: ${{ needs.codeql-analyze.outputs.codeql_mediums }}
CODEQL_LOWS: ${{ needs.codeql-analyze.outputs.codeql_lows }}
CODEQL_SUPPRESSED: ${{ needs.codeql-analyze.outputs.codeql_suppressed }}
CODEQL_UNSCANNED_LANGS: ${{ needs.codeql-analyze.outputs.codeql_unscanned_langs }}
CLAMAV_RESULT: ${{ needs.clamav-scan.outputs.clamav_status }}
CLAMAV_INFECTED: ${{ needs.clamav-scan.outputs.clamav_infected }}
Expand Down