From 77cbef8c23f875de50816541e1cab6c201db9d06 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Thu, 14 May 2026 15:33:09 +0200 Subject: [PATCH 1/7] test(throwaway): force-validate CodeQL severity gate (DO NOT MERGE) Adds an intentional os/exec command-injection sink behind a build tag so the default `go build` is unaffected. CodeQL's go/command-injection rule fires at security-severity >= 7.0, which the scan workflow's jq post-process step turns into a non-zero exit. This commit lives on a throwaway branch and the PR is closed without merge once we capture the failing gate observation. Co-Authored-By: Claude Opus 4.7 --- throwaway_critical.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 throwaway_critical.go diff --git a/throwaway_critical.go b/throwaway_critical.go new file mode 100644 index 0000000..8643ea4 --- /dev/null +++ b/throwaway_critical.go @@ -0,0 +1,27 @@ +//go:build throwaway_codeql_gate_force_test +// +build throwaway_codeql_gate_force_test + +// This file exists ONLY to force-validate the CodeQL severity gate in +// .github/workflows/scan.yml. It uses an obviously-Critical pattern +// (untrusted command execution via os/exec.Command with a user-controlled +// argument). The build tag above keeps it out of the default build — +// `go build` produces the same binary as before. CodeQL still analyzes +// it because the CodeQL Action's autobuild does not gate on build tags. +// +// DO NOT MERGE. This PR exists to prove the workflow rejects Critical +// findings and is closed without merging once that observation is +// captured. + +package main + +import ( + "os" + "os/exec" +) + +func throwawayUnsafeExec() error { + userInput := os.Args[1] + // nosemgrep: dangerous-exec-command, go.lang.security.audit.dangerous-exec-command.dangerous-exec-command + cmd := exec.Command("sh", "-c", userInput) // CodeQL go/command-injection + return cmd.Run() +} From 085711ea310d14098b7e20d6377f00699ac7a782 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Thu, 14 May 2026 15:39:45 +0200 Subject: [PATCH 2/7] test(throwaway): remove build tag so CodeQL autobuild sees the sink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous build tag excluded throwaway_critical.go from `go build ./...`, which is exactly the command CodeQL Go autobuild runs — so CodeQL saw no Go file at all. Drop the tag and reference the function from an init guarded by a sentinel arg, so the binary's runtime behavior is unchanged but CodeQL extracts and analyzes the code path. Co-Authored-By: Claude Opus 4.7 --- throwaway_critical.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/throwaway_critical.go b/throwaway_critical.go index 8643ea4..ad31f9c 100644 --- a/throwaway_critical.go +++ b/throwaway_critical.go @@ -1,12 +1,7 @@ -//go:build throwaway_codeql_gate_force_test -// +build throwaway_codeql_gate_force_test - // This file exists ONLY to force-validate the CodeQL severity gate in // .github/workflows/scan.yml. It uses an obviously-Critical pattern // (untrusted command execution via os/exec.Command with a user-controlled -// argument). The build tag above keeps it out of the default build — -// `go build` produces the same binary as before. CodeQL still analyzes -// it because the CodeQL Action's autobuild does not gate on build tags. +// argument). // // DO NOT MERGE. This PR exists to prove the workflow rejects Critical // findings and is closed without merging once that observation is @@ -25,3 +20,11 @@ func throwawayUnsafeExec() error { cmd := exec.Command("sh", "-c", userInput) // CodeQL go/command-injection return cmd.Run() } + +// Reference the function from an init so the dead-code eliminator does +// not drop it from analysis. +func init() { + if len(os.Args) > 1 && os.Args[1] == "__throwaway_force_codeql_gate__" { + _ = throwawayUnsafeExec() + } +} From ca435fc6d17fa74e17e6eeb3bde2ad5c94ff830a Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Thu, 14 May 2026 15:45:03 +0200 Subject: [PATCH 3/7] test(throwaway): use HTTP-derived taint source (canonical CodeQL pattern) Previous attempt used os.Args[1] as the taint source. CodeQL Go's default CommandInjection query treats command-line argv as low-confidence and produced 0 results. r.URL.Query().Get(...) is the canonical HTTP-derived untrusted source the rule was authored to catch. Two sinks: exec.Command(user-controlled) and os.Open(user-controlled-path). Co-Authored-By: Claude Opus 4.7 --- throwaway_critical.go | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/throwaway_critical.go b/throwaway_critical.go index ad31f9c..ace6707 100644 --- a/throwaway_critical.go +++ b/throwaway_critical.go @@ -1,7 +1,9 @@ // This file exists ONLY to force-validate the CodeQL severity gate in -// .github/workflows/scan.yml. It uses an obviously-Critical pattern -// (untrusted command execution via os/exec.Command with a user-controlled -// argument). +// .github/workflows/scan.yml. It contains a flow from an HTTP request +// query parameter (CodeQL's canonical untrusted source) into both +// os/exec.Command and os.Open — patterns flagged by +// codeql/go-queries' CommandInjection.ql and TaintedPath.ql at +// security-severity 9.8 and 7.5 respectively. // // DO NOT MERGE. This PR exists to prove the workflow rejects Critical // findings and is closed without merging once that observation is @@ -10,21 +12,32 @@ package main import ( + "net/http" "os" "os/exec" ) -func throwawayUnsafeExec() error { - userInput := os.Args[1] +func throwawayUnsafeHandler(w http.ResponseWriter, r *http.Request) { + // `r.URL.Query().Get` is the canonical CodeQL untrusted source. + user := r.URL.Query().Get("cmd") // nosemgrep: dangerous-exec-command, go.lang.security.audit.dangerous-exec-command.dangerous-exec-command - cmd := exec.Command("sh", "-c", userInput) // CodeQL go/command-injection - return cmd.Run() + if err := exec.Command("sh", "-c", user).Run(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + path := r.URL.Query().Get("file") + // nosemgrep: go.lang.security.audit.dangerous-system-call + f, err := os.Open(path) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _ = f.Close() } -// Reference the function from an init so the dead-code eliminator does -// not drop it from analysis. func init() { if len(os.Args) > 1 && os.Args[1] == "__throwaway_force_codeql_gate__" { - _ = throwawayUnsafeExec() + http.HandleFunc("/x", throwawayUnsafeHandler) + _ = http.ListenAndServe(":0", nil) } } From f6564463e5d161211f97690699200628cb434bdf Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Thu, 14 May 2026 15:54:34 +0200 Subject: [PATCH 4/7] fix(scan): null-safe ruleIndex lookup in severity gate A small number of SARIF results emitted by github/codeql-action lack a ruleIndex (e.g., notification entries). Indexing into the rules array with a null subscript made jq exit 5 with "Cannot index array with null", which masked the actual count behind a parse error. Guard the lookup with a type check so the gate falls back to the result's own properties (or the "0" default) when ruleIndex is missing. Verified locally against a SARIF mixing well-formed and ruleIndex-less results: count returns 2 (Critical + High), no jq error. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/scan.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index c0a6fe1..7048074 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -70,10 +70,12 @@ jobs: | (.tool.driver.rules // []) as $rules | .results[] | . as $r - | (($rules[$r.ruleIndex].properties["security-severity"] - // $r.properties["security-severity"] - // "0") | tonumber) as $sev - | select($sev >= 7.0)] | length' "$sarif") + | ((if ($r.ruleIndex | type) == "number" + then $rules[$r.ruleIndex].properties["security-severity"] + else null end) + // $r.properties["security-severity"] + // "0") as $sev + | select(($sev | tonumber) >= 7.0)] | length' "$sarif") echo "Critical/High Go findings: $count" if [ "$count" -gt 0 ]; then echo "::error::CodeQL Go found $count Critical/High results" @@ -117,10 +119,12 @@ jobs: | (.tool.driver.rules // []) as $rules | .results[] | . as $r - | (($rules[$r.ruleIndex].properties["security-severity"] - // $r.properties["security-severity"] - // "0") | tonumber) as $sev - | select($sev >= 7.0)] | length' "$sarif") + | ((if ($r.ruleIndex | type) == "number" + then $rules[$r.ruleIndex].properties["security-severity"] + else null end) + // $r.properties["security-severity"] + // "0") as $sev + | select(($sev | tonumber) >= 7.0)] | length' "$sarif") echo "Critical/High Actions findings: $count" if [ "$count" -gt 0 ]; then echo "::error::CodeQL Actions found $count Critical/High results" From c557326fedf112c716117f4e311c8ba239578ad2 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Thu, 14 May 2026 16:00:51 +0200 Subject: [PATCH 5/7] debug(scan): print SARIF shape before gate to diagnose the 0-count mismatch --- .github/workflows/scan.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 7048074..1ecc0f7 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -66,6 +66,12 @@ jobs: ls -la sarif-results || true exit 1 fi + echo "=== SARIF debug ===" + echo "results: $(jq '[.runs[].results[]] | length' "$sarif")" + echo "rules: $(jq '[.runs[].tool.driver.rules // [] | .[]] | length' "$sarif")" + echo "first result keys: $(jq -c '.runs[0].results[0] // {} | keys' "$sarif")" + echo "first rule severity: $(jq -c '.runs[0].tool.driver.rules[0].properties // {}' "$sarif")" + echo "==================" count=$(jq '[.runs[] | (.tool.driver.rules // []) as $rules | .results[] From a4c2a7ea10d1aea6b2616759c37fb43ca5e045c5 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Thu, 14 May 2026 16:04:00 +0200 Subject: [PATCH 6/7] debug(scan): probe result.rule and extensions for severity location --- .github/workflows/scan.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 1ecc0f7..315b896 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -68,9 +68,10 @@ jobs: fi echo "=== SARIF debug ===" echo "results: $(jq '[.runs[].results[]] | length' "$sarif")" - echo "rules: $(jq '[.runs[].tool.driver.rules // [] | .[]] | length' "$sarif")" - echo "first result keys: $(jq -c '.runs[0].results[0] // {} | keys' "$sarif")" - echo "first rule severity: $(jq -c '.runs[0].tool.driver.rules[0].properties // {}' "$sarif")" + echo "first result.rule: $(jq -c '.runs[0].results[0].rule // {}' "$sarif")" + echo "first result.ruleId: $(jq -c '.runs[0].results[0].ruleId // null' "$sarif")" + echo "first result.properties: $(jq -c '.runs[0].results[0].properties // {}' "$sarif")" + echo "extensions rules sample: $(jq -c '[.runs[0].tool.extensions[]?.rules[]? | select(.id | test("command-injection|path-injection")) | {id, properties}]' "$sarif")" echo "==================" count=$(jq '[.runs[] | (.tool.driver.rules // []) as $rules From cc718f07cb26a72b4b760a08156ee3468aea0301 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Thu, 14 May 2026 16:10:15 +0200 Subject: [PATCH 7/7] fix(scan): look up severity in tool.extensions[].rules (CodeQL Action SARIF shape) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL Action emits the local SARIF with tool.driver.rules empty — rule definitions land in tool.extensions[].rules instead, and each result.rule has {id, index, toolComponent: {index}} pointing into those extension arrays. The previous gate only checked driver.rules and so always saw "0" for security-severity, even though Code Scanning showed Critical/High alerts for the same SARIF. New jq builds a ruleId → security-severity map from the union of driver.rules and every extension's rules, then resolves each result by its ruleId (or .rule.id fallback). Verified locally against a synthetic SARIF that mirrors the throwaway PR's two findings (go/command-injection 9.8, go/path-injection 7.5) — count: 2. Debug echoes removed; the gate is back to a single clean run/exit. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/scan.yml | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 315b896..9004361 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -66,23 +66,17 @@ jobs: ls -la sarif-results || true exit 1 fi - echo "=== SARIF debug ===" - echo "results: $(jq '[.runs[].results[]] | length' "$sarif")" - echo "first result.rule: $(jq -c '.runs[0].results[0].rule // {}' "$sarif")" - echo "first result.ruleId: $(jq -c '.runs[0].results[0].ruleId // null' "$sarif")" - echo "first result.properties: $(jq -c '.runs[0].results[0].properties // {}' "$sarif")" - echo "extensions rules sample: $(jq -c '[.runs[0].tool.extensions[]?.rules[]? | select(.id | test("command-injection|path-injection")) | {id, properties}]' "$sarif")" - echo "==================" count=$(jq '[.runs[] - | (.tool.driver.rules // []) as $rules + | (([.tool.driver.rules // []] + + [.tool.extensions[]?.rules // []]) | add) as $rules + | ($rules | map({key: (.id // ""), + value: (.properties["security-severity"] // "0")}) + | from_entries) as $sev_by_id | .results[] - | . as $r - | ((if ($r.ruleIndex | type) == "number" - then $rules[$r.ruleIndex].properties["security-severity"] - else null end) - // $r.properties["security-severity"] - // "0") as $sev - | select(($sev | tonumber) >= 7.0)] | length' "$sarif") + | (($sev_by_id[.ruleId // .rule.id // ""] + // .properties["security-severity"] + // "0") | tonumber) as $sev + | select($sev >= 7.0)] | length' "$sarif") echo "Critical/High Go findings: $count" if [ "$count" -gt 0 ]; then echo "::error::CodeQL Go found $count Critical/High results"