feat(security): vendor Semgrep r/all ruleset with tracked exclusions#220
feat(security): vendor Semgrep r/all ruleset with tracked exclusions#220scottschreckengaust wants to merge 18 commits into
Conversation
Replace the PR#TBD placeholders in exclusions.toml and the generated EXCLUSIONS.md with this PR's number (#220). PR#89 (the OpenAI rule) is left as-is. Same-length swap; no table realignment or rule content change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements RFC #211. All Semgrep invocations (pre-commit, CI, mise) now run against a single pinned, locally-vendored ruleset instead of fetching r/all live from the registry on every scan. Exclusions are baked into the vendored file, and a generated doc records what was decided and why. Adds tools/semgrep/: - update.py stdlib-only update mechanism (goal C). Downloads r/all, diffs each rule's version_id against the prior snapshot to derive a "last changed" date, drops excluded rules, and regenerates the artifacts. Byte-preserving: it removes whole rule blocks and never re-serializes YAML (re-serializing was verified to silently break the ruleset). - exclusions.toml human-owned source of truth (id -> status/pr/reason), seeded from the 22 pre-existing --exclude-rule flags. - r-all.active.yaml pre-filtered rules; the only file scans load (goal A). - rule-state.json version_id/date baseline for diffing. - EXCLUSIONS.md generated tracking table (goal B). Integration: - mise.toml: SEMGREP_RULES env (bare `semgrep` uses vendored rules with zero flags) + `semgrep:update` task; drop 22 --exclude-rule flags. - CI + pre-commit: --config=tools/semgrep/r-all.active.yaml; drop exclude flags. Baseline/SARIF logic unchanged. - .semgrepignore: skip tools/semgrep/*.yaml (rules self-match). - .gitleaks.toml: allowlist the active file (contains secret regexes). - dprint.json: exclude generated EXCLUSIONS.md / rule-state.json. - .pre-commit-config.yaml: top-level `exclude` for the generated files (r-all.active.yaml, rule-state.json, EXCLUSIONS.md) so content hooks (detect-private-key, check-vcs-permalinks, check-added-large-files) skip them and autofix hooks never corrupt the byte-for-byte ruleset. The hand-authored update.py/exclusions.toml stay in scope. - Remove stub .semgrep.yaml (never loaded by Semgrep; misleading). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the PR#TBD placeholders in exclusions.toml and the generated EXCLUSIONS.md with this PR's number (#220). PR#89 (the OpenAI rule) is left as-is. Same-length swap; no table realignment or rule content change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebased onto main, which added an exclusion for `bbp-pattern-inject` (PR #200) — a malformed community rule that fails to parse ("Invalid pattern for Python: Stdlib.Parsing.Parse_error") and makes semgrep exit 2 on every run. main handled it with a --exclude-rule flag; this expresses the same decision through the vendoring mechanism by adding it to exclusions.toml, so update.py physically drops it from r-all.active.yaml. Regenerated the active file, rule-state.json, and EXCLUSIONS.md (23 excluded / 3108 active). Verified: semgrep now exits 0 with no parse errors against the vendored ruleset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
05b878c to
2ca2848
Compare
The r/all feed carries 104 rules with source=https://semgrep.dev/r/None: a single-burst set of offensive-security probes (SSRF via rule metadata targeting 169.254.169.254, stored-XSS payloads in message/fix fields, command-injection and secret-exfil tests, plus PWNED-PRODUCTION-RULE and a rule impersonating a real id). None have a canonical registry source; none appear in semgrep/semgrep-rules. See PR #220 provenance analysis. Rather than list 104 ids by hand, add a source-based auto-exclusion policy so this generalizes to FUTURE probes: - exclusions.toml gains [auto_exclude]: any rule whose `source` matches a listed value is dropped automatically. A rule with no canonical source (/r/None) is not a curated community rule — this is how new r/all rules are judged (no positive "valid" signal exists; absence of a real source is a reliable invalidity signal). An explicit [rules."id"] status=active force-keeps a rule the policy would drop. - update.py parses each rule's `source`, applies the policy, reports "N auto-excluded", and lists them in EXCLUSIONS.md as `auto-excluded`. - Harden the doc generator: sanitize untrusted rule messages (escape <>`| ) so XSS/markup payloads in rule text render as inert text instead of injecting markup or breaking the table (this is what test-bbp-xss-msg's "<img src=x onerror=...>" message was probing). Result: 3005 active / 126 excluded. The real subprocess-shell-true rule survives (the /None impersonator is dropped), semgrep exits 0 with no parse errors, and the Semgrep OSS code-scanning check clears (bbp-rate-check no longer flags print() calls). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dprint already leaves rule-state.json untouched — update.py emits json.dumps(indent=2, sort_keys=True), which matches dprint's JSON style (verified: `dprint check` exits 0 with no diff). The exclude was belt-and-suspenders and, since dprint.json is strict JSON with no comment syntax, it read as accidental. Remove it and document the generated-file formatting policy in update.py's docstring instead, where comments are allowed. EXCLUSIONS.md stays dprint-excluded (dprint would reflow its tables and churn on every regeneration); rule-state.json stays in the pre-commit exclude (large-file/private-key hooks would still act on it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
update.py's render_table now column-pads the EXCLUSIONS.md markdown table exactly as the dprint markdown plugin does (each cell ljust to its column's max code-point width, delimiter dashes to match, min 3). Verified: with the exclude removed, `dprint check tools/semgrep/EXCLUSIONS.md` exits 0 with no diff, so the generated file already matches what dprint would produce. Removes the last tools/semgrep entry from dprint.json's excludes — both generated files (EXCLUSIONS.md, rule-state.json) are now emitted in dprint's own format, so neither needs a formatter exclude. They remain in the pre-commit exclude only for content hooks (large-file/private-key/whitespace). Trade-off: render_table mirrors dprint's table algorithm, so a future markdown-plugin bump or config change could require a matching update.py tweak; the build's fmt:check catches any such drift immediately. Cells are already sanitized ASCII, so len()-based widths match dprint's. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Semgrep registry serves r/all in a nondeterministic block order, so re-running update.py reshuffled r-all.active.yaml and produced a spurious ~46k-line diff even when no rule actually changed (verified: same ids, same version_ids, 0 added/removed/changed — order only). That would make every future `mise run semgrep:update` unreviewable. Sort active rule blocks by rule id before writing. Now output is a pure function of content: two consecutive runs are byte-identical, and a real upstream change is the only thing that shows up in git — matching the RFC's "reviewable drift" goal (rule-state.json is already sort_keys=True). Verified: 3005 rules intact, semgrep scans the sorted file with exit 0 and no parse errors, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address four Semgrep code-scanning findings with real code changes, not # nosec/# noqa suppressions (all suppression comments removed). CWE-939 / dynamic-urllib-use-detected (urlopen with non-literal URL): the default urllib opener installs file:// and ftp:// handlers, so a URL resolving to another scheme could read local files. Replace urllib.request.urlopen with an OpenerDirector that installs ONLY the HTTPS/redirect/error handlers — no FileHandler/FTPHandler — so non-HTTPS schemes have no handler and are unreachable by construction, plus an explicit https scheme guard. Verified: file:// and ftp:// return no content; https download of r/all still succeeds byte-for-byte. string-concat-in-list (implicitly concatenated string literals in the render_doc lines list): make the concatenation explicit with `+` so each list element is unambiguously one string (guards against a missing-comma bug the rule warns about). Verified with the exact flagged rules: dynamic-urllib 1->0 findings, string-concat-in-list 0 findings, bandit "No issues" with 0 nosec skips, update.py still produces byte-identical output (3005 active rules). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the [auto_exclude] source policy with explicit per-rule entries, per review feedback. Three concerns addressed: 1. No silent exclusion. source=/r/None is a reliable junk signal *today* (all 104 probes lack the `license:` field every real rule carries), but it is not guaranteed — a future legitimate rule could carry /r/None and would have been auto-dropped. Now every exclusion is listed explicitly in exclusions.toml; nothing is dropped implicitly, so a new upstream rule can never silently disappear — it surfaces as `new` for triage. The 79 probe rules (78 + bbp-pattern-inject) are now explicit entries. 2. Correct PR provenance (from git pickaxe, replacing the earlier PR#TBD->220 guess): the 21 original exclusions were PR #11 (2026-02-07), OpenAI was PR #89, bbp-pattern-inject was PR #200. Only the 78 new probe exclusions are PR #220. 3. Duplicate rows fixed. r/all reuses some ids across blocks (bbp-x x11, bbp-ssrf x8, ...); EXCLUSIONS.md now collapses rows by id. Status legend documents excluded/active/new (auto-excluded removed). Also: when an excluded rule is later removed upstream, update.py now surfaces the orphaned entry in a "Removed upstream (safe to prune)" doc section (and console), rather than only a console warning — a human prunes it, nothing auto-deletes. Result: 101 explicit entries (21 PR#11, 1 PR#89, 1 PR#200, 78 PR#220), 3005 active rules, semgrep exit 0 with no parse errors, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ions Make the three semgrep entry points consistent on the cosmetic log flags (these affect only stderr verbosity — never findings or exit code): - mise `security:semgrep`: swap `--quiet` for `--verbose` (they are mutually exclusive in semgrep, so --quiet had to go). Keeps --max-log-list-entries=0, which was previously a no-op under --quiet. - pre-commit hook: add `--max-log-list-entries=0` (it was --verbose without the flag, so it printed "<SKIPPED DATA>" instead of the full rule list). - CI: unchanged (already --verbose --max-log-list-entries=0). `--max-log-list-entries=0` disables truncation → shows ALL rule-ids/skipped files (verified empirically: =2 prints "<SKIPPED DATA>", =0 prints the full list). Intentional per-invocation differences remain: --baseline-commit (local mise scans everything; CI/pre-commit diff a baseline) and --sarif-output/--error (CI/pre-commit only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Aligned semgrep flags across the three invocations (
|
| Flag | mise security:semgrep |
CI security-scanners.yml |
pre-commit hook |
|---|---|---|---|
| verbosity | --quiet → --verbose |
--verbose (unchanged) |
--verbose (unchanged) |
--max-log-list-entries=0 |
✅ (was a no-op under --quiet) |
✅ (unchanged) | ❌ → ✅ added |
Net: all three now use --verbose --max-log-list-entries=0.
Notes / gotchas
--quietand--verboseare mutually exclusive in semgrep (it exits with an error), so adding--verboseto the mise task required dropping--quiet. Confirmed the swap changes neither exit code nor findings — only output volume.mise run security:semgrepis now verbose locally.- Under the old
--quiet, mise's--max-log-list-entries=0did nothing (quiet already suppresses those lists); it's now meaningful. - pre-commit was
--verbosewithout the flag, so with the ~3000-rule vendored config it printed<SKIPPED DATA>for the rule list instead of the full list — now consistent with CI.
Intentional differences that remain (by design, not inconsistencies)
--baseline-commit— mise runs a full local scan; CI ($BASELINE_SHAon PRs) and pre-commit (main) diff against a baseline so only new findings block.--sarif-output/--error— only CI and pre-commit emit SARIF / gate on findings; the mise task is interactive.
The [auto_exclude] source policy was removed earlier, leaving parse_block extracting a `source` value that main() unpacked and discarded (_source), plus a now-orphaned SOURCE_RE whose comment referenced the deleted policy. Drop SOURCE_RE and return a 3-tuple from parse_block — pure behavior- preserving refactor; generated files are unaffected (only update.py changes). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… orphaned state
Coordinate the tools/semgrep/ filenames under a rules-* prefix and simplify to
the files that actually carry state:
- r-all.active.yaml -> rules-vendored.yaml (what semgrep runs)
- exclusions.toml -> rules-status.toml (human decisions: excluded/active)
- update.py -> rules-update.py (the updater)
- rule-state.json -> DELETED (see below)
- EXCLUSIONS.md -> DELETED (see below)
Why the two deletions:
- EXCLUSIONS.md was a rendered join of the toml (decisions) + rule-state.json
(dates) + snapshot (descriptions). Its only non-derivable column was the
description gloss; the rest duplicated rules-status.toml. It's redundant with
the source files, so it's dropped.
- rule-state.json stored {version_id, updated} per rule. `updated` existed only
to fill EXCLUSIONS.md's rule-updated column; `version_id` existed only to
compute `updated`. With the doc gone, nothing consumes either field — so the
file has no reason to exist ("no version or updated" left). Deleted.
Decision model is now two stored statuses (excluded/active) in rules-status.toml
plus two DERIVED states reported each run for human action:
- new — a feed rule with no decision, not seen last run (triage it).
- orphaned — a rules-status.toml entry whose rule left the feed (prune or keep).
Both are derived statelessly from artifacts already in git (the prior
rules-vendored.yaml + rules-status.toml reconstruct "ids seen last run"), so no
state file is needed. Nothing is auto-deleted; the human decides.
rules-update.py loses the json/date/render_doc/render_table/sanitize machinery
(~130 lines) and only reads rules-status.toml, so hand-written TOML comments are
preserved. Verified: rules-vendored.yaml rule bodies are byte-identical to the
old r-all.active.yaml (3005 active / 126 excluded); regeneration is
deterministic; new/orphaned detection works; build green.
Updated all references: mise.toml (SEMGREP_RULES, security:semgrep,
semgrep:update), CI security-scanners.yml, .pre-commit-config.yaml (config path
+ exclude regex), .gitleaks.toml. .semgrepignore's tools/semgrep/*.yaml glob
still covers the renamed vendored file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ry scan gitleaks scans full git history. Renaming r-all.active.yaml -> rules-vendored.yaml left the old-named blobs in earlier branch commits (c8f5403, b189d87) no longer matched by the allowlist, so gitleaks flagged their embedded secret-detection regexes (slack-webhook-url, private-key) — failing both the standalone `gitleaks` check and the `security:gitleaks` step of `mise run build`. Allowlist BOTH the current and former filename so the branch history scans clean. (Post-squash-merge, main only ever has the new name, but CI scans the branch history, which still contains the old name.) Verified: gitleaks reports "no leaks found" over full history. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a short directory README so the vendored-ruleset setup is legible in the GitHub folder view without opening the files: what each file is (and which is human-edited vs generated), how to refresh it (`mise run semgrep:update`), and a glossary of the excluded/active/new/orphaned states. Deliberately carries no volatile figures (rule counts, ids) so it can't go stale; the rules-update.py docstring remains the authoritative design reference. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isadeks
left a comment
There was a problem hiding this comment.
Excellent, carefully-reasoned PR. Vendoring r/all with explicit-over-implicit exclusions, fail-loud regeneration, and byte-preserving determinism is the right design, and the security rationale (probe-rule removal + supply-chain pinning) is well-substantiated. Reviewed the script + all integration files against the final HEAD tree.
Correctness — verified
- Integration paths are internally consistent: all final files reference the renamed
rules-vendored.yaml/rules-status.toml/rules-update.py. The intermediate-commit names (r-all.active.yaml,update.py,exclusions.toml,EXCLUSIONS.md,rule-state.json) visible ingh pr diff --patchare all cleaned up before HEAD — the final tree has exactly 4 files with no dangling references. ✓ .gitleaks.tomlallowlists both the new and old filenames (deliberate, to cover branch history / #224). ✓- Byte-preserving split is sound: 3005
^-blocks = 3005 distinct ids; the 14 rules leading with- patterns:/- fix:correctly resolveid:via the later-line regex. ✓ .semgrepignorefilters scan targets not the--configfile, so the ruleset still loads while being excluded from self-match. ✓- HTTPS-only
OpenerDirectorgenuinely closes CWE-939 (noFileHandler/FTPHandler→ non-HTTPS has no dispatch handler), backed by an explicit scheme guard. Real fix, not a suppression. ✓ - Fail-loud aborts before the tracked file is written; output written only after all parsing succeeds;
new/orphanedderivation is stateless and bootstrap-safe. ✓
Suggestions (minor, non-blocking)
- Determinism has a latent gap on duplicate active ids. Sort key is id-only (
rb[0]) with a stable sort, so same-id blocks fall back to the registry's nondeterministic order. Harmless today (no active dups — the dups are all excluded). Tie-break on content to make the guarantee total:active_blocks.sort(key=lambda rb: (rb[0], rb[1])). - Split assumes rule content never begins a column-0
-line (e.g. inside amessage/fixblock scalar). Fail-loudrule_id_ofprevents silent corruption, so this is a resilience note — worth a one-line comment acknowledging the assumption. security:semgrepswitched--quiet→--verbose(intentional flag alignment; noting the local-output change).
Risks / tradeoffs
- +6 MB / 171k committed lines is the core RFC tradeoff (offline + pinning + supply-chain control vs. size). Deliberate; byte-preserve + sort-by-id keeps no-op regenerations at zero diff, which is the right mitigation.
- Maintenance is manual by design (
semgrep:update+ human triage of new/orphaned) — correct for a security ruleset, clearly documented. - No automated test for
rules-update.py; a small fixture test ofsplit_rules/rule_id_ofwould guard the parser against future regressions. Acceptable for a manually-run tool.
Verification is thorough (24/24 CI, validated against Semgrep 1.167.0, two-run determinism, real-vs-impersonator discrimination). Approving — suggestions are optional polish; only the duplicate-id tie-break is worth considering before the next r/all refresh.
🤖 Generated with Claude Code
Three minor, non-blocking review items: - rules-status.toml header named the old files (exclusions.toml, r-all.active.yaml, EXCLUSIONS.md, update.py). It's the file humans edit, so fix the comments to name the files that actually exist (rules-status.toml, rules-vendored.yaml, rules-update.py) and drop the removed EXCLUSIONS.md references — new/orphaned are reported on the console by rules-update.py. - Make the deterministic sort total: tie-break active blocks on their text (rb[0], rb[1]) so duplicate ids (r/all reuses some) also order stably instead of falling back to the download's nondeterministic order. Latent today (0 active dups — the dups are all excluded); byte-identical output verified, so no change to the vendored file now. - Document split_rules' assumption that rule content never begins a column-0 "- " line; if it ever did, the mis-split block fails rule_id_of and aborts loudly rather than silently corrupting the ruleset. (The reviewer's third note — security:semgrep --quiet -> --verbose — was the intentional flag alignment; no change needed.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
theagenticguy
left a comment
There was a problem hiding this comment.
Continued review — verified against the new head (8b2010b0), fresh checkout.
The review-nit commit addresses all four items, and I confirmed each against the actual tree rather than the commit message:
| Item | Status |
|---|---|
rules-status.toml header named renamed/deleted files (exclusions.toml, r-all.active.yaml, EXCLUSIONS.md, update.py) |
✅ Fixed — header now names rules-status.toml / rules-vendored.yaml / rules-update.py; EXCLUSIONS.md references dropped (new/orphaned are console-reported). |
| Determinism gap on duplicate active ids | ✅ Fixed — sort is now key=lambda rb: (rb[0], rb[1]), a total tie-break on block text. |
split_rules column-0 - assumption |
✅ Documented — docstring now states the assumption and that a mis-split aborts loudly via rule_id_of rather than corrupting silently. |
security:semgrep --quiet → --verbose |
✅ Correctly identified as intentional flag alignment; no change needed. |
Ground-truth re-checks at 8b2010b0:
rules-vendored.yamlis byte-identical to the prior head — the tie-break is a no-op today (0 duplicate active ids; the dups are all excluded), exactly as the commit claims.- Exclusions still fully honored: 101 excluded ids, 0 leak into the 3,005-rule vendored file; blocks remain id-sorted.
- No stale filename references survive in any active config or doc (the
r-all.active.yamlentry in.gitleaks.tomlis the intentional history allowlist). - 24/24 CI checks green.
Design and implementation are sound: explicit-over-implicit exclusions, byte-preserving + deterministic regeneration, fail-loud download, and a real CWE-939 remediation (HTTPS-only OpenerDirector) rather than a suppression. From a technical standpoint this is ready to merge.
Posting this as a review comment rather than a formal approval — the approving review here is a required-reviewer gate I'm deferring to @lalsaado to cast.
Reviewed by Bonk (Laith's collective agent teammate).
|
@isadeks thanks for the thorough review — all three suggestions are taken care of in
Also folded in the related nit: On your optional note about a |
Closes #211: vendor the Semgrep
r/allruleset locally so every scan (pre-commit, CI, mise) runs against a single pinned, offline-capable ruleset with exclusions baked in, instead of fetchingr/alllive on every run.Related
pre-commitfailures surfaced during this work are tracked separately in Pre-existingpre-commit run --all-filesfailures (tracking) #213 (sub-issues pre-commit: 12code_templates/*.pyfiles fail Python-parse hooks (check-ast, ruff-format) #214–pre-commit: bandit hook fails (exit 2) — missing .bandit-baseline.json #219); they are not introduced by this PR.Changes
New
tools/semgrep/(goals A/B/C):rules-vendored.yaml— pre-filtered rules; the only file semgrep loads. Script-generated byte-for-byte fromr/allminus excluded rules. Byte-preserving (removes whole rule blocks, never re-serializes YAML — re-serializing was verified to silently break rules) and deterministic (sorts rules by id, so a regeneration with no upstream change yields an empty diff).rules-status.toml— human-owned source of truth: explicit[rules."id"]decisions (status=excluded/active,pr,reason). Every exclusion is listed individually. The script only reads it, so hand-written#comments are preserved.rules-update.py— stdlib-only update mechanism. Downloadsr/all, drops excluded rules, rewritesrules-vendored.yaml, and reports derivednew/orphanedrules.Explicit exclusions (no implicit dropping):
rules-status.tomlwithstatus = "excluded". Nothing is dropped implicitly, so a rule appearing in a futurer/allcan never be silently excluded — it surfaces asnewfor triage.r/all(SSRF-via-metadata, stored-XSS inmessage/fix, command-injection/secret-exfil tests,PWNED-PRODUCTION-RULE, and a rule impersonating a real id). These all carrysource = https://semgrep.dev/r/None(no canonical registry source) in one contiguousr_idburst and never appear insemgrep/semgrep-rules. See RFC: Vendor Semgrep r/all rules locally with tracked exclusions #211 for the full provenance analysis. They are listed explicitly (rather than auto-excluded by source) so a legitimate rule that ever carried/r/Nonecould not be silently dropped.rules-update.pyreports it as anorphanedstate on the console — a human then prunes therules-status.tomlentry or keeps it. Nothing auto-deletes. (newandorphanedare derived statelessly from the priorrules-vendored.yaml+rules-status.toml; no state file is stored.)PR provenance (from git history): the exclusions carry their real originating PR — 21 from #11 (2026-02-07), 1 from #89 (OpenAI), 1 from #200 (
bbp-pattern-inject, a broken rule that made semgrep exit 2), and 78 from this PR (the remaining probe rules).Integration points:
mise.toml: addSEMGREP_RULESenv (baresemgrepuses vendored rules with zero flags) +semgrep:updatetask; drop the 22--exclude-ruleflags.security-scanners.yml) + pre-commit hook:--config=tools/semgrep/rules-vendored.yaml; drop exclude flags. Baseline/SARIF logic unchanged. All three invocations aligned on--verbose --max-log-list-entries=0(log verbosity only; see the flag-alignment comment below)..semgrepignore: skiptools/semgrep/*.yaml(rules would self-match)..gitleaks.toml: allowlist the active file (contains secret-detection regexes)..pre-commit-config.yaml: exclude the generated files from content/whitespace/large-file hooks so autofix hooks never corrupt the byte-for-byte ruleset.rules-update.py/rules-status.tomlstay in scope..semgrep.yaml(never loaded by Semgrep; misleading).Hardening: GHAS code-scanning findings in
rules-update.py(dynamic-urllib/CWE-939, string-concat-in-list) were remediated with real code changes — an HTTPS-onlyOpenerDirectorthat makesfile:///ftp://unreachable, and explicit string concatenation — not suppressions.Result
3005 active / 126 excluded blocks (101 distinct excluded ids: 21 PR#11, 1 PR#89, 1 PR#200, 78 PR#220; the remaining excluded blocks are duplicate ids the
r/allfeed reuses). The realsubprocess-shell-truerule survives while its/r/Noneimpersonator is dropped, showing the exclusion list discriminates valid from invalid.Verification
mise run buildpasses: lint (0 errors), fmt:check, validate, bandit ("No issues"), gitleaks (no leaks), checkov, grype. All CI checks green (24/24).rules-update.pyis deterministic: two consecutive runs produce byte-identical output.Merge
This branch is intended to be merged with "Squash and merge". The commit history is intentionally granular (it records the iterative design, including an
[auto_exclude]approach that was later replaced with explicit listing); squashing collapses it into a single clean commit onmain.Acknowledgment
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.
Generated with Claude Code