From 3646399e1d346081616146f41813be1d38180d91 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 16:21:21 -0700 Subject: [PATCH 1/4] Produce gosec SARIF via golangci-lint for suppression parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The security workflow ran standalone gosec v2.25.0 with -no-fail and uploaded the SARIF to code scanning. Standalone gosec cannot read the repo's //nolint:gosec suppressions or .golangci.yml gosec excludes (its only conventions are #nosec / //gosec:disable), so every accepted site appeared as an open alert (159 on main) and any PR touching an annotated exec site turned the non-required "gosec" code-scanning check red with a new alert (#294, #307) — recurring noise with no signal, since the required Lint gate (golangci-lint) already enforces gosec with those suppressions applied. Generate the SARIF with the same pinned golangci-lint action and version as test.yml's Lint job, restricted to gosec via --enable-only, so the Security tab reflects exactly what the enforcing gate enforces. A jq step rewrites the SARIF driver name to match the historical standalone uploads (so the first main upload supersedes them, closing all 159 open alerts as "fixed") and restores per-rule G### identity that golangci-lint flattens to a single "gosec" ruleId. Tradeoff: golangci-lint's SARIF carries no driver.rules metadata, so future findings lose gosec's rich rule help text in the Security tab — acceptable since the expected steady state is zero findings. --- .github/workflows/security.yml | 35 +++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 2c2270a81..93e1bf1c4 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -91,18 +91,31 @@ jobs: with: go-version-file: 'go.mod' - - name: Run gosec + # SARIF comes from golangci-lint (gosec linter only), not the standalone + # gosec binary, so the Security tab reflects exactly what the required + # Lint gate enforces: the standalone binary can't read `//nolint:gosec` + # suppressions or .golangci.yml gosec excludes, so it perpetually + # reported every accepted site and turned the code-scanning "gosec" + # check red on any PR touching an annotated line. One suppression + # convention, one source of truth. To audit accepted findings: + # `grep -rn 'nolint:gosec'` — justifications are inline at each site. + # --issues-exit-code 0 keeps this job advisory; a real escape still + # fails the required Lint check, and code scanning still alerts on it. + - name: Run gosec (via golangci-lint) + uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 + with: + version: v2.11.1 # keep in lockstep with test.yml Lint + args: --enable-only gosec --build-tags dev --issues-exit-code 0 --output.sarif.path gosec-results.sarif + + - name: Keep code-scanning tool continuity run: | - go install github.com/securego/gosec/v2/cmd/gosec@v2.25.0 - # Advisory scan: full SARIF uploaded to the Security tab for visibility - # (-no-fail). The enforcing gosec gate is golangci-lint (make lint), - # which honors the repo's //nolint:gosec suppressions per-site; the - # standalone binary can't read those, so making it hard-fail would - # require either re-annotating every accepted site or excluding rule - # IDs globally — and a global exclude (e.g. G101/G204) would create - # real blind spots for new hard-coded creds / unsafe exec, including in - # release scans. Keep the full advisory scan instead. - gosec -tags dev -no-fail -fmt sarif -out gosec-results.sarif ./... + # Rewrite the driver name so this upload supersedes the historical + # standalone-gosec analyses (first main upload closes their alerts as + # "fixed"), and restore per-rule identity: golangci-lint flattens every + # finding to ruleId "gosec", but the G### code leads each message. + jq '.runs[].tool.driver.name = "Golang security checks by gosec" + | .runs[].results[] |= (.ruleId = ((.message.text // "" | capture("^(?G[0-9]{3}):").g) // .ruleId))' \ + gosec-results.sarif > tmp.sarif && mv tmp.sarif gosec-results.sarif - name: Upload gosec scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 From 7c4eddf92e86c39f5ccd964972899d5991e2082f Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 16:36:39 -0700 Subject: [PATCH 2/4] Debug: capture uploaded SARIF and driver name (temporary) --- .github/workflows/security.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 93e1bf1c4..c7904e157 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -116,6 +116,14 @@ jobs: jq '.runs[].tool.driver.name = "Golang security checks by gosec" | .runs[].results[] |= (.ruleId = ((.message.text // "" | capture("^(?G[0-9]{3}):").g) // .ruleId))' \ gosec-results.sarif > tmp.sarif && mv tmp.sarif gosec-results.sarif + echo "Driver name after rewrite:" + jq '.runs[].tool.driver.name' gosec-results.sarif + + - name: Save uploaded SARIF for inspection + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: gosec-sarif-debug + path: gosec-results.sarif - name: Upload gosec scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 From 381fe8b43bc14508fa51a07fb67afbefae017409 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 16:41:36 -0700 Subject: [PATCH 3/4] Revert "Debug: capture uploaded SARIF and driver name (temporary)" This reverts commit 7c4eddf92e86c39f5ccd964972899d5991e2082f. --- .github/workflows/security.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c7904e157..93e1bf1c4 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -116,14 +116,6 @@ jobs: jq '.runs[].tool.driver.name = "Golang security checks by gosec" | .runs[].results[] |= (.ruleId = ((.message.text // "" | capture("^(?G[0-9]{3}):").g) // .ruleId))' \ gosec-results.sarif > tmp.sarif && mv tmp.sarif gosec-results.sarif - echo "Driver name after rewrite:" - jq '.runs[].tool.driver.name' gosec-results.sarif - - - name: Save uploaded SARIF for inspection - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: gosec-sarif-debug - path: gosec-results.sarif - name: Upload gosec scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 From e753df1dfed181676b45929e41d28f60f12358b3 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 16:55:51 -0700 Subject: [PATCH 4/4] Make SARIF ruleId rewrite error-resilient jq's capture throws on non-matching input and // does not catch exceptions, so a gosec message without a leading G### code would fail the continuity step and block the SARIF upload. Suppress errors with ? on both the capture pipeline and the results traversal so non-matching messages keep their existing ruleId and a run without results passes through untouched. --- .github/workflows/security.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 93e1bf1c4..7fe343527 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -114,7 +114,7 @@ jobs: # "fixed"), and restore per-rule identity: golangci-lint flattens every # finding to ruleId "gosec", but the G### code leads each message. jq '.runs[].tool.driver.name = "Golang security checks by gosec" - | .runs[].results[] |= (.ruleId = ((.message.text // "" | capture("^(?G[0-9]{3}):").g) // .ruleId))' \ + | .runs[].results[]? |= (.ruleId = ((.message.text // "" | capture("^(?G[0-9]{3}):").g)? // .ruleId))' \ gosec-results.sarif > tmp.sarif && mv tmp.sarif gosec-results.sarif - name: Upload gosec scan results to GitHub Security tab