Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 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
7 changes: 6 additions & 1 deletion CHANGELOG.d/1099-claude-plugin-supply-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,12 @@
``gh pr review``, ``gh release list``, ``kubectl get``, ``docker ps``,
``terraform apply`` fails as `claude-plugin-terraform-apply-command`.
``helm install`` fails as `claude-plugin-helm-install-command`.
``terraform plan``, ``helm list``, ``vercel deploy``, and ``fly deploy``
``vercel deploy`` fails as `claude-plugin-vercel-deploy-command`.
``fly deploy`` and ``flyctl deploy`` fail as
`claude-plugin-fly-deploy-command`. Hook comments and
``echo``/``printf`` lookalikes are not those classes.
``terraform plan``, ``helm list``,
``vercel ls``, and ``fly status``
stay inventory. Hardcoded
PATs stay `claude-plugin-github-write-token`. Snippets are command
labels, not tokens.
Expand Down
120 changes: 115 additions & 5 deletions appguardrail_core/claude_plugin_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,19 @@
Hook or manifest ``kubectl apply`` and ``docker push`` fail closed as
deployment-write command findings. Hook or manifest ``terraform apply``
and ``helm install`` fail closed as infra-write command findings.
``terraform plan``, ``helm list``, ``vercel deploy``, and ``fly deploy``
Hook or manifest ``vercel deploy`` and ``fly deploy`` fail closed as
hosted-deploy command findings. Unquoted ``#`` comments and
``echo``/``printf``/``print`` lookalikes are not that class.
``terraform plan``, ``helm list``, ``vercel ls``, and ``fly status``
stay inventory. Hook or manifest paths into
``~/.netrc``, ``~/.aws/credentials``,
GitHub CLI hosts, Docker auth ``config.json``, cookie jars, and
``~/.ssh/id_*`` private keys fail closed as credential-store findings.
Chrome and Firefox profile stores stay browser-profile findings.
Hardcoded PATs stay write-token findings.
``gh issue create``, ``gh pr review``, ``kubectl get``, ``docker ps``,
``terraform plan``, and ``helm list`` stay inventory. Skill
``terraform plan``, ``helm list``, ``vercel ls``, and ``fly status``
stay inventory. Skill
homoglyph, injection, exfiltration, and placeholder hits reuse #1036 rule
identities. Skill, command, or agent text that hides tool use, rewrites
the system prompt, or escalates the declared goal is a separate
Expand Down Expand Up @@ -247,6 +251,16 @@
"is write authority on a cluster. Remove the command. "
"[CWE-250 - Execution with Unnecessary Privileges]"
)
CLAUDE_PLUGIN_VERCEL_DEPLOY_COMMAND_MESSAGE: Final = (
"Claude plugin hook or manifest runs vercel deploy. Publishing to a "
"hosted platform is write authority. Remove the command. "
"[CWE-269 - Improper Privilege Management]"
)
CLAUDE_PLUGIN_FLY_DEPLOY_COMMAND_MESSAGE: Final = (
"Claude plugin hook or manifest runs fly deploy. Publishing to a "
"hosted platform is write authority. 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 @@ -379,6 +393,8 @@
_HELM_INSTALL_COMMAND = re.compile(
r"\bhelm\s+install(?=$|[\s;&|()<>])", re.IGNORECASE
)
_VERCEL_DEPLOY_COMMAND = re.compile(r"\bvercel\s+deploy\b", re.IGNORECASE)
_FLY_DEPLOY_COMMAND = re.compile(r"\b(?:fly|flyctl)\s+deploy\b", re.IGNORECASE)
_REPORTING_BUILTINS: Final = frozenset(
{":", "echo", "false", "print", "printf", "true"}
)
Expand Down Expand Up @@ -643,7 +659,7 @@
"deployment_write",
re.compile(
r"\b(?:kubectl\s+apply|terraform\s+apply|helm\s+install|"
r"vercel\s+deploy|fly\s+deploy|docker\s+push)\b",
r"vercel\s+deploy|fly(?:ctl)?\s+deploy|docker\s+push)\b",
re.IGNORECASE,
),
),
Expand Down Expand Up @@ -897,6 +913,8 @@ def inspect_claude_plugin_file(
hits.extend(_docker_push_command_hits(content, manifest=manifest))
hits.extend(_terraform_apply_command_hits(content, manifest=manifest))
hits.extend(_helm_install_command_hits(content, manifest=manifest))
hits.extend(_vercel_deploy_command_hits(content, manifest=manifest))
hits.extend(_fly_deploy_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 @@ -1707,6 +1725,44 @@ def _docker_push_command_hits(
)
return ()

def _terraform_apply_command_hits(
content: str, *, manifest: bool = False
) -> tuple[PluginHit, ...]:
"""Return executable terraform apply findings without vars."""
for source, first_line in _hosted_command_sources(content, manifest=manifest):
match = _executable_command_match(source, _TERRAFORM_APPLY_COMMAND)
if match is None:
continue
return (
PluginHit(
rule_id="claude-plugin-terraform-apply-command",
line=first_line + source[: match.start()].count("\n"),
snippet="terraform apply",
message=CLAUDE_PLUGIN_TERRAFORM_APPLY_COMMAND_MESSAGE,
),
)
return ()


def _helm_install_command_hits(
content: str, *, manifest: bool = False
) -> tuple[PluginHit, ...]:
"""Return executable helm install findings without chart names."""
for source, first_line in _hosted_command_sources(content, manifest=manifest):
match = _executable_command_match(source, _HELM_INSTALL_COMMAND)
if match is None:
continue
return (
PluginHit(
rule_id="claude-plugin-helm-install-command",
line=first_line + source[: match.start()].count("\n"),
snippet="helm install",
message=CLAUDE_PLUGIN_HELM_INSTALL_COMMAND_MESSAGE,
),
)
return ()


def _unquoted_hash_index(line: str) -> int | None:
"""Return the index of an unquoted ``#`` shell comment, if any.

Expand Down Expand Up @@ -2103,8 +2159,6 @@ def _executable_command_match( content: str, pattern: re.Pattern[str]
return match
break
return None


def _shell_payload_index(
arguments: tuple[str, ...] | list[str], *, shell_name: str
) -> int | None:
Expand Down Expand Up @@ -2284,6 +2338,62 @@ def _helm_install_command_hits(
return ()


def _vercel_deploy_command_hits(
content: str, *, manifest: bool = False
) -> tuple[PluginHit, ...]:
"""Return ``vercel deploy`` findings with a command label, not tokens.

Args:
content: Hook or manifest text.

Returns:
One hit when an executable ``vercel deploy`` is present.
``vercel ls``, README wording, hook comments, and echo/printf
lookalikes are not this class.
"""
for source, first_line in _hosted_command_sources(content, manifest=manifest):
match = _executable_command_match(source, _VERCEL_DEPLOY_COMMAND)
if match is None:
continue
return (
PluginHit(
rule_id="claude-plugin-vercel-deploy-command",
line=first_line + source[: match.start()].count("\n"),
snippet="vercel deploy",
message=CLAUDE_PLUGIN_VERCEL_DEPLOY_COMMAND_MESSAGE,
),
)
return ()


def _fly_deploy_command_hits(
content: str, *, manifest: bool = False
) -> tuple[PluginHit, ...]:
"""Return ``fly deploy`` findings with a command label, not app names.

Args:
content: Hook or manifest text.

Returns:
One hit for executable ``fly deploy`` or ``flyctl deploy``.
``fly status``, hook comments, and echo/printf lookalikes are
not this class.
"""
for source, first_line in _hosted_command_sources(content, manifest=manifest):
match = _executable_command_match(source, _FLY_DEPLOY_COMMAND)
if match is None:
continue
return (
PluginHit(
rule_id="claude-plugin-fly-deploy-command",
line=first_line + source[: match.start()].count("\n"),
snippet="fly deploy",
message=CLAUDE_PLUGIN_FLY_DEPLOY_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-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 executable `terraform apply`, `claude-plugin-helm-install-command` for executable `helm install` (closed literal here-document payloads, quoted/commented prose, reporting builtins, and assignment values are negative boundaries; commands after those boundaries remain positive), `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-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-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: 4 additions & 2 deletions docs/sast-dast-rule-research.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,16 @@ files being scanned, then applies the union of relevant checks. Examples:
`claude-plugin-kubectl-apply-command` for ``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``, and
`claude-plugin-helm-install-command` for ``helm install``,
`claude-plugin-vercel-deploy-command` for ``vercel deploy``,
`claude-plugin-fly-deploy-command` for ``fly deploy``, and
`claude-plugin-credential-store-access` for host ``~/.netrc``,
``~/.aws/credentials``, GitHub CLI hosts, Docker auth, cookie jars, and
SSH private keys. Chrome/Firefox profile stores stay
`claude-plugin-browser-profile-access`. Hardcoded PATs stay
`claude-plugin-github-write-token`. ``gh issue create``, ``gh pr review``,
``kubectl get``, ``docker ps``, ``terraform plan``, ``helm list``,
``vercel deploy``, and ``fly deploy`` stay inventory.
``vercel ls``, and ``fly status`` 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