Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.d/1099-claude-plugin-supply-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,17 @@
`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`.
Quoted or unquoted exact ``deno publish`` tasks fail as
`claude-plugin-deno-publish-command`. Quoted or unquoted exact
``pod trunk push`` tasks fail as `claude-plugin-pod-trunk-push-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``, ``luarocks list``, ``sbt compile``,
and ``conan list``
``conan list``, ``deno info``, and ``pod install``
stay inventory. Hardcoded
PATs stay `claude-plugin-github-write-token`. Snippets are command
labels, not tokens.
Expand Down
92 changes: 89 additions & 3 deletions appguardrail_core/claude_plugin_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,16 @@
"package is write authority on Conan Center. Remove the command. "
"[CWE-250 - Execution with Unnecessary Privileges]"
)
CLAUDE_PLUGIN_DENO_PUBLISH_COMMAND_MESSAGE: Final = (
"Claude plugin hook or manifest runs deno publish. Publishing a "
"package is write authority on JSR. Remove the command. "
"[CWE-269 - Improper Privilege Management]"
)
CLAUDE_PLUGIN_POD_TRUNK_PUSH_COMMAND_MESSAGE: Final = (
"Claude plugin hook or manifest runs pod trunk push. Publishing a "
"podspec is write authority on CocoaPods trunk. 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 "
Expand Down Expand Up @@ -591,6 +601,19 @@
r"\bconan\s+upload\b",
re.IGNORECASE,
)
_DENO_PUBLISH_COMMAND = re.compile(
r"(?<![A-Za-z0-9_])(?P<cli_quote>['\"]?)deno(?P=cli_quote)"
r"[ \t]+(?P<quote>['\"]?)publish(?P=quote)"
r"(?=$|[ \t;&|`\)])",
re.IGNORECASE,
)
_POD_TRUNK_PUSH_COMMAND = re.compile(
r"(?<![A-Za-z0-9_])(?P<cli_quote>['\"]?)pod(?P=cli_quote)"
r"[ \t]+(?P<trunk_quote>['\"]?)trunk(?P=trunk_quote)"
r"[ \t]+(?P<push_quote>['\"]?)push(?P=push_quote)"
r"(?=$|[ \t;&|`\)])",
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(
Expand Down Expand Up @@ -876,8 +899,8 @@
(
"package_install",
re.compile(
r"\b(?:pip|npm|pnpm|yarn|uv|cargo|apt-get)\s+install\b|"
r"\b(?:npm\s+publish|pnpm\s+publish|twine\s+upload|cargo\s+publish|"
r"(?<![A-Za-z0-9_])(?:pip|npm|pnpm|yarn|uv|cargo|apt-get)\s+install\b|"
r"(?<![A-Za-z0-9_])(?:npm\s+publish|pnpm\s+publish|twine\s+upload|cargo\s+publish|"
r"uv\s+publish|poetry\s+publish|gem\s+push|"
r"(?:dotnet\s+)?nuget\s+push|"
r"(?:dart\s+|flutter\s+)?pub\s+publish|"
Expand All @@ -888,7 +911,12 @@
r"gradlew?\s+publish|"
r"luarocks\s+upload|"
r"sbt\s+publish(?:Signed)?|"
r"conan\s+upload)\b",
r"conan\s+upload|"
r"(?:deno|\"deno\"|'deno')[ \t]+"
r"(?:publish|\"publish\"|'publish')|"
r"(?:pod|\"pod\"|'pod')[ \t]+"
r"(?:trunk|\"trunk\"|'trunk')[ \t]+"
r"(?:push|\"push\"|'push'))(?![A-Za-z0-9_])",
re.IGNORECASE,
),
),
Expand Down Expand Up @@ -1125,6 +1153,8 @@ def inspect_claude_plugin_file(
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(_deno_publish_command_hits(content, manifest=manifest))
hits.extend(_pod_trunk_push_command_hits(content, manifest=manifest))
hits.extend(_docker_socket_hits(content))
hits.extend(_browser_profile_hits(content))
hits.extend(_credential_store_hits(content))
Expand Down Expand Up @@ -2898,6 +2928,62 @@ def _conan_upload_command_hits(
return ()


def _deno_publish_command_hits(
content: str, *, manifest: bool = False
) -> tuple[PluginHit, ...]:
"""Return ``deno publish`` 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 ``deno publish``. ``deno info`` is not
this class. ``sbt publish`` stays the sbt class.
"""
for source, first_line in _hosted_command_sources(content, manifest=manifest):
match = _executable_command_match(source, _DENO_PUBLISH_COMMAND)
if match is None:
continue
return (
PluginHit(
rule_id="claude-plugin-deno-publish-command",
line=first_line + source[: match.start()].count("\n"),
snippet="deno publish",
message=CLAUDE_PLUGIN_DENO_PUBLISH_COMMAND_MESSAGE,
),
)
return ()


def _pod_trunk_push_command_hits(
content: str, *, manifest: bool = False
) -> tuple[PluginHit, ...]:
"""Return ``pod trunk push`` findings with a command label, not pod names.

Args:
content: Hook or manifest text.
manifest: When true, only structural command values are scanned.

Returns:
One hit for executable ``pod trunk push``. ``pod install`` and
``pod lib lint`` are not this class.
"""
for source, first_line in _hosted_command_sources(content, manifest=manifest):
match = _executable_command_match(source, _POD_TRUNK_PUSH_COMMAND)
if match is None:
continue
return (
PluginHit(
rule_id="claude-plugin-pod-trunk-push-command",
line=first_line + source[: match.start()].count("\n"),
snippet="pod trunk push",
message=CLAUDE_PLUGIN_POD_TRUNK_PUSH_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)
Expand Down
2 changes: 1 addition & 1 deletion docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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 |
| 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-deno-publish-command` for quoted or unquoted exact `deno publish` tasks, `claude-plugin-pod-trunk-push-command` for quoted or unquoted exact `pod trunk push` tasks, `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` |

Expand Down
6 changes: 5 additions & 1 deletion docs/sast-dast-rule-research.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ files being scanned, then applies the union of relevant checks. Examples:
`claude-plugin-luarocks-upload-command` for ``luarocks upload``,
`claude-plugin-sbt-publish-command` for ``sbt publish``,
`claude-plugin-conan-upload-command` for ``conan upload``,
`claude-plugin-deno-publish-command` for quoted or unquoted exact
``deno publish`` tasks,
`claude-plugin-pod-trunk-push-command` for quoted or unquoted exact
``pod trunk push`` tasks,
and
`claude-plugin-credential-store-access` for host ``~/.netrc``,
``~/.aws/credentials``, GitHub CLI hosts, Docker auth, cookie jars, and
Expand All @@ -140,7 +144,7 @@ files being scanned, then applies the union of relevant checks. Examples:
``az account show``, ``npm pack``, ``cargo check``, ``gem list``,
``nuget list``, ``hex info``, ``conda list``, ``cabal list``,
``mvn package``, ``gradle tasks``, ``luarocks list``, ``sbt compile``,
and ``conan list`` stay inventory.
``conan list``, ``deno info``, and ``pod install`` 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
Expand Down
Loading