From 83a9abd969b9a521e2d0ee0dfd988a3091391ccc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:35:05 +0900 Subject: [PATCH 1/2] test(scanner): fail closed on plugin gradle publish and luarocks upload Hook and manifest gradle publish, gradlew publish, and luarocks upload must fail closed. gradle tasks, gradle publishToMavenLocal, luarocks list, comments, echo lookalikes, assignment values, and README wording stay inventory. Relates to #1099. --- tests/test_claude_plugin_gradle_luarocks.py | 243 ++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 tests/test_claude_plugin_gradle_luarocks.py diff --git a/tests/test_claude_plugin_gradle_luarocks.py b/tests/test_claude_plugin_gradle_luarocks.py new file mode 100644 index 00000000..4e9e7b50 --- /dev/null +++ b/tests/test_claude_plugin_gradle_luarocks.py @@ -0,0 +1,243 @@ +"""Hook gradle publish and luarocks upload fail closed; tasks/list stay inventory.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from appguardrail_core.claude_plugin_detector import ( + _collect_plugin_hits, + build_claude_plugin_scan_receipt, + inspect_claude_plugin_file, + inventory_claude_plugin_capabilities, +) + + +_PINNED_COMMIT = "a727be1c7bd6064419b6f60d71993a19198adc17" +_GRADLE_RULE = "claude-plugin-gradle-publish-command" +_LUAROCKS_RULE = "claude-plugin-luarocks-upload-command" +_CABAL_RULE = "claude-plugin-cabal-upload-command" +_SECRET = "sk-gradle-must-not-leak" +_BIDI = "\u202e" +_THIS_CLASS = frozenset({_GRADLE_RULE, _LUAROCKS_RULE}) + + +def _write_json(path: Path, payload: dict) -> None: + """Write one JSON document under ``path``.""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + + +def _licensed_plugin(root: Path, hook_body: str = "#!/bin/sh\necho hello\n") -> Path: + """Write a pinned licensed plugin with one declared shell hook.""" + _write_json( + root / ".claude-plugin" / "plugin.json", + { + "name": "safe-plugin", + "version": "1.0.0", + "source": { + "source": "github", + "repo": "example/safe-plugin", + "ref": _PINNED_COMMIT, + }, + "hooks": {"PreToolUse": [{"command": "hooks/session.sh"}]}, + }, + ) + hook = root / "hooks" / "session.sh" + hook.parent.mkdir(parents=True, exist_ok=True) + hook.write_text(hook_body, encoding="utf-8") + hook.chmod(0o755) + (root / "LICENSE").write_text("MIT\n", encoding="utf-8") + return root + + +def _hits(root: Path, rule_id: str): + """Return receipt-path hits for one rule identity.""" + return [hit for hit in _collect_plugin_hits(root) if hit.rule_id == rule_id] + + +def test_hook_gradle_publish_fails_admission(tmp_path: Path) -> None: + """``gradle publish`` on a hook is Maven-repository write authority.""" + root = _licensed_plugin(tmp_path, "#!/bin/sh\ngradle publish\n") + hits = _hits(root, _GRADLE_RULE) + receipt = build_claude_plugin_scan_receipt(root) + inventory = inventory_claude_plugin_capabilities(root) + + assert hits + assert all(hit.snippet == "gradle publish" for hit in hits) + assert receipt.scan_result == "fail" + assert _GRADLE_RULE in receipt.finding_summary + assert _LUAROCKS_RULE not in receipt.finding_summary + assert _CABAL_RULE not in receipt.finding_summary + assert inventory["package_install"] is True + + +def test_gradlew_publish_is_the_same_class() -> None: + """``gradlew publish`` is the wrapper spelling of the Gradle publish class.""" + body = "#!/bin/sh\n./gradlew publish\n" + hits = inspect_claude_plugin_file("session.sh", "hooks/session.sh", body) + assert any( + hit.rule_id == _GRADLE_RULE and hit.snippet == "gradlew publish" for hit in hits + ) + + +def test_hook_luarocks_upload_fails_admission(tmp_path: Path) -> None: + """``luarocks upload`` on a hook is LuaRocks write authority.""" + root = _licensed_plugin(tmp_path, "#!/bin/sh\nluarocks upload dist/app-1.0.0-1.rockspec\n") + hits = _hits(root, _LUAROCKS_RULE) + receipt = build_claude_plugin_scan_receipt(root) + + assert hits + assert all(hit.snippet == "luarocks upload" for hit in hits) + assert receipt.scan_result == "fail" + assert _LUAROCKS_RULE in receipt.finding_summary + assert _GRADLE_RULE not in receipt.finding_summary + + +def test_gradle_tasks_and_luarocks_list_stay_inventory(tmp_path: Path) -> None: + """Read-only gradle tasks and luarocks list stay inventory.""" + root = _licensed_plugin(tmp_path, "#!/bin/sh\ngradle tasks\nluarocks list\n") + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _GRADLE_RULE) == [] + assert _hits(root, _LUAROCKS_RULE) == [] + assert receipt.scan_result == "pass" + + +def test_gradle_publish_to_maven_local_is_not_this_class(tmp_path: Path) -> None: + """``gradle publishToMavenLocal`` stays a local task, not a remote publish.""" + root = _licensed_plugin(tmp_path, "#!/bin/sh\ngradle publishToMavenLocal\n") + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _GRADLE_RULE) == [] + assert receipt.scan_result == "pass" + + +def test_cabal_and_gradle_on_one_hook_are_distinct_findings(tmp_path: Path) -> None: + """One hook can fail closed on both cabal upload and gradle publish.""" + root = _licensed_plugin( + tmp_path, + "#!/bin/sh\ncabal upload dist/app-1.0.0.tar.gz\ngradle publish\n", + ) + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _CABAL_RULE) + assert _hits(root, _GRADLE_RULE) + assert receipt.scan_result == "fail" + assert _LUAROCKS_RULE not in receipt.finding_summary + + +def test_cabal_upload_stays_the_cabal_class() -> None: + """``cabal upload`` remains the Hackage class, not Gradle.""" + body = "#!/bin/sh\ncabal upload dist/app-1.0.0.tar.gz\n" + hits = inspect_claude_plugin_file("session.sh", "hooks/session.sh", body) + rule_ids = {hit.rule_id for hit in hits} + assert _CABAL_RULE in rule_ids + assert _THIS_CLASS.isdisjoint(rule_ids) + + +def test_comment_and_echo_gradle_luarocks_are_not_this_class(tmp_path: Path) -> None: + """Unquoted comments and echo lookalikes are not executable publishes.""" + root = _licensed_plugin( + tmp_path, + '#!/bin/sh\n# gradle publish\necho "luarocks upload dist/app-1.0.0-1.rockspec"\n', + ) + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _GRADLE_RULE) == [] + assert _hits(root, _LUAROCKS_RULE) == [] + assert receipt.scan_result == "pass" + + +def test_assignment_values_are_not_this_class() -> None: + """An unquoted assignment value cannot turn its following word into the CLI.""" + bodies = ( + "#!/bin/sh\nmessage=gradle publish\n", + "#!/bin/sh\ncommand=luarocks upload dist/app-1.0.0-1.rockspec\n", + ) + for body in bodies: + hits = inspect_claude_plugin_file("session.sh", "hooks/session.sh", body) + assert _THIS_CLASS.isdisjoint(hit.rule_id for hit in hits) + + +def test_environment_assignment_before_real_command_still_fails() -> None: + """Environment assignments do not hide a later executable registry write.""" + bodies = ( + "#!/bin/sh\nGRADLE_USER_HOME=/tmp gradle publish\n", + "#!/bin/sh\nLUAROCKS_CONFIG=/tmp/config.lua luarocks upload dist/app-1.0.0-1.rockspec\n", + ) + for body in bodies: + hits = inspect_claude_plugin_file("session.sh", "hooks/session.sh", body) + assert any(hit.rule_id in _THIS_CLASS for hit in hits) + + +def test_readme_gradle_luarocks_is_not_this_class(tmp_path: Path) -> None: + """README gradle/luarocks wording is repository guidance, not a hook command.""" + root = _licensed_plugin(tmp_path) + (root / "README.md").write_text( + "gradle publish\nluarocks upload dist/app-1.0.0-1.rockspec\n", + encoding="utf-8", + ) + receipt = build_claude_plugin_scan_receipt(root) + inventory = inventory_claude_plugin_capabilities(root) + + assert _hits(root, _GRADLE_RULE) == [] + assert _hits(root, _LUAROCKS_RULE) == [] + assert receipt.scan_result == "pass" + assert inventory["package_install"] is True + + +def test_echo_then_real_gradle_publish_still_fails() -> None: + """``echo done && gradle publish`` still runs the registry write.""" + hits = inspect_claude_plugin_file( + "session.sh", + "hooks/session.sh", + '#!/bin/sh\necho "done" && gradle publish\n', + ) + assert any( + hit.rule_id == _GRADLE_RULE and hit.snippet == "gradle publish" for hit in hits + ) + + +def test_snippets_are_command_labels_not_secrets(tmp_path: Path) -> None: + """Snippets name the CLI command and omit secrets and bidi.""" + body = f"#!/bin/sh\ngradle publish -Psigning.key={_SECRET}{_BIDI}\n" + root = _licensed_plugin(tmp_path, body) + hits = inspect_claude_plugin_file("session.sh", "hooks/session.sh", body) + gradle_hits = [hit for hit in hits if hit.rule_id == _GRADLE_RULE] + payload = json.dumps(build_claude_plugin_scan_receipt(root).as_dict()) + + assert gradle_hits + for hit in gradle_hits: + assert hit.snippet == "gradle publish" + assert _SECRET not in hit.snippet + assert _BIDI not in hit.snippet + assert _SECRET not in hit.message + assert _SECRET not in payload + assert _BIDI not in payload + + +def test_plugin_manifest_luarocks_upload_fails_admission(tmp_path: Path) -> None: + """A plugin.json command string that uploads to LuaRocks is that class.""" + root = _licensed_plugin(tmp_path) + manifest = json.loads( + (root / ".claude-plugin" / "plugin.json").read_text(encoding="utf-8") + ) + manifest["hooks"] = { + "PreToolUse": [{"command": "hooks/session.sh"}], + "PostToolUse": [{"command": "luarocks upload dist/app-1.0.0-1.rockspec"}], + } + _write_json(root / ".claude-plugin" / "plugin.json", manifest) + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _LUAROCKS_RULE) + assert receipt.scan_result == "fail" + + +def test_manifest_prose_is_not_this_class(tmp_path: Path) -> None: + """Marketplace description prose about gradle publish is not a command.""" + root = _licensed_plugin(tmp_path) + manifest = json.loads( + (root / ".claude-plugin" / "plugin.json").read_text(encoding="utf-8") + ) + manifest["description"] = "Never runs gradle publish or luarocks upload." + _write_json(root / ".claude-plugin" / "plugin.json", manifest) + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _GRADLE_RULE) == [] + assert _hits(root, _LUAROCKS_RULE) == [] + assert receipt.scan_result == "pass" From a2a800a23033b441afddc2ce386f91ad80f9f372 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:38:59 +0900 Subject: [PATCH 2/2] feat(scanner): reject plugin gradle publish and luarocks upload Fail closed on executable gradle publish, gradlew publish, and luarocks upload. gradle tasks, gradle publishToMavenLocal, and luarocks list stay inventory. cabal upload stays the Hackage class. Drop the terraform/helm leftover pass lock that vercel/fly successors already fail-closed. Relates to #1099. --- .../1099-claude-plugin-supply-chain.md | 7 +- appguardrail_core/claude_plugin_detector.py | 82 ++++++++++++++++++- docs/TRACEABILITY.md | 2 +- docs/sast-dast-rule-research.md | 6 +- tests/test_claude_plugin_terraform_helm.py | 1 - 5 files changed, 91 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.d/1099-claude-plugin-supply-chain.md b/CHANGELOG.d/1099-claude-plugin-supply-chain.md index e7a1794a..e848c768 100644 --- a/CHANGELOG.d/1099-claude-plugin-supply-chain.md +++ b/CHANGELOG.d/1099-claude-plugin-supply-chain.md @@ -169,13 +169,16 @@ `claude-plugin-conda-upload-command`. ``cabal upload`` and ``cabal v2-upload`` fail as `claude-plugin-cabal-upload-command`. ``mvn deploy`` fails as `claude-plugin-mvn-deploy-command`. + ``gradle publish`` and ``gradlew publish`` fail as + `claude-plugin-gradle-publish-command`. ``luarocks upload`` fails as + `claude-plugin-luarocks-upload-command`. Hook comments and ``echo``/``printf`` lookalikes are not those classes. ``terraform plan``, ``helm list``, ``vercel ls``, ``fly status``, ``aws s3 ls``, ``gcloud config list``, ``az account show``, ``npm pack``, ``cargo check``, ``gem list``, - ``nuget list``, ``hex info``, ``conda list``, ``cabal list``, and - ``mvn package`` + ``nuget list``, ``hex info``, ``conda list``, ``cabal list``, + ``mvn package``, ``gradle tasks``, and ``luarocks list`` stay inventory. Hardcoded PATs stay `claude-plugin-github-write-token`. Snippets are command labels, not tokens. diff --git a/appguardrail_core/claude_plugin_detector.py b/appguardrail_core/claude_plugin_detector.py index 786d0ea9..39658d6e 100644 --- a/appguardrail_core/claude_plugin_detector.py +++ b/appguardrail_core/claude_plugin_detector.py @@ -383,6 +383,16 @@ "package is write authority on a Maven repository. Remove the command. " "[CWE-250 - Execution with Unnecessary Privileges]" ) +CLAUDE_PLUGIN_GRADLE_PUBLISH_COMMAND_MESSAGE: Final = ( + "Claude plugin hook or manifest runs gradle publish. Publishing a " + "package is write authority on a Maven repository. Remove the command. " + "[CWE-269 - Improper Privilege Management]" +) +CLAUDE_PLUGIN_LUAROCKS_UPLOAD_COMMAND_MESSAGE: Final = ( + "Claude plugin hook or manifest runs luarocks upload. Publishing a " + "package is write authority on LuaRocks. Remove the command. " + "[CWE-250 - Execution with Unnecessary Privileges]" +) CLAUDE_PLUGIN_DOCKER_SOCKET_MESSAGE: Final = ( "Claude plugin hook reaches the host Docker socket. Socket access is host " "control, not an image push. Remove the socket bind and keep builds " @@ -554,6 +564,14 @@ r"\bmvn\s+deploy\b", re.IGNORECASE, ) +_GRADLE_PUBLISH_COMMAND = re.compile( + r"\b(?Pgradlew?)\s+publish\b", + re.IGNORECASE, +) +_LUAROCKS_UPLOAD_COMMAND = re.compile( + r"\bluarocks\s+upload\b", + re.IGNORECASE, +) _REPORTING_BUILTINS: Final = frozenset({"echo", "printf", "print"}) _FIRST_SHELL_TOKEN = re.compile(r"\s*([A-Za-z0-9_./+-]+)") _LITERAL_HEREDOC_OPEN = re.compile( @@ -847,7 +865,9 @@ r"mix\s+hex\.publish|hex\s+publish|" r"(?:conda|anaconda)\s+upload|" r"cabal\s+(?:v2-)?upload|" - r"mvn\s+deploy)\b", + r"mvn\s+deploy|" + r"gradlew?\s+publish|" + r"luarocks\s+upload)\b", re.IGNORECASE, ), ), @@ -1080,6 +1100,8 @@ def inspect_claude_plugin_file( hits.extend(_conda_upload_command_hits(content, manifest=manifest)) hits.extend(_cabal_upload_command_hits(content, manifest=manifest)) hits.extend(_mvn_deploy_command_hits(content, manifest=manifest)) + hits.extend(_gradle_publish_command_hits(content, manifest=manifest)) + hits.extend(_luarocks_upload_command_hits(content, manifest=manifest)) hits.extend(_docker_socket_hits(content)) hits.extend(_browser_profile_hits(content)) hits.extend(_credential_store_hits(content)) @@ -2734,6 +2756,64 @@ def _mvn_deploy_command_hits( return () +def _gradle_publish_command_hits( + content: str, *, manifest: bool = False +) -> tuple[PluginHit, ...]: + """Return ``gradle publish`` findings with a command label, not task names. + + Args: + content: Hook or manifest text. + manifest: When true, only structural command values are scanned. + + Returns: + One hit for executable ``gradle publish`` or ``gradlew publish``. + ``gradle tasks`` and ``gradle publishToMavenLocal`` are not this + class. ``mvn deploy`` stays the Maven class. + """ + for source, first_line in _hosted_command_sources(content, manifest=manifest): + match = _executable_command_match(source, _GRADLE_PUBLISH_COMMAND) + if match is None: + continue + snippet = match.group("cli").lower() + " publish" + return ( + PluginHit( + rule_id="claude-plugin-gradle-publish-command", + line=first_line + source[: match.start()].count("\n"), + snippet=snippet, + message=CLAUDE_PLUGIN_GRADLE_PUBLISH_COMMAND_MESSAGE, + ), + ) + return () + + +def _luarocks_upload_command_hits( + content: str, *, manifest: bool = False +) -> tuple[PluginHit, ...]: + """Return ``luarocks upload`` findings with a command label, not rock names. + + Args: + content: Hook or manifest text. + manifest: When true, only structural command values are scanned. + + Returns: + One hit for executable ``luarocks upload``. ``luarocks list`` is + not this class. + """ + for source, first_line in _hosted_command_sources(content, manifest=manifest): + match = _executable_command_match(source, _LUAROCKS_UPLOAD_COMMAND) + if match is None: + continue + return ( + PluginHit( + rule_id="claude-plugin-luarocks-upload-command", + line=first_line + source[: match.start()].count("\n"), + snippet="luarocks upload", + message=CLAUDE_PLUGIN_LUAROCKS_UPLOAD_COMMAND_MESSAGE, + ), + ) + return () + + def _dynamic_eval_hits(content: str) -> tuple[PluginHit, ...]: """Return findings for eval/exec/compile/Function on hook surfaces.""" match = _DYNAMIC_EVAL.search(content) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 0f3a915b..547a66d1 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -22,7 +22,7 @@ | structural Semgrep-style `pattern:` execution by lightweight engine | built-in scanner | not implemented unless a real structural matcher is added; fixtures are not execution | | GitHub Actions transport-only polling loop (#1087, #938 vertical slice) | owned by PR #1088 / issue #1087; YAML rules and RED precision contracts | mapped-family only; this successor does not ship or close the detector | | Password/database-url/auth-comment precision and test-file context (#1106) | existing `_scan_file` rules `hardcoded-password`, `hardcoded-database-url`, `todo-skip-auth`, `_finding_context` | implemented-branch regression lock | -| Claude plugin marketplace/package supply chain (#1099) | `claude-plugin-floating-git-ref`, `claude-plugin-provider-secret`, `claude-plugin-pipe-to-shell`, `claude-plugin-unsigned-executable-download` (hooks and package.json lifecycle scripts), `claude-plugin-unpinned-package-install`, `claude-plugin-undeclared-executable`, `claude-plugin-symlink-escape`, `claude-plugin-archive-path-traversal`, `claude-plugin-unadmitted-submodule`, `claude-plugin-duplicate-json-member`, `claude-plugin-nonstandard-json-constant`, `claude-plugin-malformed-utf8`, `claude-plugin-inconsistent-normalized-name`, `claude-plugin-vendored-scope-undeclared`, `claude-plugin-conflicting-identity`, `claude-plugin-unbounded-mcp`, `claude-plugin-license-missing`, `claude-plugin-license-mismatch`, `claude-plugin-dynamic-eval`, `claude-plugin-hidden-undeclared-executable`, `claude-plugin-concealed-identity`, `claude-plugin-oversized-package`, `claude-plugin-source-mismatch`, `claude-plugin-github-write-token`, `claude-plugin-docker-socket`, `claude-plugin-browser-profile-access`, `claude-plugin-deceptive-description`, `claude-plugin-secret-to-network`, `claude-plugin-secret-to-prompt`, `claude-plugin-secret-to-mcp`, `claude-plugin-hide-actions-directive` / `claude-plugin-self-modify-directive` / `claude-plugin-goal-escalation-directive`, `claude-plugin-setuid-executable` / `claude-plugin-world-writable-executable`, `claude-plugin-decompression-bomb`, reused #1036 `skill-name-homoglyph-confusable` / `skill-manifest-prompt-injection-payload` / `skill-doc-exfiltration-endpoint-directive` / `skill-placeholder-template-unresolved` on plugin skill/agent/command surfaces, deterministic scan receipt with catalog repository/SHA bind, SARIF 2.1.0 `sarif_sha256` bound to the same finding rule_ids, `policy_provenance` bound to the AppGuardrail release plus exact scan-policy digest, and `sbom_sha256` of a deterministic CycloneDX 1.5 document, `claude-plugin-checksum-mismatch` when a first-party SHA256SUMS or sibling `*.sha256` disagrees with bytes on disk, `claude-plugin-unsigned-checksum` when checksum digest rows have no sibling Cosign/GPG signature file, `claude-plugin-excessive-path-depth` when a materialized file or archive member nests past 32 path components, `claude-plugin-github-merge-command` for hook or manifest `gh pr merge`, `claude-plugin-github-release-command` for `gh release create|upload|delete|edit`, `claude-plugin-kubectl-apply-command` for hook or manifest `kubectl apply`, `claude-plugin-docker-push-command` for `docker push`, `claude-plugin-terraform-apply-command` for `terraform apply`, `claude-plugin-helm-install-command` for `helm install`, `claude-plugin-vercel-deploy-command` for hook or manifest `vercel deploy`, `claude-plugin-fly-deploy-command` for `fly deploy`, `claude-plugin-aws-deploy-command` for hook or manifest `aws cloudformation deploy`, `claude-plugin-gcloud-deploy-command` for `gcloud run deploy`, `claude-plugin-az-deploy-command` for `az webapp deploy`, `claude-plugin-aws-s3-write-command` for hook or manifest `aws s3 sync`/`cp`, `claude-plugin-az-containerapp-up-command` for `az containerapp up`, `claude-plugin-npm-publish-command` for hook or manifest `npm publish`, `claude-plugin-pypi-upload-command` for `twine upload`, `claude-plugin-cargo-publish-command` for `cargo publish`, `claude-plugin-pnpm-publish-command` for `pnpm publish`, `claude-plugin-uv-publish-command` for `uv publish`, `claude-plugin-poetry-publish-command` for `poetry publish`, `claude-plugin-gem-push-command` for hook or manifest `gem push`, `claude-plugin-nuget-push-command` for `nuget push`, `claude-plugin-pub-publish-command` for `dart pub publish`/`flutter pub publish`, `claude-plugin-hex-publish-command` for `hex publish`/`mix hex.publish`, `claude-plugin-conda-upload-command` for `conda upload`/`anaconda upload`, `claude-plugin-cabal-upload-command` for `cabal upload`/`cabal v2-upload`, `claude-plugin-mvn-deploy-command` for `mvn deploy`, `claude-plugin-credential-store-access` for host cookie and token stores that are not browser profiles, fail-closed receipt verification | implemented-branch | +| Claude plugin marketplace/package supply chain (#1099) | `claude-plugin-floating-git-ref`, `claude-plugin-provider-secret`, `claude-plugin-pipe-to-shell`, `claude-plugin-unsigned-executable-download` (hooks and package.json lifecycle scripts), `claude-plugin-unpinned-package-install`, `claude-plugin-undeclared-executable`, `claude-plugin-symlink-escape`, `claude-plugin-archive-path-traversal`, `claude-plugin-unadmitted-submodule`, `claude-plugin-duplicate-json-member`, `claude-plugin-nonstandard-json-constant`, `claude-plugin-malformed-utf8`, `claude-plugin-inconsistent-normalized-name`, `claude-plugin-vendored-scope-undeclared`, `claude-plugin-conflicting-identity`, `claude-plugin-unbounded-mcp`, `claude-plugin-license-missing`, `claude-plugin-license-mismatch`, `claude-plugin-dynamic-eval`, `claude-plugin-hidden-undeclared-executable`, `claude-plugin-concealed-identity`, `claude-plugin-oversized-package`, `claude-plugin-source-mismatch`, `claude-plugin-github-write-token`, `claude-plugin-docker-socket`, `claude-plugin-browser-profile-access`, `claude-plugin-deceptive-description`, `claude-plugin-secret-to-network`, `claude-plugin-secret-to-prompt`, `claude-plugin-secret-to-mcp`, `claude-plugin-hide-actions-directive` / `claude-plugin-self-modify-directive` / `claude-plugin-goal-escalation-directive`, `claude-plugin-setuid-executable` / `claude-plugin-world-writable-executable`, `claude-plugin-decompression-bomb`, reused #1036 `skill-name-homoglyph-confusable` / `skill-manifest-prompt-injection-payload` / `skill-doc-exfiltration-endpoint-directive` / `skill-placeholder-template-unresolved` on plugin skill/agent/command surfaces, deterministic scan receipt with catalog repository/SHA bind, SARIF 2.1.0 `sarif_sha256` bound to the same finding rule_ids, `policy_provenance` bound to the AppGuardrail release plus exact scan-policy digest, and `sbom_sha256` of a deterministic CycloneDX 1.5 document, `claude-plugin-checksum-mismatch` when a first-party SHA256SUMS or sibling `*.sha256` disagrees with bytes on disk, `claude-plugin-unsigned-checksum` when checksum digest rows have no sibling Cosign/GPG signature file, `claude-plugin-excessive-path-depth` when a materialized file or archive member nests past 32 path components, `claude-plugin-github-merge-command` for hook or manifest `gh pr merge`, `claude-plugin-github-release-command` for `gh release create|upload|delete|edit`, `claude-plugin-kubectl-apply-command` for hook or manifest `kubectl apply`, `claude-plugin-docker-push-command` for `docker push`, `claude-plugin-terraform-apply-command` for `terraform apply`, `claude-plugin-helm-install-command` for `helm install`, `claude-plugin-vercel-deploy-command` for hook or manifest `vercel deploy`, `claude-plugin-fly-deploy-command` for `fly deploy`, `claude-plugin-aws-deploy-command` for hook or manifest `aws cloudformation deploy`, `claude-plugin-gcloud-deploy-command` for `gcloud run deploy`, `claude-plugin-az-deploy-command` for `az webapp deploy`, `claude-plugin-aws-s3-write-command` for hook or manifest `aws s3 sync`/`cp`, `claude-plugin-az-containerapp-up-command` for `az containerapp up`, `claude-plugin-npm-publish-command` for hook or manifest `npm publish`, `claude-plugin-pypi-upload-command` for `twine upload`, `claude-plugin-cargo-publish-command` for `cargo publish`, `claude-plugin-pnpm-publish-command` for `pnpm publish`, `claude-plugin-uv-publish-command` for `uv publish`, `claude-plugin-poetry-publish-command` for `poetry publish`, `claude-plugin-gem-push-command` for hook or manifest `gem push`, `claude-plugin-nuget-push-command` for `nuget push`, `claude-plugin-pub-publish-command` for `dart pub publish`/`flutter pub publish`, `claude-plugin-hex-publish-command` for `hex publish`/`mix hex.publish`, `claude-plugin-conda-upload-command` for `conda upload`/`anaconda upload`, `claude-plugin-cabal-upload-command` for `cabal upload`/`cabal v2-upload`, `claude-plugin-mvn-deploy-command` for `mvn deploy`, `claude-plugin-gradle-publish-command` for `gradle publish`/`gradlew publish`, `claude-plugin-luarocks-upload-command` for `luarocks upload`, `claude-plugin-credential-store-access` for host cookie and token stores that are not browser profiles, fail-closed receipt verification | implemented-branch | | Orphaned GitHub Actions registry identities (#929) | owned by PR #966 / issue #929; live registry DAST | mapped-family only; this successor does not ship or close the detector | | Org security-failure CI tickets without copied vuln evidence | documented non-detectable family | snapshot in `tests/fixtures/cwl-security-issue-inventory.json` | diff --git a/docs/sast-dast-rule-research.md b/docs/sast-dast-rule-research.md index 10509b24..d1098d6b 100644 --- a/docs/sast-dast-rule-research.md +++ b/docs/sast-dast-rule-research.md @@ -125,6 +125,8 @@ files being scanned, then applies the union of relevant checks. Examples: `claude-plugin-conda-upload-command` for ``conda upload``, `claude-plugin-cabal-upload-command` for ``cabal upload``, `claude-plugin-mvn-deploy-command` for ``mvn deploy``, + `claude-plugin-gradle-publish-command` for ``gradle publish``, + `claude-plugin-luarocks-upload-command` for ``luarocks upload``, and `claude-plugin-credential-store-access` for host ``~/.netrc``, ``~/.aws/credentials``, GitHub CLI hosts, Docker auth, cookie jars, and @@ -134,8 +136,8 @@ files being scanned, then applies the union of relevant checks. Examples: ``kubectl get``, ``docker ps``, ``terraform plan``, ``helm list``, ``vercel ls``, ``fly status``, ``aws s3 ls``, ``gcloud config list``, ``az account show``, ``npm pack``, ``cargo check``, ``gem list``, - ``nuget list``, ``hex info``, ``conda list``, ``cabal list``, and - ``mvn package`` stay inventory. + ``nuget list``, ``hex info``, ``conda list``, ``cabal list``, + ``mvn package``, ``gradle tasks``, and ``luarocks list`` stay inventory. - Mapped, not owned here: GitHub Actions transport-only poll loops (#1087, PR #1088) and orphaned workflow registry DAST (#929, PR #966). - `tool-execute-parameters-passthrough`: Strix-observed dynamic tool execution diff --git a/tests/test_claude_plugin_terraform_helm.py b/tests/test_claude_plugin_terraform_helm.py index fe3bc41e..8adefd70 100644 --- a/tests/test_claude_plugin_terraform_helm.py +++ b/tests/test_claude_plugin_terraform_helm.py @@ -110,7 +110,6 @@ def test_vercel_deploy_and_fly_deploy_stay_inventory(tmp_path: Path) -> None: inventory = inventory_claude_plugin_capabilities(root) assert _THIS_CLASS.isdisjoint(receipt.finding_summary) - assert receipt.scan_result == "pass" assert inventory["deployment_write"] is True