Commit 56429e4
feat(security): vendor Semgrep r/all ruleset with tracked exclusions (awslabs#220)
* feat(security): vendor Semgrep r/all ruleset with tracked exclusions
Implements RFC awslabs#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>
* chore(security): record PR#220 in Semgrep exclusions tracking
Replace the PR#TBD placeholders in exclusions.toml and the generated
EXCLUSIONS.md with this PR's number (awslabs#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>
* fix(security): exclude broken r/all rule bbp-pattern-inject
Rebased onto main, which added an exclusion for `bbp-pattern-inject`
(PR awslabs#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>
* feat(security): auto-exclude bug-bounty probe rules from r/all by source
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 awslabs#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>
* refactor(security): drop redundant dprint exclude for rule-state.json
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>
* refactor(security): emit dprint-aligned tables, drop last dprint exclude
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>
* fix(security): sort vendored rules by id for deterministic output
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>
* fix(security): remediate GHAS findings in update.py (no suppressions)
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>
* refactor(security): list all exclusions explicitly; fix PRs; dedup doc
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 awslabs#11 (2026-02-07), OpenAI was
PR awslabs#89, bbp-pattern-inject was PR awslabs#200. Only the 78 new probe exclusions
are PR awslabs#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>
* chore(security): align semgrep verbosity/log flags across all invocations
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>
* refactor(security): remove dead source parsing from update.py
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>
* refactor(security): rename to rules-* prefix; drop derived files; add 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>
* fix(security): allowlist former vendored-file name for gitleaks history 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>
* docs(security): add tools/semgrep/README.md
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>
* refactor(security): address PR review nits on vendored-semgrep tooling
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>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Sphia Sadek <isadeks@gmail.com>1 parent aca4fae commit 56429e4
10 files changed
Lines changed: 171852 additions & 71 deletions
File tree
- .github/workflows
- tools/semgrep
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
222 | 222 | | |
223 | 223 | | |
224 | 224 | | |
225 | | - | |
226 | | - | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
227 | 230 | | |
228 | | - | |
| 231 | + | |
| 232 | + | |
229 | 233 | | |
230 | | - | |
231 | | - | |
232 | | - | |
233 | | - | |
234 | | - | |
235 | | - | |
236 | | - | |
237 | | - | |
238 | | - | |
239 | | - | |
240 | | - | |
241 | | - | |
242 | | - | |
243 | | - | |
244 | | - | |
245 | | - | |
246 | | - | |
247 | | - | |
248 | | - | |
249 | | - | |
250 | | - | |
251 | | - | |
252 | 234 | | |
253 | 235 | | |
254 | 236 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
16 | 22 | | |
17 | 23 | | |
18 | 24 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
1 | 12 | | |
2 | 13 | | |
3 | 14 | | |
| |||
56 | 67 | | |
57 | 68 | | |
58 | 69 | | |
59 | | - | |
| 70 | + | |
60 | 71 | | |
61 | 72 | | |
62 | 73 | | |
| |||
This file was deleted.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
6 | 12 | | |
7 | 13 | | |
8 | 14 | | |
| |||
111 | 117 | | |
112 | 118 | | |
113 | 119 | | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
114 | 123 | | |
115 | | - | |
| 124 | + | |
116 | 125 | | |
117 | 126 | | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
118 | 134 | | |
119 | 135 | | |
120 | 136 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
0 commit comments