Skip to content

Commit 56429e4

Browse files
scottschreckengaustclaudeisadeks
authored
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/security-scanners.yml

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -222,33 +222,15 @@ jobs:
222222
BASELINE_ARGS="--baseline-commit $BASELINE_SHA"
223223
fi
224224
225-
# Run semgrep, capture exit code
226-
# Rule exclusions documented in .semgrep.yaml
225+
# Run semgrep against the vendored ruleset (RFC #211).
226+
# Exclusions are baked into tools/semgrep/rules-vendored.yaml
227+
# (source of truth: tools/semgrep/rules-status.toml), so no
228+
# --exclude-rule flags are needed. --config is explicit because
229+
# CI does not inherit mise's SEMGREP_RULES env.
227230
set +e
228-
semgrep scan --oss-only --verbose --metrics=off --config=r/all \
231+
semgrep scan --oss-only --verbose --metrics=off \
232+
--config=tools/semgrep/rules-vendored.yaml \
229233
--max-log-list-entries=0 \
230-
--exclude-rule="bbp-pattern-inject" \
231-
--exclude-rule="ai.generic.detect-generic-ai-anthprop.detect-generic-ai-anthprop" \
232-
--exclude-rule="generic.secrets.security.detected-sonarqube-docs-api-key.detected-sonarqube-docs-api-key" \
233-
--exclude-rule="apex.lang.best-practice.ncino.accessmodifiers.globalaccessmodifiers.global-access-modifiers" \
234-
--exclude-rule="apex.lang.best-practice.ncino.urls.absoluteurls.absolute-urls" \
235-
--exclude-rule="apex.lang.security.ncino.dml.apexcsrfconstructor.apex-csrf-constructor" \
236-
--exclude-rule="apex.lang.security.ncino.dml.dmlnativestatements.dml-native-statements" \
237-
--exclude-rule="apex.lang.security.ncino.encryption.badcrypto.bad-crypto" \
238-
--exclude-rule="apex.lang.security.ncino.endpoints.insecurehttprequest.insecure-http-request" \
239-
--exclude-rule="apex.lang.security.ncino.endpoints.namedcredentialsconstantmatch.named-credentials-constant-match" \
240-
--exclude-rule="apex.lang.security.ncino.endpoints.namedcredentialsstringmatch.named-credentials-string-match" \
241-
--exclude-rule="apex.lang.security.ncino.injection.apexsoqlinjectionfromunescapedurlparam.soql-injection-unescaped-url-param" \
242-
--exclude-rule="apex.lang.security.ncino.injection.apexsoqlinjectionunescapedparam.soql-injection-unescaped-param" \
243-
--exclude-rule="apex.lang.security.ncino.sharing.specifysharinglevel.specify-sharing-level" \
244-
--exclude-rule="apex.lang.security.ncino.system.systemdebug.system-debug" \
245-
--exclude-rule="elixir.lang.best-practice.deprecated-bnot-operator.deprecated_bnot_operator" \
246-
--exclude-rule="elixir.lang.best-practice.deprecated-bxor-operator.deprecated_bxor_operator" \
247-
--exclude-rule="elixir.lang.best-practice.deprecated-calendar-iso-day-of-week-3.deprecated_calendar_iso_day_of_week_3" \
248-
--exclude-rule="elixir.lang.best-practice.deprecated-use-bitwise.deprecated_use_bitwise" \
249-
--exclude-rule="elixir.lang.best-practice.enum-map-into.enum_map_into" \
250-
--exclude-rule="elixir.lang.best-practice.enum-map-join.enum_map_join" \
251-
--exclude-rule="elixir.lang.correctness.atom-exhaustion.atom_exhaustion" \
252234
--sarif-output semgrep-report_sarif.json $BASELINE_ARGS
253235
SEMGREP_EXIT=$?
254236
set -e

