Skip to content
Draft
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
138 changes: 138 additions & 0 deletions .github/scripts/sarif.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env bash

sarif_source_modules() {
grep -oE '<module>\.\./[^<]+</module>' ddk-parent/pom.xml \
| sed -E 's#.*\.\./([^<]+)</module>#\1#' \
| while IFS= read -r module; do
if [ -f "$module/META-INF/MANIFEST.MF" ] && [ -d "$module/src" ]; then
echo "$module"
fi
done
}

validate_sarif() {
local report=$1 analyzer=${2:?expected analyzer required}
if ! jq -se --arg analyzer "$analyzer" '
def valid_base($run; $seen):
. as $id | type == "string" and ($seen | index($id) | not)
and ($run.originalUriBaseIds[$id] | type == "object")
and ($run.originalUriBaseIds[$id] |
if has("uriBaseId") then .uriBaseId | valid_base($run; $seen + [$id])
else (.uri | type == "string") end);
def optional_array($key): (has($key) | not) or (.[$key] | type == "array");
length == 1 and (.[0] |
type == "object" and .version == "2.1.0"
and (.runs | type == "array" and length > 0)
and all(.runs[];
(.tool.driver.name == $analyzer)
and (.results | type == "array")
and (. as $run | all(.. | objects | select(has("uriBaseId")); .uriBaseId | valid_base($run; [])))
and optional_array("invocations")
and ($analyzer == "Checkstyle" or (.invocations | type == "array" and length > 0))
and all(.results[]?;
type == "object" and (.message | type == "object"))
and all(.invocations[]?;
type == "object"
and .executionSuccessful == true
and optional_array("toolExecutionNotifications")
and optional_array("toolConfigurationNotifications")
and all(.toolExecutionNotifications[]?, .toolConfigurationNotifications[]?;
type == "object" and .level != "error"))))
' "$report" >/dev/null; then
echo "::error::${report} is missing, invalid SARIF, or reports unsuccessful analysis."
return 1
fi
}

