From e73754bcd199c0d5d18459b8a67cd3e460262089 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Tue, 29 Sep 2026 09:55:13 +0000 Subject: [PATCH 01/21] chore(lint): copy shared golangci config --- .golangci.yaml | 27 +++++++++++++++++++++++++++ lint/rules.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 .golangci.yaml create mode 100644 lint/rules.go diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..f3cde2e --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,27 @@ +version: "2" +linters: + enable: [exhaustive, errorlint, noctx, forbidigo, gocritic] + settings: + errorlint: {errorf: true, asserts: true, comparison: true} + forbidigo: + analyze-types: true + forbid: + - {pattern: '^os\.Exit$', msg: "return an error; exit only from main.go, or add //nolint:forbidigo // "} + - {pattern: '^log\.Fatal(f|ln)?$', msg: "return an error; exit only from main.go, or add //nolint:forbidigo // "} + - {pattern: '^zap\.(Logger|SugaredLogger)\.Fatal(f|w|ln)?$', pkg: '^go\.uber\.org/zap$', msg: "Fatal calls os.Exit and skips shutdown; return an error"} + - {pattern: '^uuid\.MustParse$', pkg: '^github\.com/google/uuid$', msg: "MustParse panics on bad input; use uuid.Parse and return InvalidArgument"} + - {pattern: '^otlp\w+\.WithInsecure$', msg: "plaintext OTLP exporter; make it a config opt-in or add //nolint:forbidigo // "} + - {pattern: '^http\.DefaultClient$', pkg: '^net/http$', msg: "DefaultClient has no timeout; use a client with Timeout"} + gocritic: + disable-all: true + enabled-checks: [ruleguard] + settings: + ruleguard: + rules: '${base-path}/lint/rules.go' + exclusions: + generated: lax + paths: ['api/oas_.*', 'gen/'] + rules: + - {path: '_test\.go$', linters: [forbidigo, gocritic, errorlint, noctx]} + - {path: '(^|/)main\.go$', linters: [forbidigo], text: 'os\.Exit|Fatal'} + - {path: '(^|/)(seed|test)/', linters: [forbidigo], text: 'MustParse'} diff --git a/lint/rules.go b/lint/rules.go new file mode 100644 index 0000000..5dd9fdf --- /dev/null +++ b/lint/rules.go @@ -0,0 +1,45 @@ +//go:build ignore + +package gorules + +import "github.com/quasilyte/go-ruleguard/dsl" + +func errorTextMatch(m dsl.Matcher) { + m.Match(`strings.Contains($e.Error(), $_)`, `strings.HasPrefix($e.Error(), $_)`, + `strings.HasSuffix($e.Error(), $_)`, `$e.Error() == $_`, `$e.Error() != $_`). + Where(m["e"].Type.Implements("error")). + Report(`compare errors with errors.Is/errors.As, not by message text`) +} + +func grpcInternalLeak(m dsl.Matcher) { + m.Import("google.golang.org/grpc/status") + m.Import("google.golang.org/grpc/codes") + m.Match(`status.Errorf(codes.Internal, $_, $_, $*_)`). + Report(`formatted codes.Internal can leak internals; log the cause, return a fixed message`) + m.Match(`status.Error(codes.Internal, $e.Error())`). + Where(m["e"].Type.Implements("error")). + Report(`do not send err.Error() to gRPC clients; log it, return a fixed message`) +} + +func apiMessageLeak(m dsl.Matcher) { + m.Match(`$t{$*_, Message: $e.Error(), $*_}`). + Where(m["e"].Type.Implements("error")). + Report(`do not send err.Error() to API clients; log it, return a stable message`) +} + +func promErrorLabel(m dsl.Matcher) { + m.Match(`$v.WithLabelValues($*args)`). + Where(m["args"].Text.Matches(`\.Error\(\)`)). + Report(`err.Error() as a label value is unbounded; use a fixed error class`) +} + +func httpClientNoTimeout(m dsl.Matcher) { + m.Match(`http.Client{$*fields}`). + Where(!m["fields"].Text.Matches(`\bTimeout\s*:`)). + Report(`http.Client without Timeout can hang forever; set Timeout`) +} + +func unboundedBodyRead(m dsl.Matcher) { + m.Match(`io.ReadAll($x.Body)`). + Report(`wrap the body in io.LimitReader or http.MaxBytesReader before io.ReadAll`) +} From 0f5de4afe8df54047addcb9625b3c1e6500ef542 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Tue, 29 Sep 2026 09:55:15 +0000 Subject: [PATCH 02/21] chore: adopt shared pre-commit hooks --- .pre-commit-config.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 228081e..8fdfdef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,9 +17,21 @@ repos: - id: go-fmt - id: go-imports - id: no-go-testing - - id: golangci-lint # - id: go-unit-tests + - repo: https://github.com/golangci/golangci-lint + rev: v2.12.2 + hooks: + - id: golangci-lint + args: [--new-from-rev=HEAD] + + - repo: git@github.com:truvami/ci-templates + rev: 3f3bc486f1c175d2acb665f8a5337d93f94cbcf3 # pragma: allowlist secret + hooks: + - id: goose-no-transaction + - id: no-default-encryption-key + - id: detect-secrets-fail-closed + - repo: https://github.com/adrienverge/yamllint rev: v1.35.1 hooks: From 9c16cafd62d46736315b3260efcb574ce8f075a2 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Tue, 29 Sep 2026 10:16:18 +0000 Subject: [PATCH 03/21] ci: lint only new issues and fail closed on secret scans The shared golangci config flags existing code, and decoder's own lint job was still checking the whole tree. Secret-scan failures were also swallowed, which the new hook rejects once this workflow is edited. --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd3a52e..1e487ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v5 @@ -99,6 +101,7 @@ jobs: uses: golangci/golangci-lint-action@v8 with: version: v2.10.1 + only-new-issues: true - name: Run JSON tags camelCase check run: make check-json-tags @@ -133,9 +136,9 @@ jobs: run: | echo "🔍 Scanning for secrets..." if command -v detect-secrets >/dev/null 2>&1; then - detect-secrets scan --baseline .secrets.baseline --all-files || echo "Secret detection completed with findings" + detect-secrets scan --baseline .secrets.baseline --all-files if [ -f ".secrets.baseline" ]; then - detect-secrets audit .secrets.baseline --statistics || echo "Baseline audit completed" + detect-secrets audit .secrets.baseline --statistics fi else echo "detect-secrets not available, skipping secret scan (basic validation will still run)" From 3bcd091bfb347e7c0b9157dead2849e90a75a8bd Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Tue, 29 Sep 2026 10:26:40 +0000 Subject: [PATCH 04/21] chore: pick up shared lint hook fixes Pin the shared hooks to the commit that lets only-new-issues read pull requests, and copy the tightened golangci config. --- .golangci.yaml | 3 ++- .pre-commit-config.yaml | 3 ++- lint/rules.go | 10 +++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index f3cde2e..3486153 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -18,9 +18,10 @@ linters: settings: ruleguard: rules: '${base-path}/lint/rules.go' + failOnError: true exclusions: generated: lax - paths: ['api/oas_.*', 'gen/'] + paths: ['api/oas_.*', 'gen/', 'lint/rules\\.go'] rules: - {path: '_test\.go$', linters: [forbidigo, gocritic, errorlint, noctx]} - {path: '(^|/)main\.go$', linters: [forbidigo], text: 'os\.Exit|Fatal'} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8fdfdef..d581058 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,9 +24,10 @@ repos: hooks: - id: golangci-lint args: [--new-from-rev=HEAD] + exclude: ^lint/rules\.go$ - repo: git@github.com:truvami/ci-templates - rev: 3f3bc486f1c175d2acb665f8a5337d93f94cbcf3 # pragma: allowlist secret + rev: 59b79e9eeeef382abee0d3e7dc429b71a5fe7bbe # pragma: allowlist secret hooks: - id: goose-no-transaction - id: no-default-encryption-key diff --git a/lint/rules.go b/lint/rules.go index 5dd9fdf..5111e62 100644 --- a/lint/rules.go +++ b/lint/rules.go @@ -12,11 +12,14 @@ func errorTextMatch(m dsl.Matcher) { } func grpcInternalLeak(m dsl.Matcher) { + m.Import("fmt") m.Import("google.golang.org/grpc/status") m.Import("google.golang.org/grpc/codes") - m.Match(`status.Errorf(codes.Internal, $_, $_, $*_)`). + m.Match(`status.Errorf(codes.Internal, $_, $_, $*_)`, + `status.Error(codes.Internal, fmt.Sprintf($_, $_, $*_))`). Report(`formatted codes.Internal can leak internals; log the cause, return a fixed message`) - m.Match(`status.Error(codes.Internal, $e.Error())`). + m.Match(`status.Error(codes.Internal, $e.Error())`, + `status.Errorf(codes.Internal, $e.Error())`). Where(m["e"].Type.Implements("error")). Report(`do not send err.Error() to gRPC clients; log it, return a fixed message`) } @@ -35,11 +38,12 @@ func promErrorLabel(m dsl.Matcher) { func httpClientNoTimeout(m dsl.Matcher) { m.Match(`http.Client{$*fields}`). - Where(!m["fields"].Text.Matches(`\bTimeout\s*:`)). + Where(!m["fields"].Text.Matches(`(^|,\s*)Timeout\s*:`)). Report(`http.Client without Timeout can hang forever; set Timeout`) } func unboundedBodyRead(m dsl.Matcher) { m.Match(`io.ReadAll($x.Body)`). + Where(m["x"].Type.Is("*http.Response") || m["x"].Type.Is("*http.Request")). Report(`wrap the body in io.LimitReader or http.MaxBytesReader before io.ReadAll`) } From bee91903993a5e25141f8c1c4fa253656afa95bb Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Tue, 29 Sep 2026 11:13:51 +0000 Subject: [PATCH 05/21] fix(lint): make the shared golangci config load v2.12 rejects failOnError, and the rules need the ruleguard module or lint exits before it reports anything. --- .golangci.yaml | 3 +-- .pre-commit-config.yaml | 2 +- go.mod | 5 ++++- go.sum | 2 ++ lint/deps.go | 3 +++ 5 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 lint/deps.go diff --git a/.golangci.yaml b/.golangci.yaml index 3486153..412a9d9 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -18,10 +18,9 @@ linters: settings: ruleguard: rules: '${base-path}/lint/rules.go' - failOnError: true exclusions: generated: lax - paths: ['api/oas_.*', 'gen/', 'lint/rules\\.go'] + paths: ['api/oas_.*', 'gen/', 'lint/rules\\.go', 'oas_.*\\.go'] rules: - {path: '_test\.go$', linters: [forbidigo, gocritic, errorlint, noctx]} - {path: '(^|/)main\.go$', linters: [forbidigo], text: 'os\.Exit|Fatal'} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d581058..36542a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: golangci-lint args: [--new-from-rev=HEAD] - exclude: ^lint/rules\.go$ + exclude: ^lint/ - repo: git@github.com:truvami/ci-templates rev: 59b79e9eeeef382abee0d3e7dc429b71a5fe7bbe # pragma: allowlist secret diff --git a/go.mod b/go.mod index ef20c91..b1d9f0b 100644 --- a/go.mod +++ b/go.mod @@ -62,4 +62,7 @@ require ( github.com/spf13/viper v1.21.0 ) -require golang.org/x/mod v0.33.0 +require ( + github.com/quasilyte/go-ruleguard/dsl v0.3.22 + golang.org/x/mod v0.33.0 +) diff --git a/go.sum b/go.sum index adfaaf4..2614e88 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= +github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/lint/deps.go b/lint/deps.go new file mode 100644 index 0000000..904ef5a --- /dev/null +++ b/lint/deps.go @@ -0,0 +1,3 @@ +package gorules + +import _ "github.com/quasilyte/go-ruleguard/dsl" From cb3818ceaba39b6e96cf60ad1be6d2b569e11a98 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Tue, 29 Sep 2026 11:27:14 +0000 Subject: [PATCH 06/21] fix(ci): use a valid detect-secrets audit flag --statistics is not a flag in detect-secrets 1.5.0, so the audit command exited 2. --stats reports the saved baseline and exits 0. The scan still fails when the baseline changes. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e487ae..33d6273 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,7 +138,7 @@ jobs: if command -v detect-secrets >/dev/null 2>&1; then detect-secrets scan --baseline .secrets.baseline --all-files if [ -f ".secrets.baseline" ]; then - detect-secrets audit .secrets.baseline --statistics + detect-secrets audit .secrets.baseline --stats fi else echo "detect-secrets not available, skipping secret scan (basic validation will still run)" From b5c851778252ba5d30982c7b44f68f7a1fcdcc6f Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 08:55:57 +0000 Subject: [PATCH 07/21] chore(lint): sync shared golangci config and ruleguard rules Copy the hub config and rules from truvami/ci-templates@cdcceba. rules.go now uses the ruleguard build tag, which keeps the dsl module in go.mod, so lint/deps.go is no longer needed. --- .golangci.yaml | 4 +++- lint/deps.go | 3 --- lint/rules.go | 5 ++++- 3 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 lint/deps.go diff --git a/.golangci.yaml b/.golangci.yaml index 412a9d9..2e61c60 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -3,6 +3,7 @@ linters: enable: [exhaustive, errorlint, noctx, forbidigo, gocritic] settings: errorlint: {errorf: true, asserts: true, comparison: true} + exhaustive: {check: [switch, map]} forbidigo: analyze-types: true forbid: @@ -18,9 +19,10 @@ linters: settings: ruleguard: rules: '${base-path}/lint/rules.go' + failOn: dsl,import exclusions: generated: lax - paths: ['api/oas_.*', 'gen/', 'lint/rules\\.go', 'oas_.*\\.go'] + paths: ['api/oas_.*', 'gen/'] rules: - {path: '_test\.go$', linters: [forbidigo, gocritic, errorlint, noctx]} - {path: '(^|/)main\.go$', linters: [forbidigo], text: 'os\.Exit|Fatal'} diff --git a/lint/deps.go b/lint/deps.go deleted file mode 100644 index 904ef5a..0000000 --- a/lint/deps.go +++ /dev/null @@ -1,3 +0,0 @@ -package gorules - -import _ "github.com/quasilyte/go-ruleguard/dsl" diff --git a/lint/rules.go b/lint/rules.go index 5111e62..890a45c 100644 --- a/lint/rules.go +++ b/lint/rules.go @@ -1,4 +1,4 @@ -//go:build ignore +//go:build ruleguard package gorules @@ -36,10 +36,13 @@ func promErrorLabel(m dsl.Matcher) { Report(`err.Error() as a label value is unbounded; use a fixed error class`) } +// The text match also accepts a Timeout key nested in a field value, e.g. a net.Dialer. func httpClientNoTimeout(m dsl.Matcher) { m.Match(`http.Client{$*fields}`). Where(!m["fields"].Text.Matches(`(^|,\s*)Timeout\s*:`)). Report(`http.Client without Timeout can hang forever; set Timeout`) + m.Match(`http.Client{$*_, Timeout: 0, $*_}`). + Report(`Timeout: 0 means no timeout; set a positive Timeout`) } func unboundedBodyRead(m dsl.Matcher) { From 94356d4301fbd71dd2e6bd078244d27a0370d5b8 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 08:56:10 +0000 Subject: [PATCH 08/21] chore: pin shared hooks to ci-templates cdcceba over HTTPS Use the canonical golangci-lint hooks, including config verify, and enable only detect-secrets-fail-closed: the repo has no goose migrations and no Makefile ENCRYPTION_KEY. --- .pre-commit-config.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36542a4..2bbae09 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,15 +22,12 @@ repos: - repo: https://github.com/golangci/golangci-lint rev: v2.12.2 hooks: + - id: golangci-lint-config-verify - id: golangci-lint - args: [--new-from-rev=HEAD] - exclude: ^lint/ - - repo: git@github.com:truvami/ci-templates - rev: 59b79e9eeeef382abee0d3e7dc429b71a5fe7bbe # pragma: allowlist secret + - repo: https://github.com/truvami/ci-templates + rev: cdcceba401831c5b3aed064b75f41b9419236e25 # pragma: allowlist secret hooks: - - id: goose-no-transaction - - id: no-default-encryption-key - id: detect-secrets-fail-closed - repo: https://github.com/adrienverge/yamllint From bd9726e09a882081365e60be7cb0e5b0de14fde5 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 08:57:18 +0000 Subject: [PATCH 09/21] fix(ci): make the detect-secrets check fail on new secrets detect-secrets scan --baseline rewrote the baseline and exited 0, so new secrets never failed the job. detect-secrets-hook exits non-zero on findings missing from the baseline. Pin detect-secrets to the baseline version, since a version mismatch also makes the hook rewrite the baseline and fail. --- .github/workflows/ci.yml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33d6273..ca2f026 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,19 +130,12 @@ jobs: - name: Install detect-secrets run: | - pip install detect-secrets + pip install detect-secrets==1.5.0 - name: Detect secrets run: | - echo "🔍 Scanning for secrets..." - if command -v detect-secrets >/dev/null 2>&1; then - detect-secrets scan --baseline .secrets.baseline --all-files - if [ -f ".secrets.baseline" ]; then - detect-secrets audit .secrets.baseline --stats - fi - else - echo "detect-secrets not available, skipping secret scan (basic validation will still run)" - fi + set -euo pipefail + git ls-files -z | xargs -0 detect-secrets-hook --baseline .secrets.baseline test: name: Test (matrix) From 4958d4dd79a7d419f8ff5340a82a6ed2b2339b09 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 08:57:47 +0000 Subject: [PATCH 10/21] chore(secrets): refresh baseline for findings already on main The fail-closed check flags two changed hex payloads in decoder tests and the tlvTagLteApiKey TLV tag constant in tagxl port151.go. All three are false positives; the rest is line-number drift. --- .secrets.baseline | 101 +++++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 961dbfd..b4adcf5 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -331,7 +331,7 @@ { "type": "Hex High Entropy String", "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", - "hashed_secret": "5834801389e8dc5649da5afb03daa5984c109656", + "hashed_secret": "c5a8ca343688765b47d783e2712123d1b83d70ca", "is_verified": false, "line_number": 241 }, @@ -1437,7 +1437,7 @@ { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", - "hashed_secret": "5834801389e8dc5649da5afb03daa5984c109656", + "hashed_secret": "c5a8ca343688765b47d783e2712123d1b83d70ca", "is_verified": false, "line_number": 94 }, @@ -1446,28 +1446,28 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b5950857a05765aaec2390c6fb6e6d178fde38ec", "is_verified": false, - "line_number": 110 + "line_number": 109 }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0e05716b379fab8edb51cf787e89d8113274d1d8", "is_verified": false, - "line_number": 126 + "line_number": 124 }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "cb385f264f3d41b09fb2081247e518beaa8c850c", "is_verified": false, - "line_number": 142 + "line_number": 139 }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "ed2b7805cbf0feec0ab81d143f4df1125c57bf3f", "is_verified": false, - "line_number": 178, + "line_number": 174, "is_secret": false }, { @@ -1475,14 +1475,14 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "bd38e013e088fd839d5d8f17d0ffd959632e83b1", "is_verified": false, - "line_number": 196 + "line_number": 192 }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "555917d2175ab1600c6cd926d255df6ad502febc", "is_verified": false, - "line_number": 298, + "line_number": 294, "is_secret": false }, { @@ -1490,7 +1490,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "6c45c0c1b339a7891ba24bf33ef5b40b040a86a0", "is_verified": false, - "line_number": 318, + "line_number": 314, "is_secret": false }, { @@ -1498,7 +1498,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "32179884041e9ddc27e1c5e0e45ccc6e81637d65", "is_verified": false, - "line_number": 443, + "line_number": 439, "is_secret": false }, { @@ -1506,7 +1506,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "10e8fe5b6a5342c5ead45cffec2d001a28e0c1bb", "is_verified": false, - "line_number": 456, + "line_number": 452, "is_secret": false }, { @@ -1514,7 +1514,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b0771e36dfb55414a423ca9c0ceb087b03ea3cfc", "is_verified": false, - "line_number": 461, + "line_number": 457, "is_secret": false }, { @@ -1522,7 +1522,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5cbb5bf20d8b56d849c06d5e0474a3cd42e6bc16", "is_verified": false, - "line_number": 466, + "line_number": 462, "is_secret": false }, { @@ -1530,7 +1530,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "632ee27a7e117b634cb0fb234f7f7d199db5d5d1", "is_verified": false, - "line_number": 471, + "line_number": 467, "is_secret": false }, { @@ -1538,7 +1538,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "9e1335b0e4c10057a072e6aa67e5cfb9d0e5d324", "is_verified": false, - "line_number": 476, + "line_number": 472, "is_secret": false }, { @@ -1546,35 +1546,35 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "bc4afa2f3630480ca72a32c5c8421b39f3ce1a71", "is_verified": false, - "line_number": 503 + "line_number": 499 }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "395900609a0d94e6b75d0f0cb6b647d1d554c442", "is_verified": false, - "line_number": 515 + "line_number": 511 }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "3a7e66ddf331d70868f554601fa617e041defb0f", "is_verified": false, - "line_number": 529 + "line_number": 525 }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4d3e8fa3f017be4abc77d60232e4fb4860ef42d5", "is_verified": false, - "line_number": 542 + "line_number": 538 }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "55472b68d8a8560add30831739dd3552e63d5b33", "is_verified": false, - "line_number": 573, + "line_number": 569, "is_secret": false }, { @@ -1582,7 +1582,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "463609dcc13b7b90fcf29ca237191ad5bf977c46", "is_verified": false, - "line_number": 584, + "line_number": 580, "is_secret": false }, { @@ -1590,7 +1590,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4c35a80282e5237761aeb3b9b2c8d422b16df653", "is_verified": false, - "line_number": 596, + "line_number": 592, "is_secret": false }, { @@ -1598,7 +1598,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5bcf7c2f08e382a84f0a78f1c6aa91f711806aa8", "is_verified": false, - "line_number": 609, + "line_number": 605, "is_secret": false }, { @@ -1606,7 +1606,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0560cb6af09786d2305b91018ca587c93c0d7dbd", "is_verified": false, - "line_number": 623, + "line_number": 619, "is_secret": false }, { @@ -1614,7 +1614,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "66cd8eba7181b16377a615d793be286a3aacb087", "is_verified": false, - "line_number": 633, + "line_number": 629, "is_secret": false }, { @@ -1622,7 +1622,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "a6c92fb0cd83e9a6f6f2bd5bfdb1a297dfe7a502", "is_verified": false, - "line_number": 645, + "line_number": 641, "is_secret": false }, { @@ -1630,7 +1630,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "c0e35b955de71e6fe09016adf1216ed73f1d7a8b", "is_verified": false, - "line_number": 659, + "line_number": 655, "is_secret": false }, { @@ -1638,7 +1638,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5a0861255e90d61193afbc62ee5b7924739d1b54", "is_verified": false, - "line_number": 675, + "line_number": 671, "is_secret": false }, { @@ -1646,7 +1646,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "1aa68aee442b8b1c5c9fdca3fc2e18ed2f84a637", "is_verified": false, - "line_number": 693, + "line_number": 689, "is_secret": false }, { @@ -1654,7 +1654,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "bb265a97223c679953c85c60d61907ee7683468e", "is_verified": false, - "line_number": 847, + "line_number": 843, "is_secret": false }, { @@ -1662,7 +1662,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4df89cb03f258ca60c13bf53e3442d60826bacf7", "is_verified": false, - "line_number": 853, + "line_number": 849, "is_secret": false }, { @@ -1670,7 +1670,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "f5188ea01f60dd0e30b8ff8126123c81f38ba425", "is_verified": false, - "line_number": 864, + "line_number": 860, "is_secret": false }, { @@ -1678,7 +1678,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "164c11e5bb3bdbb53a3682942846936da8006274", "is_verified": false, - "line_number": 876, + "line_number": 872, "is_secret": false }, { @@ -1686,7 +1686,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "39e57284237493c8386cbfebd10364b4f25b86bd", "is_verified": false, - "line_number": 889, + "line_number": 885, "is_secret": false }, { @@ -1694,7 +1694,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "d4fc2a168f60a698eef5c40e42f7147798791b70", "is_verified": false, - "line_number": 903, + "line_number": 899, "is_secret": false }, { @@ -1702,7 +1702,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0c24951224219592f4f044aa8c1a43cd87d14bae", "is_verified": false, - "line_number": 918, + "line_number": 914, "is_secret": false }, { @@ -1710,7 +1710,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "67dfa780930cf12323bf6d3a2737f8be7168d2e7", "is_verified": false, - "line_number": 929, + "line_number": 925, "is_secret": false }, { @@ -1718,7 +1718,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b821604371f934e1ce969c042520adc0f69859bf", "is_verified": false, - "line_number": 942, + "line_number": 938, "is_secret": false }, { @@ -1726,7 +1726,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "737544481bcf878548b5d3cef6898ebaaa307e35", "is_verified": false, - "line_number": 957, + "line_number": 953, "is_secret": false }, { @@ -1734,7 +1734,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56bdd17763f2ca6b25584e70ca4888acd267da77", "is_verified": false, - "line_number": 974, + "line_number": 970, "is_secret": false }, { @@ -1742,7 +1742,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56f27e2c927e138a36b3cb7d07b942da7667b8f2", "is_verified": false, - "line_number": 999, + "line_number": 995, "is_secret": false }, { @@ -1750,7 +1750,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "516dead2735f9bcd1eced3f678aa6dbb0ed87c86", "is_verified": false, - "line_number": 1329, + "line_number": 1325, "is_secret": false }, { @@ -1758,21 +1758,21 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "efad2d96a209d66c94f26258d9a719b1cfd15d61", "is_verified": false, - "line_number": 1358 + "line_number": 1354 }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "27f23fd7eeacf75013aa08bd69ddf71855be27e8", "is_verified": false, - "line_number": 1450 + "line_number": 1446 }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 1570, + "line_number": 1566, "is_secret": false } ], @@ -1900,6 +1900,15 @@ "is_secret": false } ], + "pkg/decoder/tagxl/v1/port151.go": [ + { + "type": "Secret Keyword", + "filename": "pkg/decoder/tagxl/v1/port151.go", + "hashed_secret": "5b8015dbc2a3ab89e8156f7ac1cfbf9d0d0858cf", + "is_verified": false, + "line_number": 103 + } + ], "pkg/decoder/tagxl/v1/response.json": [ { "type": "Hex High Entropy String", @@ -2347,5 +2356,5 @@ } ] }, - "generated_at": "2026-09-08T07:51:11Z" + "generated_at": "2026-10-19T08:57:26Z" } From e6b526c43463ae20215f3cf5a0b098b1ba06376c Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 08:57:51 +0000 Subject: [PATCH 11/21] ci: run golangci-lint v2.12.2 with lint-action v9 The shared config and ruleguard rules are tested on v2.12.2, the same version the pre-commit hook runs. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca2f026..fa2599b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,9 +98,9 @@ jobs: run: $(go env GOPATH)/bin/staticcheck ./... - name: golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v9 with: - version: v2.10.1 + version: v2.12.2 only-new-issues: true - name: Run JSON tags camelCase check From d7ea4447c24f47a7c3de4d8fceb3a63efbac30ae Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 10:31:59 +0000 Subject: [PATCH 12/21] build(lint): move to golangci-lint v2.14 and ci-templates 7850271 pre-commit builds the golangci-lint hook with the newest Go it finds. With Go 1.27, v2.12.2 panics in staticcheck; v2.13 added Go 1.27 support. The new hub commit also fixes the merge_group checkout depth and catches ENCRYPTION_KEY defaults set with define. --- .github/workflows/ci.yml | 2 +- .pre-commit-config.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa2599b..ce5edf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,7 +100,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.12.2 + version: v2.14.0 only-new-issues: true - name: Run JSON tags camelCase check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2bbae09..cbbe3bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,13 +20,13 @@ repos: # - id: go-unit-tests - repo: https://github.com/golangci/golangci-lint - rev: v2.12.2 + rev: v2.14.0 hooks: - id: golangci-lint-config-verify - id: golangci-lint - repo: https://github.com/truvami/ci-templates - rev: cdcceba401831c5b3aed064b75f41b9419236e25 # pragma: allowlist secret + rev: 7850271f878e385bdcea442c59ee8755795ce59a # pragma: allowlist secret hooks: - id: detect-secrets-fail-closed From d2c7f27460199b630f360143018260452a149955 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 11:09:00 +0000 Subject: [PATCH 13/21] fix(ci): fail the secrets check only on new secrets detect-secrets-hook exits 3 when it only rewrites baselined line numbers, and xargs turned that into 123, so any edit above a baselined line failed CI. Accept 3 and keep failing on 1. --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce5edf3..e3f08f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,7 +135,11 @@ jobs: - name: Detect secrets run: | set -euo pipefail - git ls-files -z | xargs -0 detect-secrets-hook --baseline .secrets.baseline + mapfile -d '' files < <(git ls-files -z) + # Exit 1 means new secrets. Exit 3 only means baselined line numbers moved. + rc=0 + detect-secrets-hook --baseline .secrets.baseline "${files[@]}" || rc=$? + [ "$rc" -eq 0 ] || [ "$rc" -eq 3 ] test: name: Test (matrix) From cc7cf262583eec8efad075e5175d3594497ee279 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 11:09:03 +0000 Subject: [PATCH 14/21] ci: lint new issues against the merge base with main only-new-issues gets a diff only for pull requests and pushes. A manual run linted the whole tree and failed on findings that already exist on main. --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3f08f3..089ea74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,7 +101,8 @@ jobs: uses: golangci/golangci-lint-action@v9 with: version: v2.14.0 - only-new-issues: true + # Works for every event, unlike only-new-issues, which lints the whole tree on manual runs. + args: --new-from-merge-base=origin/main - name: Run JSON tags camelCase check run: make check-json-tags From 5139a0b4395c9aff342d13110c0d2336dd007844 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 11:10:59 +0000 Subject: [PATCH 15/21] build(lint): pin ci-templates c766412 go-ci now lints new issues against the merge base, so manual runs and tag pushes no longer fail on existing findings. The shared hooks stop backtracking on long exclude lists and accept quoted $ references. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cbbe3bf..abbdf0b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: golangci-lint - repo: https://github.com/truvami/ci-templates - rev: 7850271f878e385bdcea442c59ee8755795ce59a # pragma: allowlist secret + rev: c766412c418d0b198f393ce8723f9f0d1f903133 # pragma: allowlist secret hooks: - id: detect-secrets-fail-closed From 5a5e218f770ec71c649a3f21961b8c6897b4f131 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 11:16:17 +0000 Subject: [PATCH 16/21] build(lint): pin ci-templates 70dee10 Picks up the typed err.Error() label-value rule from the hub. --- .pre-commit-config.yaml | 2 +- lint/rules.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index abbdf0b..05cc930 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: golangci-lint - repo: https://github.com/truvami/ci-templates - rev: c766412c418d0b198f393ce8723f9f0d1f903133 # pragma: allowlist secret + rev: 70dee10c98fb4fdc9524dcb0987884cf247e68bd # pragma: allowlist secret hooks: - id: detect-secrets-fail-closed diff --git a/lint/rules.go b/lint/rules.go index 890a45c..df6cd53 100644 --- a/lint/rules.go +++ b/lint/rules.go @@ -31,8 +31,8 @@ func apiMessageLeak(m dsl.Matcher) { } func promErrorLabel(m dsl.Matcher) { - m.Match(`$v.WithLabelValues($*args)`). - Where(m["args"].Text.Matches(`\.Error\(\)`)). + m.Match(`$v.WithLabelValues($*_, $e.Error(), $*_)`). + Where(m["e"].Type.Implements("error")). Report(`err.Error() as a label value is unbounded; use a fixed error class`) } From 50127cf3a276e1869b1ddb5b785686ac0803744e Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 11:43:32 +0000 Subject: [PATCH 17/21] fix(ci): fail the secrets check when there are no files to scan A git ls-files failure inside the process substitution does not trip set -e, so the hook would have scanned nothing and passed. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 089ea74..ee93d02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,6 +137,7 @@ jobs: run: | set -euo pipefail mapfile -d '' files < <(git ls-files -z) + [ "${#files[@]}" -gt 0 ] || { echo "::error::git ls-files returned no files"; exit 1; } # Exit 1 means new secrets. Exit 3 only means baselined line numbers moved. rc=0 detect-secrets-hook --baseline .secrets.baseline "${files[@]}" || rc=$? From ab4c176ff136d73b7b5e57f03c87779b0c1a59c3 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 12:39:04 +0000 Subject: [PATCH 18/21] build(lint): pin ci-templates 7b12191 go-ci lints against the parent commit on the default branch and tags, and the merge base elsewhere. The detect-secrets hook flags any || handler that does not fail closed. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 05cc930..9b1504c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: golangci-lint - repo: https://github.com/truvami/ci-templates - rev: 70dee10c98fb4fdc9524dcb0987884cf247e68bd # pragma: allowlist secret + rev: 7b12191081477587555eaa587bc67b6134fcec66 # pragma: allowlist secret hooks: - id: detect-secrets-fail-closed From 387fafc2cf97baab8182ccfe40339a858aa300fb Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Mon, 19 Oct 2026 17:50:27 +0000 Subject: [PATCH 19/21] ci: lint against the parent commit on main and tags There the merge base with main is HEAD itself, so --new-from-merge-base reported nothing. --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee93d02..214aedd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,8 +101,9 @@ jobs: uses: golangci/golangci-lint-action@v9 with: version: v2.14.0 - # Works for every event, unlike only-new-issues, which lints the whole tree on manual runs. - args: --new-from-merge-base=origin/main + # The merge base works for every event, unlike only-new-issues. On main and on + # tags it is HEAD itself, so compare with the parent commit there. + args: ${{ (github.ref_type == 'tag' || github.ref_name == 'main') && '--new-from-rev=HEAD~1' || '--new-from-merge-base=origin/main' }} - name: Run JSON tags camelCase check run: make check-json-tags From 8d36e273b545d4f1f564c5b6bb1728fa1a25de27 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Tue, 20 Oct 2026 06:43:05 +0000 Subject: [PATCH 20/21] build(lint): pin ci-templates 62b8b95 The http.Client rule now counts only a top-level Timeout, so a nested net.Dialer Timeout no longer hides a client without one. The body-read rule checks responses only, since handlers bound r.Body with http.MaxBytesReader in a statement the rule cannot see. --- .pre-commit-config.yaml | 2 +- lint/rules.go | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9b1504c..6aa306b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: golangci-lint - repo: https://github.com/truvami/ci-templates - rev: 7b12191081477587555eaa587bc67b6134fcec66 # pragma: allowlist secret + rev: 62b8b95b9342baf5484d43ebb91e596f77bd7192 # pragma: allowlist secret hooks: - id: detect-secrets-fail-closed diff --git a/lint/rules.go b/lint/rules.go index df6cd53..5ea616b 100644 --- a/lint/rules.go +++ b/lint/rules.go @@ -36,17 +36,20 @@ func promErrorLabel(m dsl.Matcher) { Report(`err.Error() as a label value is unbounded; use a fixed error class`) } -// The text match also accepts a Timeout key nested in a field value, e.g. a net.Dialer. +// Timeout must be a top-level field. The regexp skips balanced {...} up to three +// levels deep, so a net.Dialer Timeout does not count. Deeper nesting is reported. func httpClientNoTimeout(m dsl.Matcher) { m.Match(`http.Client{$*fields}`). - Where(!m["fields"].Text.Matches(`(^|,\s*)Timeout\s*:`)). + Where(!m["fields"].Text.Matches(`^(?:(?:[^{}]|\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})*,)?\s*Timeout\s*:`)). Report(`http.Client without Timeout can hang forever; set Timeout`) m.Match(`http.Client{$*_, Timeout: 0, $*_}`). Report(`Timeout: 0 means no timeout; set a positive Timeout`) } +// Responses only: a handler bounds r.Body with http.MaxBytesReader in an earlier +// statement, which a pattern cannot see. func unboundedBodyRead(m dsl.Matcher) { m.Match(`io.ReadAll($x.Body)`). - Where(m["x"].Type.Is("*http.Response") || m["x"].Type.Is("*http.Request")). - Report(`wrap the body in io.LimitReader or http.MaxBytesReader before io.ReadAll`) + Where(m["x"].Type.Is("*http.Response")). + Report(`wrap the body in io.LimitReader before io.ReadAll`) } From cbb4917cbb99db1ec3ab856c859101ebc1dc9474 Mon Sep 17 00:00:00 2001 From: Niko Kriznik Date: Tue, 20 Oct 2026 07:36:44 +0000 Subject: [PATCH 21/21] build(lint): pin ci-templates c1d25f4 The http.Client Timeout check now skips strings, runes and comments, so text in a string can neither hide nor fake a top-level Timeout. --- .pre-commit-config.yaml | 2 +- lint/rules.go | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6aa306b..8ee44b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: golangci-lint - repo: https://github.com/truvami/ci-templates - rev: 62b8b95b9342baf5484d43ebb91e596f77bd7192 # pragma: allowlist secret + rev: c1d25f499c933042579e1fc01dee3d74f7f4b89c # pragma: allowlist secret hooks: - id: detect-secrets-fail-closed diff --git a/lint/rules.go b/lint/rules.go index 5ea616b..6384426 100644 --- a/lint/rules.go +++ b/lint/rules.go @@ -36,11 +36,15 @@ func promErrorLabel(m dsl.Matcher) { Report(`err.Error() as a label value is unbounded; use a fixed error class`) } -// Timeout must be a top-level field. The regexp skips balanced {...} up to three -// levels deep, so a net.Dialer Timeout does not count. Deeper nesting is reported. +// topLevelTimeout matches a Timeout key among the top-level fields of a composite +// literal. It skips strings, runes, comments and balanced {...} up to three levels +// deep, so a net.Dialer Timeout or text in a string does not count. Deeper nesting +// is reported. Generated: atom = string|raw|rune|comment|other char|{atom*}. +const topLevelTimeout = "^(?:(?:\"(?:[^\"\\\\\\n]|\\\\.)*\"|`[^`]*`|'(?:[^'\\\\\\n]|\\\\.)*'|//[^\\n]*\\n|/\\*(?:[^*]|\\*+[^*/])*\\*+/|/[^/*]|[^{}\"\\'`/]|\\{(?:\"(?:[^\"\\\\\\n]|\\\\.)*\"|`[^`]*`|'(?:[^'\\\\\\n]|\\\\.)*'|//[^\\n]*\\n|/\\*(?:[^*]|\\*+[^*/])*\\*+/|/[^/*]|[^{}\"\\'`/]|\\{(?:\"(?:[^\"\\\\\\n]|\\\\.)*\"|`[^`]*`|'(?:[^'\\\\\\n]|\\\\.)*'|//[^\\n]*\\n|/\\*(?:[^*]|\\*+[^*/])*\\*+/|/[^/*]|[^{}\"\\'`/]|\\{(?:\"(?:[^\"\\\\\\n]|\\\\.)*\"|`[^`]*`|'(?:[^'\\\\\\n]|\\\\.)*'|//[^\\n]*\\n|/\\*(?:[^*]|\\*+[^*/])*\\*+/|/[^/*]|[^{}\"\\'`/])*\\})*\\})*\\})*,)?(?:\\s|//[^\\n]*\\n|/\\*(?:[^*]|\\*+[^*/])*\\*+/)*Timeout\\s*:" + func httpClientNoTimeout(m dsl.Matcher) { m.Match(`http.Client{$*fields}`). - Where(!m["fields"].Text.Matches(`^(?:(?:[^{}]|\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})*,)?\s*Timeout\s*:`)). + Where(!m["fields"].Text.Matches(topLevelTimeout)). Report(`http.Client without Timeout can hang forever; set Timeout`) m.Match(`http.Client{$*_, Timeout: 0, $*_}`). Report(`Timeout: 0 means no timeout; set a positive Timeout`)