.gitleaks.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ description = "Global allowlist"
1313
paths = [
1414
'''\.gitleaksignore$''',
1515
'''\.gitleaks-baseline\.json$''',
16+
# Vendored Semgrep ruleset (RFC #211): contains secret-detection regexes
17+
# that gitleaks would false-positive on. Generated by tools/semgrep/rules-update.py.
18+
# gitleaks scans full history, so we allowlist BOTH the current filename and
19+
# the former name (r-all.active.yaml) it carried in earlier branch commits.
20+
'''tools/semgrep/rules-vendored\.yaml$''',
21+
'''tools/semgrep/r-all\.active\.yaml$''',
1622
]
1723

1824
# Example: Add custom rules or override defaults

.pre-commit-config.yaml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# The generated, byte-for-byte vendored Semgrep ruleset (RFC #211) is produced
2+
# by tools/semgrep/rules-update.py and must stay identical to the upstream
3+
# download. It necessarily contains detection patterns (private-key markers,
4+
# example secrets, non-permalink URLs) and exceeds size limits, so ALL content
5+
# hooks must skip it; any autofix hook would corrupt the ruleset. The
6+
# hand-authored rules-update.py and rules-status.toml stay in scope on purpose.
7+
exclude: |
8+
(?x)^(
9+
tools/semgrep/rules-vendored\.yaml
10+
)$
11+
112
repos:
213
- repo: https://github.com/pre-commit/pre-commit-hooks
314
rev: v6.0.0
@@ -56,7 +67,7 @@ repos:
5667
rev: v1.151.0
5768
hooks:
5869
- id: semgrep
59-
args: [ '--oss-only', '--verbose', '--metrics=off', '--baseline-commit', 'main', '--config=r/all', '--sarif-output', '.tmp/semgrep.sarif.json', '--error' ]
70+
args: [ '--oss-only', '--verbose', '--metrics=off', '--baseline-commit', 'main', '--config=tools/semgrep/rules-vendored.yaml', '--max-log-list-entries=0', '--sarif-output', '.tmp/semgrep.sarif.json', '--error' ]
6071
stages: [pre-push]
6172
- repo: https://github.com/bridgecrewio/checkov
6273
rev: 3.2.500

.semgrep.yaml

Lines changed: 0 additions & 44 deletions
This file was deleted.

.semgrepignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ uv.lock
1919
.gitleaks-baseline.json
2020
.bandit-baseline.json
2121
.semgrepignore
22+
23+
# Vendored Semgrep ruleset (RFC #211) — Semgrep must not scan its own rule
24+
# files (they contain detection regexes that would self-match).
25+
tools/semgrep/*.yaml

mise.toml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
min_version = "2026.2.4"
55

6+
[env]
7+
# Vendored Semgrep ruleset (RFC #211). Used by any bare `semgrep` call in a mise
8+
# context because Semgrep reads SEMGREP_RULES whenever --config is absent. See
9+
# tools/semgrep/ and `mise run semgrep:update`.
10+
SEMGREP_RULES = "tools/semgrep/rules-vendored.yaml"
11+
612
[tools]
713
uv = "0.10"
814
node = "24"
@@ -111,10 +117,20 @@ run = [
111117

112118
[tasks."security:semgrep"]
113119
description = "Run SemGrepOSS"
120+
# Uses the vendored ruleset (RFC #211): exclusions are baked into the vendored
121+
# file, so no --exclude-rule flags. --config is explicit here for parity with
122+
# CI/pre-commit; SEMGREP_RULES ([env]) covers bare `semgrep` calls too.
114123
run = [
115-
"semgrep scan --quiet --oss-only --metrics=off --config=r/all --max-log-list-entries=0 --exclude-rule='ai.generic.detect-generic-ai-anthprop.detect-generic-ai-anthprop' --exclude-rule='generic.secrets.security.detected-sonarqube-docs-api-key.detected-sonarqube-docs-api-key' --exclude-rule='apex.lang.best-practice.ncino.accessmodifiers.globalaccessmodifiers.global-access-modifiers' --exclude-rule='apex.lang.best-practice.ncino.urls.absoluteurls.absolute-urls' --exclude-rule='apex.lang.security.ncino.dml.apexcsrfconstructor.apex-csrf-constructor' --exclude-rule='apex.lang.security.ncino.dml.dmlnativestatements.dml-native-statements' --exclude-rule='apex.lang.security.ncino.encryption.badcrypto.bad-crypto' --exclude-rule='apex.lang.security.ncino.endpoints.insecurehttprequest.insecure-http-request' --exclude-rule='apex.lang.security.ncino.endpoints.namedcredentialsconstantmatch.named-credentials-constant-match' --exclude-rule='apex.lang.security.ncino.endpoints.namedcredentialsstringmatch.named-credentials-string-match' --exclude-rule='apex.lang.security.ncino.injection.apexsoqlinjectionfromunescapedurlparam.soql-injection-unescaped-url-param' --exclude-rule='apex.lang.security.ncino.injection.apexsoqlinjectionunescapedparam.soql-injection-unescaped-param' --exclude-rule='apex.lang.security.ncino.sharing.specifysharinglevel.specify-sharing-level' --exclude-rule='apex.lang.security.ncino.system.systemdebug.system-debug' --exclude-rule='elixir.lang.best-practice.deprecated-bnot-operator.deprecated_bnot_operator' --exclude-rule='elixir.lang.best-practice.deprecated-bxor-operator.deprecated_bxor_operator' --exclude-rule='elixir.lang.best-practice.deprecated-calendar-iso-day-of-week-3.deprecated_calendar_iso_day_of_week_3' --exclude-rule='elixir.lang.best-practice.deprecated-use-bitwise.deprecated_use_bitwise' --exclude-rule='elixir.lang.best-practice.enum-map-into.enum_map_into' --exclude-rule='elixir.lang.best-practice.enum-map-join.enum_map_join' --exclude-rule='elixir.lang.correctness.atom-exhaustion.atom_exhaustion' --exclude-rule='ai.generic.detect-generic-ai-oai.detect-generic-ai-oai'"
124+
"semgrep scan --oss-only --verbose --metrics=off --config=tools/semgrep/rules-vendored.yaml --max-log-list-entries=0"
116125
]
117126

127+
[tasks."semgrep:update"]
128+
description = "Refresh the vendored Semgrep ruleset (RFC #211)"
129+
# Re-downloads r/all and rebuilds rules-vendored.yaml (dropping rules marked
130+
# excluded in rules-status.toml); reports new/orphaned rules. Run manually;
131+
# then re-run security:semgrep.
132+
run = "uv run tools/semgrep/rules-update.py"
133+
118134
[tasks."security:checkov"]
119135
description = "Run Checkov"
120136
run = [

tools/semgrep/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Vendored Semgrep ruleset
2+
3+
This directory pins the Semgrep [`r/all`](https://semgrep.dev/c/r/all)
4+
community ruleset locally (RFC #211), so every scan (pre-commit, GitHub
5+
Actions, `mise`) runs the same reviewed, offline ruleset instead of fetching
6+
`r/all` live.
7+
8+
| File | What it is |
9+
| --------------------- | ----------------------------------------------------------------------------------------- |
10+
| `rules-vendored.yaml` | Generated; DO NOT EDIT. The only ruleset Semgrep loads — `r/all` minus excluded rules. |
11+
| `rules-status.toml` | Humans edit the "state" with a "reason" at any time based on the table below. |
12+
| `rules-update.py` | Refreshes `rules-vendored.yaml` from `r/all`; run manually via `mise run semgrep:update`. |
13+
14+
## Updating
15+
16+
```bash
17+
mise run semgrep:update # re-download r/all, rebuild rules-vendored.yaml
18+
mise run security:semgrep # verify the result scans clean
19+
```
20+
21+
The updater reports rules in these states:
22+
23+
| State | Meaning |
24+
| ---------- | -------------------------------------------------------------------- |
25+
| `excluded` | Removed from the vendored ruleset by a `rules-status.toml` entry. |
26+
| `active` | Kept, with the decision explicitly recorded in `rules-status.toml`. |
27+
| `new` | Appeared upstream with no `rules-status.toml` entry — triage it. |
28+
| `orphaned` | A `rules-status.toml` entry whose rule left `r/all` — prune or keep. |
29+
30+
See the `rules-update.py` module docstring for the full design.

0 commit comments

Comments
 (0)