merge_sarif() {
local report=$1 output=$2 modules=$3 analyzer=${4:?expected analyzer required} module
local inputs=()
for module in $modules; do
validate_sarif "${module}/target/${report}" "$analyzer" || return 1
inputs+=("${module}/target/${report}")
done
if [ ${#inputs[@]} -eq 0 ]; then
echo "::error::No expected modules supplied for ${report}."
return 1
fi
mkdir -p "$(dirname "$output")"
jq -s --arg root "${GITHUB_WORKSPACE:-$(pwd -P)}" '
def file_path:
(if startswith("file://localhost/") then ltrimstr("file://localhost")
elif startswith("file:/") then sub("^file:/+"; "/")
elif startswith("/") then . else error("Expected a file URI") end) as $path
| reduce ($path | split("/"))[] as $part ([];
if $part == "" or $part == "." then .
elif $part == ".." then
if length > 0 then .[:-1] else error("Source path escapes filesystem root") end
else . + [$part] end)
| "/" + join("/") + (if ($path | endswith("/")) and length > 0 then "/" else "" end);
def resolve_uri($run; $root):
. as $location
| (if has("uriBaseId") then $run.originalUriBaseIds[.uriBaseId] | resolve_uri($run; $root)
else $root end) as $base
| ($location.uri // "") as $uri
| if ($uri | startswith("/") or startswith("file:/")) then $uri | file_path
elif ($uri | test("^[A-Za-z][A-Za-z0-9+.-]*:")) then error("Unsupported source URI scheme")
elif $uri == "" then $base
else (($base | sub("[^/]*$"; "")) + $uri) | file_path end;
def repository_locations($root):
. as $run | del(.originalUriBaseIds)
| walk(if type == "object" and (has("uri") or has("uriBaseId")) then
(resolve_uri($run; $root)) as $absolute
| if ($absolute | startswith($root)) then
.uri = ($absolute | ltrimstr($root)) | del(.uriBaseId)
else error("Source location is outside the repository: " + $absolute) end
else . end);
def identical:
unique | if length == 1 then .[0] else error("Conflicting SARIF metadata") end;
def descriptors:
group_by(.id) | map(identical);
def compatible_run:
if ((keys - ["tool", "results", "invocations", "originalUriBaseIds", "taxonomies"]) | length) > 0
or any(.. | objects; has("index") or has("invocationIndex"))
then error("Unsupported run metadata or indexed reference; update the merger") else . end;
def resolve_rules:
.tool.driver.rules as $rules
| .results |= map(if has("ruleIndex") then
.ruleIndex as $index
| if ($index | type) != "number" or $index < 0 or ($index | floor) != $index
or $rules[$index].id == null
or (has("ruleId") and .ruleId != $rules[$index].id)
then error("Invalid ruleIndex")
else .ruleId = $rules[$index].id | del(.ruleIndex) end
else . end);
($root | split("/") | map(@uri) | join("/") | rtrimstr("/") + "/") as $root_uri
| [.[].runs[] | compatible_run | resolve_rules | repository_locations($root_uri)] as $runs
| {
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
version: "2.1.0",
runs: [{
tool: ([$runs[].tool | del(.driver.rules)] | identical
| .driver.rules = ([$runs[].tool.driver.rules[]?] | descriptors)),
taxonomies: ([$runs[].taxonomies[]?] | group_by([.name, .guid])
| map(. as $group | map(del(.taxa)) | identical
| .taxa = ([$group[].taxa[]?] | descriptors))),
results: [$runs[].results[]?],
invocations: [$runs[].invocations[]?]
}]
}
' "${inputs[@]}" > "$output"
}

cpd_count() {
local report=$1 valid count
if [ ! -s "$report" ]; then
echo "::error::${report} is missing or empty." >&2
return 1
fi
valid=$(xmllint --nonet --xpath \
'boolean(/*[local-name()="pmd-cpd"] and not(//*[local-name()="error"]) and not(/*/*[local-name()!="duplication" and local-name()!="file"]))' \
"$report") || return 1
if [ "$valid" != "true" ]; then
echo "::error::${report} is not a successful CPD report." >&2
return 1
fi
count=$(xmllint --nonet --xpath 'count(/*/*[local-name()="duplication"])' "$report") || return 1
printf '%s\n' "$count"
}
53 changes: 53 additions & 0 deletions .github/tests/analysis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Analysis regression checks

Run `bash .github/tests/analysis/run.sh` from any directory. Requires Bash,
Mike Farah yq v4, jq, and xmllint (Debian/Ubuntu: `libxml2-utils`). The verify
workflow runs the same suite. Tests use temporary directories and extract the
merge and gate blocks directly from the current workflow.

The clean fixtures were emitted by Checkstyle 14.1.0, PMD 7.27.0, and SpotBugs
4.10.4. The error fixtures use those renderers with injected processing failures;
PMD omits the notification level, so its unsuccessful invocation is essential.
Checkstyle emits no invocation metadata on clean runs. The workflow therefore
also preserves the exit status of each Maven invocation with `--fail-at-end`.

The merger supports the fields emitted by these configured producers: tool,
results, invocations, originalUriBaseIds, and taxonomies. It resolves URI-base chains and rewrites artifact locations to repository-relative
URIs, resolves result rule indices before unioning rule descriptors, retains taxonomy
descriptors, and rejects conflicting descriptors or unsupported run/indexed
metadata rather than silently dropping it. It is not a general SARIF merger.

CPD fixtures were generated by PMD 7.27.0 CpdAnalysis with a normal Java source,
two duplicate sources, and an unterminated string (a real lexical error). The
SpotBugs finding fixture comes from an actual Java 21 analysis of a null
dereference. Machine-specific absolute source roots are normalized to /checkout.

Run `bash .github/tests/analysis/mutations.sh` to verify that deliberate removal
of completion, notification, XML, URI, merged-report and process-exit protections
is detected. This suite also runs automatically in the verify workflow.

The suite exercises valid findings separately from report failures, with a clean
sibling present. It also checks completion metadata, parser failures, raw and
merged reports, scoped report lists, CPD XML errors and structural counts, chained
URI bases, encoded paths, secondary locations, rule identities and severity.


`scope.sh` exercises the actual scope script against `fixtures/scope.bundle`, a
5.4 KB Git bundle containing signed fixture commits. It covers single and multiple
changed modules, docs-only and source-less changes, mixed changes, shared config,
workflow changes, deletions and moves. Module a depends on b. The tests check skip
injection, idempotence, report lists, reactor arguments, empty scopes and failures.
The bundle preserves actual Git diff behavior without requiring signing keys or
creating unsigned commits in CI. Regenerate it by creating signed scenario commits
in a temporary repository and bundling their branch refs with `git bundle create`.
The base SARIF head has no scope script; this suite runs on the descendant heads.


The fork annotation probe demonstrated that preserving arbitrary `uriBaseId`
values was insufficient: GitHub accepted the report but stored package-relative
paths that did not match the source tree. The merger now resolves file URIs and
base chains before emitting repository-relative artifact URIs without a base ID.
The repository root is `GITHUB_WORKSPACE`, or the current directory locally.
Encoded paths and dot segments are covered; locations outside this checkout or
using unsupported schemes fail explicitly. This is intentional for these source
analyzers, which report repository sources.
26 changes: 26 additions & 0 deletions .github/tests/analysis/fixtures/checkstyle-clean.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"downloadUri": "https://github.com/checkstyle/checkstyle/releases/",
"fullName": "Checkstyle",
"informationUri": "https://checkstyle.org/",
"language": "en",
"name": "Checkstyle",
"organization": "Checkstyle",
"rules": [

],
"semanticVersion": "14.1.0",
"version": "14.1.0"
}
},
"results": [

]
}
]
}
4 changes: 4 additions & 0 deletions .github/tests/analysis/fixtures/cpd-clean.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<pmd-cpd xmlns:ns0="https://pmd-code.org/schema/cpd-report" ns0:pmdVersion="7.27.0" xmlns:ns1="https://pmd-code.org/schema/cpd-report" ns1:timestamp="2026-09-26T13:09:38.091491+02:00" xmlns:ns2="https://pmd-code.org/schema/cpd-report" ns2:version="1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://pmd-code.org/schema/cpd-report https://pmd.github.io/schema/cpd-report_1_0_0.xsd" xmlns="https://pmd-code.org/schema/cpd-report">
<file ns0:path="/checkout/a/src/Example.java" ns1:totalNumberOfTokens="28"/>
</pmd-cpd>
22 changes: 22 additions & 0 deletions .github/tests/analysis/fixtures/cpd-error.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<pmd-cpd xmlns:ns0="https://pmd-code.org/schema/cpd-report" ns0:pmdVersion="7.27.0" xmlns:ns1="https://pmd-code.org/schema/cpd-report" ns1:timestamp="2026-09-26T13:09:38.170037+02:00" xmlns:ns2="https://pmd-code.org/schema/cpd-report" ns2:version="1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://pmd-code.org/schema/cpd-report https://pmd.github.io/schema/cpd-report_1_0_0.xsd" xmlns="https://pmd-code.org/schema/cpd-report">
<file ns0:path="/checkout/a/src/Example.java" ns1:totalNumberOfTokens="28"/>
<error ns0:filename="/checkout/b/src/Bad.java" ns1:msg="LexException: Lexical error in file 'b/src/Bad.java' at line 1, column 38: &lt;EOF&gt; after : &quot;\&quot;unterminated; }&quot; (in lexical state DEFAULT)">net.sourceforge.pmd.lang.ast.LexException: Lexical error in file 'b/src/Bad.java' at line 1, column 38: &lt;EOF&gt; after : "\"unterminated; }" (in lexical state DEFAULT)
at net.sourceforge.pmd.lang.ast.InternalApiBridge.newLexException(InternalApiBridge.java:25)
at net.sourceforge.pmd.lang.java.ast.JavaParserImplTokenManager.getNextToken(JavaParserImplTokenManager.java:2386)
at net.sourceforge.pmd.lang.java.ast.JavaParserImplTokenManager.getNextToken(JavaParserImplTokenManager.java:22)
at net.sourceforge.pmd.cpd.impl.BaseTokenFilter.getNextToken(BaseTokenFilter.java:44)
at net.sourceforge.pmd.cpd.impl.CpdLexerBase.tokenize(CpdLexerBase.java:40)
at net.sourceforge.pmd.cpd.CpdLexer.tokenize(CpdLexer.java:29)
at net.sourceforge.pmd.cpd.CpdAnalysis.doTokenize(CpdAnalysis.java:150)
at net.sourceforge.pmd.cpd.CpdAnalysis.tokenizeFiles(CpdAnalysis.java:225)
at net.sourceforge.pmd.cpd.CpdAnalysis.findMatches(CpdAnalysis.java:198)
at net.sourceforge.pmd.cpd.CpdAnalysis.performAnalysis(CpdAnalysis.java:164)
at CpdProbe.main(CpdProbe.java:15)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:484)
at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:208)
at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:135)
</error>
</pmd-cpd>
10 changes: 10 additions & 0 deletions .github/tests/analysis/fixtures/cpd-finding.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<pmd-cpd xmlns:ns0="https://pmd-code.org/schema/cpd-report" ns0:pmdVersion="7.27.0" xmlns:ns1="https://pmd-code.org/schema/cpd-report" ns1:timestamp="2026-09-26T13:09:38.1622+02:00" xmlns:ns2="https://pmd-code.org/schema/cpd-report" ns2:version="1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://pmd-code.org/schema/cpd-report https://pmd.github.io/schema/cpd-report_1_0_0.xsd" xmlns="https://pmd-code.org/schema/cpd-report">
<file ns0:path="/checkout/a/src/Example.java" ns1:totalNumberOfTokens="28"/>
<file ns0:path="/checkout/b/src/Example.java" ns1:totalNumberOfTokens="28"/>
<duplication ns0:lines="1" ns1:tokens="28">
<file ns0:begintoken="0" ns1:column="1" ns2:endcolumn="79" xmlns:ns3="https://pmd-code.org/schema/cpd-report" ns3:endline="1" xmlns:ns4="https://pmd-code.org/schema/cpd-report" ns4:endtoken="27" xmlns:ns5="https://pmd-code.org/schema/cpd-report" ns5:line="1" xmlns:ns6="https://pmd-code.org/schema/cpd-report" ns6:path="/checkout/a/src/Example.java"/>
<file ns0:begintoken="29" ns1:column="1" ns2:endcolumn="79" xmlns:ns3="https://pmd-code.org/schema/cpd-report" ns3:endline="1" xmlns:ns4="https://pmd-code.org/schema/cpd-report" ns4:endtoken="56" xmlns:ns5="https://pmd-code.org/schema/cpd-report" ns5:line="1" xmlns:ns6="https://pmd-code.org/schema/cpd-report" ns6:path="/checkout/b/src/Example.java"/>
<codefragment><![CDATA[class Example { void method() { int a=1; int b=2; System.out.println(a+b); } }]]></codefragment>
</duplication>
</pmd-cpd>
24 changes: 24 additions & 0 deletions .github/tests/analysis/fixtures/pmd-clean.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "PMD",
"version": "7.27.0",
"informationUri": "https://docs.pmd-code.org/latest/",
"rules": []
}
},
"results": [],
"invocations": [
{
"executionSuccessful": true,
"toolConfigurationNotifications": [],
"toolExecutionNotifications": []
}
]
}
]
}
42 changes: 42 additions & 0 deletions .github/tests/analysis/fixtures/pmd-error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "PMD",
"version": "7.27.0",
"informationUri": "https://docs.pmd-code.org/latest/",
"rules": []
}
},
"results": [],
"invocations": [
{
"executionSuccessful": false,
"toolConfigurationNotifications": [],
"toolExecutionNotifications": [
{
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "file://src/Bad.java"
}
}
}
],
"message": {
"text": "RuntimeException: synthetic analyzer failure"
},
"exception": {
"message": "java.lang.RuntimeException: synthetic analyzer failure\n\tat Probe.lambda$main$0(Probe.java:7)\n\tat net.sourceforge.pmd.util.BaseResultProducingCloseable.using(BaseResultProducingCloseable.java:66)\n\tat net.sourceforge.pmd.reporting.Report.buildReport(Report.java:262)\n\tat Probe.main(Probe.java:7)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:580)\n\tat jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:484)\n\tat jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:208)\n\tat jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:135)\n"
}
}
]
}
]
}
]
}
Binary file added .github/tests/analysis/fixtures/scope.bundle
Binary file not shown.
1 change: 1 addition & 0 deletions .github/tests/analysis/fixtures/spotbugs-clean.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[{"tool":{"extensions":[{"version":"4.10.4","name":"edu.umd.cs.findbugs.plugins.core","shortDescription":{"text":"Core SpotBugs plugin"},"informationUri":"https://github.com/spotbugs","organization":"SpotBugs project"}],"driver":{"name":"SpotBugs","version":"4.10.4","language":"en","informationUri":"https://spotbugs.github.io/","rules":[],"supportedTaxonomies":[{"name":"CWE","guid":"b8c54a32-de19-51d2-9a08-f0abfbaa7310"}]}},"invocations":[{"exitCode":0,"exitCodeDescription":"SUCCESS","executionSuccessful":true}],"results":[],"originalUriBaseIds":{},"taxonomies":[]}]}
1 change: 1 addition & 0 deletions .github/tests/analysis/fixtures/spotbugs-error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[{"tool":{"extensions":[{"version":"4.10.4","name":"edu.umd.cs.findbugs.plugins.core","shortDescription":{"text":"Core SpotBugs plugin"},"informationUri":"https://github.com/spotbugs","organization":"SpotBugs project"}],"driver":{"name":"SpotBugs","version":"4.10.4","language":"en","informationUri":"https://spotbugs.github.io/","rules":[],"supportedTaxonomies":[{"name":"CWE","guid":"b8c54a32-de19-51d2-9a08-f0abfbaa7310"}]}},"invocations":[{"exitCode":4,"exitCodeDescription":"ERROR","executionSuccessful":false,"toolExecutionNotifications":[{"descriptor":{"id":"spotbugs-error-0"},"message":{"text":"Synthetic detector failure"},"level":"error"}]}],"results":[],"originalUriBaseIds":{},"taxonomies":[]}]}
Loading
Loading