diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd3a52e..214aedd 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 @@ -96,9 +98,12 @@ 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.14.0 + # 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 @@ -127,19 +132,17 @@ 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 || echo "Secret detection completed with findings" - if [ -f ".secrets.baseline" ]; then - detect-secrets audit .secrets.baseline --statistics || echo "Baseline audit completed" - fi - else - echo "detect-secrets not available, skipping secret scan (basic validation will still run)" - fi + 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=$? + [ "$rc" -eq 0 ] || [ "$rc" -eq 3 ] test: name: Test (matrix) diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..2e61c60 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,29 @@ +version: "2" +linters: + enable: [exhaustive, errorlint, noctx, forbidigo, gocritic] + settings: + errorlint: {errorf: true, asserts: true, comparison: true} + exhaustive: {check: [switch, map]} + 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' + failOn: dsl,import + 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/.pre-commit-config.yaml b/.pre-commit-config.yaml index 228081e..8ee44b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,9 +17,19 @@ 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.14.0 + hooks: + - id: golangci-lint-config-verify + - id: golangci-lint + + - repo: https://github.com/truvami/ci-templates + rev: c1d25f499c933042579e1fc01dee3d74f7f4b89c # pragma: allowlist secret + hooks: + - id: detect-secrets-fail-closed + - repo: https://github.com/adrienverge/yamllint rev: v1.35.1 hooks: 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" } 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/rules.go b/lint/rules.go new file mode 100644 index 0000000..6384426 --- /dev/null +++ b/lint/rules.go @@ -0,0 +1,59 @@ +//go:build ruleguard + +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("fmt") + m.Import("google.golang.org/grpc/status") + m.Import("google.golang.org/grpc/codes") + 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())`, + `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`) +} + +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($*_, $e.Error(), $*_)`). + Where(m["e"].Type.Implements("error")). + Report(`err.Error() as a label value is unbounded; use a fixed error class`) +} + +// 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(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`) +} + +// 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")). + Report(`wrap the body in io.LimitReader before io.ReadAll`) +}