Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions CHANGELOG.d/1099-claude-plugin-supply-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,16 @@
``helm install`` fails as `claude-plugin-helm-install-command`.
``vercel deploy`` fails as `claude-plugin-vercel-deploy-command`.
``fly deploy`` and ``flyctl deploy`` fail as
`claude-plugin-fly-deploy-command`. Hook comments and
`claude-plugin-fly-deploy-command`. ``aws cloudformation deploy``
and ``aws deploy create-deployment`` fail as
`claude-plugin-aws-deploy-command`. ``gcloud run|app|functions
deploy`` fails as `claude-plugin-gcloud-deploy-command`.
``az webapp deploy`` fails as `claude-plugin-az-deploy-command`.
Hook comments and
``echo``/``printf`` lookalikes are not those classes.
``terraform plan``, ``helm list``,
``vercel ls``, and ``fly status``
``vercel ls``, ``fly status``, ``aws s3 ls``, ``gcloud config list``,
and ``az account show``
stay inventory. Hardcoded
PATs stay `claude-plugin-github-write-token`. Snippets are command
labels, not tokens.
Expand Down
101 changes: 97 additions & 4 deletions appguardrail_core/claude_plugin_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,22 @@
deployment-write command findings. Hook or manifest ``terraform apply``
and ``helm install`` fail closed as infra-write command findings.
Hook or manifest ``vercel deploy`` and ``fly deploy`` fail closed as
hosted-deploy command findings. Unquoted ``#`` comments and
hosted-deploy command findings. Hook or manifest ``aws cloudformation
deploy``, ``aws deploy create-deployment``, ``gcloud run|app|functions
deploy``, and ``az webapp deploy`` fail closed as cloud-deploy command
findings. Unquoted ``#`` comments and
``echo``/``printf``/``print`` lookalikes are not that class.
``terraform plan``, ``helm list``, ``vercel ls``, and ``fly status``
``terraform plan``, ``helm list``, ``vercel ls``, ``fly status``,
``aws s3 ls``, ``gcloud config list``, and ``az account show``
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``, ``helm list``, ``vercel ls``, and ``fly status``
``terraform plan``, ``helm list``, ``vercel ls``, ``fly status``,
``aws s3 ls``, ``gcloud config list``, and ``az account show``
stay inventory. Skill
homoglyph, injection, exfiltration, and placeholder hits reuse #1036 rule
identities. Skill, command, or agent text that hides tool use, rewrites
Expand Down Expand Up @@ -278,6 +283,21 @@
"hosted platform is write authority. Remove the command. "
"[CWE-250 - Execution with Unnecessary Privileges]"
)
CLAUDE_PLUGIN_AWS_DEPLOY_COMMAND_MESSAGE: Final = (
"Claude plugin hook or manifest runs AWS deploy writes. CloudFormation "
"deploy and CodeDeploy create-deployment are write authority. Remove "
"the command. [CWE-269 - Improper Privilege Management]"
)
CLAUDE_PLUGIN_GCLOUD_DEPLOY_COMMAND_MESSAGE: Final = (
"Claude plugin hook or manifest runs gcloud deploy. Publishing Cloud "
"Run, App Engine, or Functions is write authority. Remove the command. "
"[CWE-250 - Execution with Unnecessary Privileges]"
)
CLAUDE_PLUGIN_AZ_DEPLOY_COMMAND_MESSAGE: Final = (
"Claude plugin hook or manifest runs az webapp deploy. Publishing an "
"Azure web app is write authority. Remove the command. "
"[CWE-269 - Improper Privilege Management]"
)
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 @@ -413,6 +433,15 @@
)
_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)
_AWS_DEPLOY_COMMAND = re.compile(
r"\baws\s+(?:cloudformation\s+deploy|deploy\s+create-deployment)\b",
re.IGNORECASE,
)
_GCLOUD_DEPLOY_COMMAND = re.compile(
r"\bgcloud\s+(?P<service>run|app|functions)\s+deploy\b",
re.IGNORECASE,
)
_AZ_DEPLOY_COMMAND = re.compile(r"\baz\s+webapp\s+deploy\b", re.IGNORECASE)
_REPORTING_BUILTINS: Final = frozenset(
{":", "echo", "false", "print", "printf", "true"}
)
Expand Down Expand Up @@ -677,7 +706,9 @@
"deployment_write",
re.compile(
r"\b(?:kubectl\s+apply|terraform\s+apply|helm\s+install|"
r"vercel\s+deploy|fly(?:ctl)?\s+deploy|docker\s+push)\b",
r"vercel\s+deploy|fly(?:ctl)?\s+deploy|docker\s+push|"
r"aws\s+(?:cloudformation\s+deploy|deploy\s+create-deployment)|"
r"gcloud\s+(?:run|app|functions)\s+deploy|az\s+webapp\s+deploy)\b",
re.IGNORECASE,
),
),
Expand Down Expand Up @@ -933,6 +964,9 @@ def inspect_claude_plugin_file(
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(_aws_deploy_command_hits(content, manifest=manifest))
hits.extend(_gcloud_deploy_command_hits(content, manifest=manifest))
hits.extend(_az_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 @@ -2421,6 +2455,65 @@ def _fly_deploy_command_hits(
return ()


def _aws_deploy_command_hits(
content: str, *, manifest: bool = False
) -> tuple[PluginHit, ...]:
"""Return AWS deploy-write findings with a command label, not secrets."""
for source, first_line in _hosted_command_sources(content, manifest=manifest):
match = _executable_command_match(source, _AWS_DEPLOY_COMMAND)
if match is None:
continue
snippet = " ".join(match.group(0).lower().split())
return (
PluginHit(
rule_id="claude-plugin-aws-deploy-command",
line=first_line + source[: match.start()].count("\n"),
snippet=snippet,
message=CLAUDE_PLUGIN_AWS_DEPLOY_COMMAND_MESSAGE,
),
)
return ()


def _gcloud_deploy_command_hits(
content: str, *, manifest: bool = False
) -> tuple[PluginHit, ...]:
"""Return gcloud deploy findings with a service-qualified command label."""
for source, first_line in _hosted_command_sources(content, manifest=manifest):
match = _executable_command_match(source, _GCLOUD_DEPLOY_COMMAND)
if match is None:
continue
service = match.group("service").lower()
return (
PluginHit(
rule_id="claude-plugin-gcloud-deploy-command",
line=first_line + source[: match.start()].count("\n"),
snippet="gcloud " + service + " deploy",
message=CLAUDE_PLUGIN_GCLOUD_DEPLOY_COMMAND_MESSAGE,
),
)
return ()


def _az_deploy_command_hits(
content: str, *, manifest: bool = False
) -> tuple[PluginHit, ...]:
"""Return Azure webapp deploy findings with a command label, not names."""
for source, first_line in _hosted_command_sources(content, manifest=manifest):
match = _executable_command_match(source, _AZ_DEPLOY_COMMAND)
if match is None:
continue
return (
PluginHit(
rule_id="claude-plugin-az-deploy-command",
line=first_line + source[: match.start()].count("\n"),
snippet="az webapp deploy",
message=CLAUDE_PLUGIN_AZ_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-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-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-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
8 changes: 6 additions & 2 deletions docs/sast-dast-rule-research.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,18 @@ files being scanned, then applies the union of relevant checks. Examples:
`claude-plugin-terraform-apply-command` for ``terraform apply``,
`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-fly-deploy-command` for ``fly deploy``,
`claude-plugin-aws-deploy-command` for ``aws cloudformation deploy``,
`claude-plugin-gcloud-deploy-command` for ``gcloud run deploy``,
`claude-plugin-az-deploy-command` for ``az webapp 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 ls``, and ``fly status`` stay inventory.
``vercel ls``, ``fly status``, ``aws s3 ls``, ``gcloud config list``,
and ``az account show`` 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