From a88ac0660e015ce6e621126c5a3d4798cce9f6d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:44:31 +0900 Subject: [PATCH 01/10] test(scanner): fail closed on plugin sbt publish and conan upload Hook and manifest sbt publish, sbt publishSigned, and conan upload must fail closed. sbt compile, sbt publishLocal, conan list, comments, echo lookalikes, assignment values, and README wording stay inventory. Relates to #1099. --- tests/test_claude_plugin_sbt_conan.py | 243 ++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 tests/test_claude_plugin_sbt_conan.py diff --git a/tests/test_claude_plugin_sbt_conan.py b/tests/test_claude_plugin_sbt_conan.py new file mode 100644 index 00000000..4e712fec --- /dev/null +++ b/tests/test_claude_plugin_sbt_conan.py @@ -0,0 +1,243 @@ +"""Hook sbt publish and conan upload fail closed; compile/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" +_SBT_RULE = "claude-plugin-sbt-publish-command" +_CONAN_RULE = "claude-plugin-conan-upload-command" +_GRADLE_RULE = "claude-plugin-gradle-publish-command" +_SECRET = "sk-sbt-must-not-leak" +_BIDI = "\u202e" +_THIS_CLASS = frozenset({_SBT_RULE, _CONAN_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_sbt_publish_fails_admission(tmp_path: Path) -> None: + """``sbt publish`` on a hook is Maven-repository write authority.""" + root = _licensed_plugin(tmp_path, "#!/bin/sh\nsbt publish\n") + hits = _hits(root, _SBT_RULE) + receipt = build_claude_plugin_scan_receipt(root) + inventory = inventory_claude_plugin_capabilities(root) + + assert hits + assert all(hit.snippet == "sbt publish" for hit in hits) + assert receipt.scan_result == "fail" + assert _SBT_RULE in receipt.finding_summary + assert _CONAN_RULE not in receipt.finding_summary + assert _GRADLE_RULE not in receipt.finding_summary + assert inventory["package_install"] is True + + +def test_sbt_publish_signed_is_the_same_class() -> None: + """``sbt publishSigned`` is the signed spelling of the sbt publish class.""" + body = "#!/bin/sh\nsbt publishSigned\n" + hits = inspect_claude_plugin_file("session.sh", "hooks/session.sh", body) + assert any( + hit.rule_id == _SBT_RULE and hit.snippet == "sbt publishSigned" for hit in hits + ) + + +def test_hook_conan_upload_fails_admission(tmp_path: Path) -> None: + """``conan upload`` on a hook is Conan Center write authority.""" + root = _licensed_plugin(tmp_path, "#!/bin/sh\nconan upload pkg/1.0.0@user/stable\n") + hits = _hits(root, _CONAN_RULE) + receipt = build_claude_plugin_scan_receipt(root) + + assert hits + assert all(hit.snippet == "conan upload" for hit in hits) + assert receipt.scan_result == "fail" + assert _CONAN_RULE in receipt.finding_summary + assert _SBT_RULE not in receipt.finding_summary + + +def test_sbt_compile_and_conan_list_stay_inventory(tmp_path: Path) -> None: + """Read-only sbt compile and conan list stay inventory.""" + root = _licensed_plugin(tmp_path, "#!/bin/sh\nsbt compile\nconan list\n") + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _SBT_RULE) == [] + assert _hits(root, _CONAN_RULE) == [] + assert receipt.scan_result == "pass" + + +def test_sbt_publish_local_is_not_this_class(tmp_path: Path) -> None: + """``sbt publishLocal`` stays a local Ivy task, not a remote publish.""" + root = _licensed_plugin(tmp_path, "#!/bin/sh\nsbt publishLocal\n") + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _SBT_RULE) == [] + assert receipt.scan_result == "pass" + + +def test_gradle_and_sbt_on_one_hook_are_distinct_findings(tmp_path: Path) -> None: + """One hook can fail closed on both gradle publish and sbt publish.""" + root = _licensed_plugin( + tmp_path, + "#!/bin/sh\ngradle publish\nsbt publish\n", + ) + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _GRADLE_RULE) + assert _hits(root, _SBT_RULE) + assert receipt.scan_result == "fail" + assert _CONAN_RULE not in receipt.finding_summary + + +def test_gradle_publish_stays_the_gradle_class() -> None: + """``gradle publish`` remains the Gradle class, not sbt.""" + body = "#!/bin/sh\ngradle publish\n" + hits = inspect_claude_plugin_file("session.sh", "hooks/session.sh", body) + rule_ids = {hit.rule_id for hit in hits} + assert _GRADLE_RULE in rule_ids + assert _THIS_CLASS.isdisjoint(rule_ids) + + +def test_comment_and_echo_sbt_conan_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# sbt publish\necho "conan upload pkg/1.0.0@user/stable"\n', + ) + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _SBT_RULE) == [] + assert _hits(root, _CONAN_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=sbt publish\n", + "#!/bin/sh\ncommand=conan upload pkg/1.0.0@user/stable\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\nSBT_OPTS=-Xmx1g sbt publish\n", + "#!/bin/sh\nCONAN_USER_HOME=/tmp conan upload pkg/1.0.0@user/stable\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_sbt_conan_is_not_this_class(tmp_path: Path) -> None: + """README sbt/conan wording is repository guidance, not a hook command.""" + root = _licensed_plugin(tmp_path) + (root / "README.md").write_text( + "sbt publish\nconan upload pkg/1.0.0@user/stable\n", + encoding="utf-8", + ) + receipt = build_claude_plugin_scan_receipt(root) + inventory = inventory_claude_plugin_capabilities(root) + + assert _hits(root, _SBT_RULE) == [] + assert _hits(root, _CONAN_RULE) == [] + assert receipt.scan_result == "pass" + assert inventory["package_install"] is True + + +def test_echo_then_real_sbt_publish_still_fails() -> None: + """``echo done && sbt publish`` still runs the registry write.""" + hits = inspect_claude_plugin_file( + "session.sh", + "hooks/session.sh", + '#!/bin/sh\necho "done" && sbt publish\n', + ) + assert any( + hit.rule_id == _SBT_RULE and hit.snippet == "sbt 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\nsbt publish -Dsbt.sonatype.password={_SECRET}{_BIDI}\n" + root = _licensed_plugin(tmp_path, body) + hits = inspect_claude_plugin_file("session.sh", "hooks/session.sh", body) + sbt_hits = [hit for hit in hits if hit.rule_id == _SBT_RULE] + payload = json.dumps(build_claude_plugin_scan_receipt(root).as_dict()) + + assert sbt_hits + for hit in sbt_hits: + assert hit.snippet == "sbt 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_conan_upload_fails_admission(tmp_path: Path) -> None: + """A plugin.json command string that uploads to Conan Center 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": "conan upload pkg/1.0.0@user/stable"}], + } + _write_json(root / ".claude-plugin" / "plugin.json", manifest) + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _CONAN_RULE) + assert receipt.scan_result == "fail" + + +def test_manifest_prose_is_not_this_class(tmp_path: Path) -> None: + """Marketplace description prose about sbt 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 sbt publish or conan upload." + _write_json(root / ".claude-plugin" / "plugin.json", manifest) + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _SBT_RULE) == [] + assert _hits(root, _CONAN_RULE) == [] + assert receipt.scan_result == "pass" From 34ef68bd0c5ec595eedcaa04851d8fbd7789c19b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:46:28 +0900 Subject: [PATCH 02/10] feat(scanner): reject plugin sbt publish and conan upload Fail closed on executable sbt publish, sbt publishSigned, and conan upload. sbt compile, sbt publishLocal, and conan list stay inventory. gradle publish stays the Gradle class. Relates to #1099. --- .../1099-claude-plugin-supply-chain.md | 7 +- appguardrail_core/claude_plugin_detector.py | 85 ++++++++++++++++++- docs/TRACEABILITY.md | 2 +- docs/sast-dast-rule-research.md | 5 +- 4 files changed, 94 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.d/1099-claude-plugin-supply-chain.md b/CHANGELOG.d/1099-claude-plugin-supply-chain.md index e848c768..202af29e 100644 --- a/CHANGELOG.d/1099-claude-plugin-supply-chain.md +++ b/CHANGELOG.d/1099-claude-plugin-supply-chain.md @@ -171,14 +171,17 @@ ``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`. + `claude-plugin-luarocks-upload-command`. ``sbt publish`` and + ``sbt publishSigned`` fail as `claude-plugin-sbt-publish-command`. + ``conan upload`` fails as `claude-plugin-conan-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``, - ``mvn package``, ``gradle tasks``, and ``luarocks list`` + ``mvn package``, ``gradle tasks``, ``luarocks list``, ``sbt compile``, + and ``conan 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 39658d6e..aef82081 100644 --- a/appguardrail_core/claude_plugin_detector.py +++ b/appguardrail_core/claude_plugin_detector.py @@ -393,6 +393,16 @@ "package is write authority on LuaRocks. Remove the command. " "[CWE-250 - Execution with Unnecessary Privileges]" ) +CLAUDE_PLUGIN_SBT_PUBLISH_COMMAND_MESSAGE: Final = ( + "Claude plugin hook or manifest runs sbt publish. Publishing a " + "package is write authority on a Maven repository. Remove the command. " + "[CWE-269 - Improper Privilege Management]" +) +CLAUDE_PLUGIN_CONAN_UPLOAD_COMMAND_MESSAGE: Final = ( + "Claude plugin hook or manifest runs conan upload. Publishing a " + "package is write authority on Conan Center. 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 " @@ -572,6 +582,14 @@ r"\bluarocks\s+upload\b", re.IGNORECASE, ) +_SBT_PUBLISH_COMMAND = re.compile( + r"\bsbt\s+(?Ppublish(?:Signed)?)\b", + re.IGNORECASE, +) +_CONAN_UPLOAD_COMMAND = re.compile( + r"\bconan\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( @@ -867,7 +885,9 @@ r"cabal\s+(?:v2-)?upload|" r"mvn\s+deploy|" r"gradlew?\s+publish|" - r"luarocks\s+upload)\b", + r"luarocks\s+upload|" + r"sbt\s+publish(?:Signed)?|" + r"conan\s+upload)\b", re.IGNORECASE, ), ), @@ -1102,6 +1122,8 @@ def inspect_claude_plugin_file( 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(_sbt_publish_command_hits(content, manifest=manifest)) + hits.extend(_conan_upload_command_hits(content, manifest=manifest)) hits.extend(_docker_socket_hits(content)) hits.extend(_browser_profile_hits(content)) hits.extend(_credential_store_hits(content)) @@ -2814,6 +2836,67 @@ def _luarocks_upload_command_hits( return () +def _sbt_publish_command_hits( + content: str, *, manifest: bool = False +) -> tuple[PluginHit, ...]: + """Return ``sbt publish`` findings with a command label, not artifact names. + + Args: + content: Hook or manifest text. + manifest: When true, only structural command values are scanned. + + Returns: + One hit for executable ``sbt publish`` or ``sbt publishSigned``. + ``sbt compile`` and ``sbt publishLocal`` are not this class. + ``gradle publish`` stays the Gradle class. + """ + for source, first_line in _hosted_command_sources(content, manifest=manifest): + match = _executable_command_match(source, _SBT_PUBLISH_COMMAND) + if match is None: + continue + verb = match.group("verb") + snippet = ( + "sbt publishSigned" if verb.lower() == "publishsigned" else "sbt publish" + ) + return ( + PluginHit( + rule_id="claude-plugin-sbt-publish-command", + line=first_line + source[: match.start()].count("\n"), + snippet=snippet, + message=CLAUDE_PLUGIN_SBT_PUBLISH_COMMAND_MESSAGE, + ), + ) + return () + + +def _conan_upload_command_hits( + content: str, *, manifest: bool = False +) -> tuple[PluginHit, ...]: + """Return ``conan upload`` findings with a command label, not package names. + + Args: + content: Hook or manifest text. + manifest: When true, only structural command values are scanned. + + Returns: + One hit for executable ``conan upload``. ``conan list`` is not + this class. + """ + for source, first_line in _hosted_command_sources(content, manifest=manifest): + match = _executable_command_match(source, _CONAN_UPLOAD_COMMAND) + if match is None: + continue + return ( + PluginHit( + rule_id="claude-plugin-conan-upload-command", + line=first_line + source[: match.start()].count("\n"), + snippet="conan upload", + message=CLAUDE_PLUGIN_CONAN_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 547a66d1..89818a07 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-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 | +| 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-sbt-publish-command` for `sbt publish`/`sbt publishSigned`, `claude-plugin-conan-upload-command` for `conan 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 d1098d6b..942ed9bb 100644 --- a/docs/sast-dast-rule-research.md +++ b/docs/sast-dast-rule-research.md @@ -127,6 +127,8 @@ files being scanned, then applies the union of relevant checks. Examples: `claude-plugin-mvn-deploy-command` for ``mvn deploy``, `claude-plugin-gradle-publish-command` for ``gradle publish``, `claude-plugin-luarocks-upload-command` for ``luarocks upload``, + `claude-plugin-sbt-publish-command` for ``sbt publish``, + `claude-plugin-conan-upload-command` for ``conan upload``, and `claude-plugin-credential-store-access` for host ``~/.netrc``, ``~/.aws/credentials``, GitHub CLI hosts, Docker auth, cookie jars, and @@ -137,7 +139,8 @@ files being scanned, then applies the union of relevant checks. Examples: ``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``, - ``mvn package``, ``gradle tasks``, and ``luarocks list`` stay inventory. + ``mvn package``, ``gradle tasks``, ``luarocks list``, ``sbt compile``, + and ``conan 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 From ad3a238b1c35158c1301418928d8fa91e32634f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:55:08 +0900 Subject: [PATCH 03/10] test(scanner): reproduce quoted sbt publish miss --- tests/test_claude_plugin_sbt_conan.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_claude_plugin_sbt_conan.py b/tests/test_claude_plugin_sbt_conan.py index 4e712fec..ed7f77d9 100644 --- a/tests/test_claude_plugin_sbt_conan.py +++ b/tests/test_claude_plugin_sbt_conan.py @@ -241,3 +241,18 @@ def test_manifest_prose_is_not_this_class(tmp_path: Path) -> None: assert _hits(root, _SBT_RULE) == [] assert _hits(root, _CONAN_RULE) == [] assert receipt.scan_result == "pass" + + +def test_quoted_sbt_publish_tasks_fail_admission() -> None: + """Quoted exact sbt publish tasks remain executable registry writes.""" + bodies = ( + '#!/bin/sh\nsbt "publish"\n', + "#!/bin/sh\nsbt 'publishSigned'\n", + ) + expected = ("sbt publish", "sbt publishSigned") + for body, snippet in zip(bodies, expected, strict=True): + hits = inspect_claude_plugin_file("session.sh", "hooks/session.sh", body) + assert any( + hit.rule_id == _SBT_RULE and hit.snippet == snippet for hit in hits + ) + From ccce2a0db42b6998de43fdd85f8f62a3cf7accc4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:55:49 +0900 Subject: [PATCH 04/10] fix(scanner): recognize quoted sbt publish tasks --- appguardrail_core/claude_plugin_detector.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appguardrail_core/claude_plugin_detector.py b/appguardrail_core/claude_plugin_detector.py index aef82081..2b60adb6 100644 --- a/appguardrail_core/claude_plugin_detector.py +++ b/appguardrail_core/claude_plugin_detector.py @@ -583,7 +583,8 @@ re.IGNORECASE, ) _SBT_PUBLISH_COMMAND = re.compile( - r"\bsbt\s+(?Ppublish(?:Signed)?)\b", + r"\bsbt[ \t]+(?P['\"]?)(?Ppublish(?:Signed)?)(?P=quote)" + r"(?=$|[ \t;&|])", re.IGNORECASE, ) _CONAN_UPLOAD_COMMAND = re.compile( From 96069e5e4c220ac46510e984577df38844de410c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:56:10 +0900 Subject: [PATCH 05/10] test(scanner): preserve quoted sbt local-task negative --- tests/test_claude_plugin_sbt_conan.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_claude_plugin_sbt_conan.py b/tests/test_claude_plugin_sbt_conan.py index ed7f77d9..23fa3270 100644 --- a/tests/test_claude_plugin_sbt_conan.py +++ b/tests/test_claude_plugin_sbt_conan.py @@ -104,11 +104,13 @@ def test_sbt_compile_and_conan_list_stay_inventory(tmp_path: Path) -> None: def test_sbt_publish_local_is_not_this_class(tmp_path: Path) -> None: - """``sbt publishLocal`` stays a local Ivy task, not a remote publish.""" - root = _licensed_plugin(tmp_path, "#!/bin/sh\nsbt publishLocal\n") - receipt = build_claude_plugin_scan_receipt(root) - assert _hits(root, _SBT_RULE) == [] - assert receipt.scan_result == "pass" + """Quoted or unquoted publishLocal stays local, not a remote publish.""" + bodies = ("#!/bin/sh\nsbt publishLocal\n", '#!/bin/sh\nsbt "publishLocal"\n') + for body in bodies: + root = _licensed_plugin(tmp_path, body) + receipt = build_claude_plugin_scan_receipt(root) + assert _hits(root, _SBT_RULE) == [] + assert receipt.scan_result == "pass" def test_gradle_and_sbt_on_one_hook_are_distinct_findings(tmp_path: Path) -> None: From 2305b8f69519a8819a4b32daba10d676e247e37d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:56:30 +0900 Subject: [PATCH 06/10] docs(scanner): trace quoted sbt publish boundary --- docs/TRACEABILITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 89818a07..5387db30 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-gradle-publish-command` for `gradle publish`/`gradlew publish`, `claude-plugin-luarocks-upload-command` for `luarocks upload`, `claude-plugin-sbt-publish-command` for `sbt publish`/`sbt publishSigned`, `claude-plugin-conan-upload-command` for `conan upload`, `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-sbt-publish-command` for quoted or unquoted exact `sbt publish`/`sbt publishSigned` tasks (`publishLocal` remains negative), `claude-plugin-conan-upload-command` for `conan 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` | From 25e07a631492f797858ce2622c607d5e6560d599 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:57:40 +0900 Subject: [PATCH 07/10] test(scanner): reproduce quoted sbt substitution miss --- tests/test_claude_plugin_sbt_conan.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_claude_plugin_sbt_conan.py b/tests/test_claude_plugin_sbt_conan.py index 23fa3270..9557405c 100644 --- a/tests/test_claude_plugin_sbt_conan.py +++ b/tests/test_claude_plugin_sbt_conan.py @@ -258,3 +258,14 @@ def test_quoted_sbt_publish_tasks_fail_admission() -> None: hit.rule_id == _SBT_RULE and hit.snippet == snippet for hit in hits ) + +def test_quoted_sbt_task_in_substitution_still_fails() -> None: + """Quoted sbt publish tasks inside shell substitutions remain executable.""" + bodies = ( + '#!/bin/sh\nresult=$(sbt "publishSigned")\n', + "#!/bin/sh\nresult=\`sbt 'publish'\`\n", + ) + for body in bodies: + hits = inspect_claude_plugin_file("session.sh", "hooks/session.sh", body) + assert any(hit.rule_id == _SBT_RULE for hit in hits) + From 935a5fd7f4a8d7e979c46d9822ff6a9a2d05e968 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:57:51 +0900 Subject: [PATCH 08/10] fix(scanner): keep quoted sbt substitutions executable --- appguardrail_core/claude_plugin_detector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appguardrail_core/claude_plugin_detector.py b/appguardrail_core/claude_plugin_detector.py index 2b60adb6..0c29d995 100644 --- a/appguardrail_core/claude_plugin_detector.py +++ b/appguardrail_core/claude_plugin_detector.py @@ -584,7 +584,7 @@ ) _SBT_PUBLISH_COMMAND = re.compile( r"\bsbt[ \t]+(?P['\"]?)(?Ppublish(?:Signed)?)(?P=quote)" - r"(?=$|[ \t;&|])", + r"(?=$|[ \t;&|)`])", re.IGNORECASE, ) _CONAN_UPLOAD_COMMAND = re.compile( From 08aa1014bea3a84fea88f04bb55eb85a578c26fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:58:05 +0900 Subject: [PATCH 09/10] docs(scanner): trace quoted sbt substitution boundary --- docs/TRACEABILITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 5387db30..9a37ba1b 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-gradle-publish-command` for `gradle publish`/`gradlew publish`, `claude-plugin-luarocks-upload-command` for `luarocks upload`, `claude-plugin-sbt-publish-command` for quoted or unquoted exact `sbt publish`/`sbt publishSigned` tasks (`publishLocal` remains negative), `claude-plugin-conan-upload-command` for `conan upload`, `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-sbt-publish-command` for quoted or unquoted exact `sbt publish`/`sbt publishSigned` tasks, including shell substitutions (`publishLocal` remains negative), `claude-plugin-conan-upload-command` for `conan 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` | From 63c42a76c7b06d2b00ace53cfdde59d56ae9f0ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:58:38 +0900 Subject: [PATCH 10/10] test(scanner): remove invalid backtick escape --- tests/test_claude_plugin_sbt_conan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_claude_plugin_sbt_conan.py b/tests/test_claude_plugin_sbt_conan.py index 9557405c..23897373 100644 --- a/tests/test_claude_plugin_sbt_conan.py +++ b/tests/test_claude_plugin_sbt_conan.py @@ -263,7 +263,7 @@ def test_quoted_sbt_task_in_substitution_still_fails() -> None: """Quoted sbt publish tasks inside shell substitutions remain executable.""" bodies = ( '#!/bin/sh\nresult=$(sbt "publishSigned")\n', - "#!/bin/sh\nresult=\`sbt 'publish'\`\n", + "#!/bin/sh\nresult=`sbt 'publish'`\n", ) for body in bodies: hits = inspect_claude_plugin_file("session.sh", "hooks/session.sh", body)