diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e7e6176..a8b28b0 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Security vulnerability - url: https://github.com/VantaInc/grantguard/security/advisories/new + url: https://github.com/OpenVanta/GrantGuard/security/advisories/new about: Please report security issues privately, not as public issues. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a240e6..68ea892 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,8 +21,6 @@ jobs: python-version: ["3.10", "3.14"] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cb0444..a7ce4f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Detectors now classify colon-syntax prefix wildcards (`Bash(cmd:*)`, the form + Claude Code itself writes for "always allow") the same as the equivalent + space form (`Bash(cmd *)`), and flag bare tool-wide wildcards such as + `Bash(*)` as overbroad instead of safe. ([#11]) +- Dead `VantaInc/grantguard` links now point to `OpenVanta/GrantGuard`: the + CONTRIBUTING clone command, this changelog's link references, the issue + chooser's security-advisory link, and the web UI's share buttons. + +[#11]: https://github.com/OpenVanta/GrantGuard/issues/11 + ## [0.1.0] - 2026-07-07 Initial release. @@ -35,5 +46,5 @@ Initial release. `Origin`, resisting DNS-rebinding and CSRF against the write endpoint, and caps request body size. -[Unreleased]: https://github.com/VantaInc/grantguard/compare/v0.1.0...HEAD -[0.1.0]: https://github.com/VantaInc/grantguard/releases/tag/v0.1.0 +[Unreleased]: https://github.com/OpenVanta/GrantGuard/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/OpenVanta/GrantGuard/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d764c85..2a7989b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,8 +19,8 @@ coverage are all very welcome. ## Local setup ```bash -git clone https://github.com/VantaInc/grantguard.git -cd grantguard +git clone https://github.com/OpenVanta/GrantGuard.git +cd GrantGuard uv run grantguard.py audit # CLI, dry-run uv run grantguard.py # local web UI ``` @@ -37,7 +37,7 @@ the code looking consistent: - **[tsgo](https://github.com/microsoft/typescript-go)** (`typescript@rc`) — the new Go-based TypeScript compiler. We use it in `--checkJs` mode to catch type errors in the JS without a compile step. -- **[oxfmt](https://github.com/nicolo-ribaudo/oxfmt)** — a fast JS/TS formatter +- **[oxfmt](https://oxc.rs/docs/guide/usage/formatter)** — a fast JS/TS formatter built on oxc. ```bash diff --git a/grantguard/core/detectors.py b/grantguard/core/detectors.py index 6e90e15..f4faee3 100644 --- a/grantguard/core/detectors.py +++ b/grantguard/core/detectors.py @@ -170,6 +170,8 @@ def masked_text(self) -> str: ) ] +# In Claude Code rules, `cmd:*` and `cmd *` are equivalent prefix wildcards, +# and the colon form is what "always allow" writes — so patterns accept both. DESTRUCTIVE_DETECTORS = [ PatternDetector( category=RiskCategory.DESTRUCTIVE, @@ -180,8 +182,8 @@ def masked_text(self) -> str: r"\brmdir\s+/s", # Windows recursive rmdir r"\bdel\s+/[a-z]", # Windows del /q /s r"Remove-Item\b.*-Recurse", # PowerShell - r"git\s+reset\s+\*", - r"git\s+rebase\s+\*", + r"git\s+reset[\s:]+\*", + r"git\s+rebase[\s:]+\*", r"\bkill\s+-9\b", r"\bpkill\b", r"\bxargs\s+kill", @@ -206,8 +208,9 @@ def masked_text(self) -> str: pattern=re.compile(pattern_source), ) for pattern_source in ( - r"^Bash\((?:sudo\s+)?[\w./\\-]+\s+\*\)$", # `tool *` - r"git\s+(?:clone|add|commit|fetch|merge|pull|checkout|ls-remote|rev-list|ls-tree)\s+\*", + r"^(?:\w{1,64}\(\*\)|\*)$", # `Tool(*)` / bare `*` + r"^Bash\((?:sudo\s+)?[\w./\\-]+[\s:]+\*\)$", # `tool *` / `tool:*` + r"git\s+(?:clone|add|commit|fetch|merge|pull|checkout|ls-remote|rev-list|ls-tree)[\s:]+\*", r"(?:npm|pip|pip3|npx|gh|cargo|brew|apt|yum|choco)\b.*\*", r"chmod\s+\+x", ) diff --git a/grantguard/web/app.js b/grantguard/web/app.js index 34e3667..7a665a6 100644 --- a/grantguard/web/app.js +++ b/grantguard/web/app.js @@ -52,7 +52,7 @@ const CHEVRON_EL = svgEl(svgIcon(``)); const X_SVG = ``; const LINKEDIN_SVG = ``; -const REPO_URL = "https://github.com/VantaInc/grantguard"; +const REPO_URL = "https://github.com/OpenVanta/GrantGuard"; // ── Global state ───────────────────────────────────────────────────────────── const state = { @@ -670,7 +670,7 @@ class GgShareModal extends HTMLElement { ta, h("div", { class: "modal-link" }, [ "Shares ", - h("code", { text: "github.com/VantaInc/grantguard" }), + h("code", { text: "github.com/OpenVanta/GrantGuard" }), ]), h("div", { class: "modal-actions" }, [ status, diff --git a/tests/test_detectors.py b/tests/test_detectors.py index 9761edf..345482a 100644 --- a/tests/test_detectors.py +++ b/tests/test_detectors.py @@ -33,6 +33,24 @@ def test_buckets(self): for text, expected in cases.items(): self.assertIs(C(text), expected, text) + def test_bare_tool_wildcards_are_overbroad(self): + for text in ("Bash(*)", "Skill(*)", "*"): + self.assertIs(C(text), RiskCategory.OVERBROAD, text) + + def test_colon_and_space_wildcard_syntax_classify_identically(self): + pairs = ( + ("Bash(nc *)", "Bash(nc:*)"), + ("Bash(sudo cp *)", "Bash(sudo cp:*)"), + ("Bash(git reset *)", "Bash(git reset:*)"), + ("Bash(git rebase *)", "Bash(git rebase:*)"), + ("Bash(git add *)", "Bash(git add:*)"), + ("Bash(git push *)", "Bash(git push:*)"), + ("Bash(npm install *)", "Bash(npm install:*)"), + ) + for space_form, colon_form in pairs: + self.assertIsNot(C(space_form), RiskCategory.SAFE, space_form) + self.assertIs(C(colon_form), C(space_form), colon_form) + def test_placeholder_not_flagged_as_secret(self): self.assertIsNot(C('Bash(curl -H "X-Api-Key: __TRACKED_VAR__" https://x.y)'), RiskCategory.SECRET)