cisco-ai-defense/ defenseclaw
AgentSecurity Governance for Agentic AI
Trust score
Scanned Oct. 15th, 2026 at 14f245d against 34 rules · How scoring works
At a glance
- Repository-wide3 of 15
- mcp-server (MCP server)2 of 11
What to fix first
5 issues, most severe first. Each one points to the exact code, with the recommended fix and a prompt you can paste into your coding agent.
- 1CriticalOAuth 2.1 + PKCE Correctness (MCP Authorization)mcp-server (MCP server) · ATR-M0005The gateway sidecar's management API is a configurable TCP HTTP surface (not stdio-only); ATR-M0005 applies to any remote HTTP transport and treats bearer-only authorization without OAuth 2.1 + PKCE as vulnerable. This component authenticates every management request with a single shared bearer token (see ATR-M0010) and exposes no OAuth protected-resource metadata endpoint, no authorization-code flow, and no PKCE implementation anywhere in the codebase (confirmed by repo-wide grep for oauth/PKCE/code_verifier, finding zero hits in internal/gateway). Because the component serves a remote-capable HTTP transport without implementing the MCP Authorization spec's OAuth 2.1 + PKCE requirement, the rule's remote-transport condition is violated rather than inapplicable.
// APIBindHost returns the address the gateway REST API listens on: an explicit // gateway.api_bind, else loopback. Every listener, hook/plugin address, and // health probe (upgrade, watchdog, status, the Python CLI's api_bind_host) // derives the API host from here so they cannot disagree. func APIBindHost(cfg *Config) string { if cfg == nil || cfg.Gateway.APIBind == "" { return "127.0.0.1" } return cfg.Gateway.APIBindShow 2 more locations
serveTCP := func(ln net.Listener) { go func() { fmt.Fprintf(os.Stderr, "[sidecar-api] listening on %s\n", a.addr) if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed { errCh <- err } }()// tokenAuth wraps a handler with Bearer token authentication. Management // clients may use Authorization, X-DefenseClaw-Token, or the proxy-compatible // X-DC-Auth header; all are compared against the same gateway token. // GET /health is exempt to allow unauthenticated health checks. In the // standalone profile the listener proof route answers before authentication // and never reaches next. func (a *APIServer) tokenAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/health" && r.Method == http.MethodGet { next.ServeHTTP(w, r) return } if r.URL.Path == connector.UserScopedListenerProofPath && a.userScopedCredentialsRequired() {+1 more location in the finding JSON
Recommended fixImplement MCP Authorization (OAuth 2.1 + PKCE) for the gateway's HTTP management surface -- or restrict it to a transport the rule exempts (e.g., stdio-only) -- rather than relying solely on a shared bearer token for a remote-capable HTTP listener.
- 2HighDependency Risk & Supply ChainDeclared behaviorRepository-wide · ATR-C0007Trivy's completed advisory scan confirms the Go dependency graph declared in go.mod resolves vulnerable module versions, including golang.org/x/net v0.58.0 (CVE-2026-78669, HIGH), golang.org/x/mod v0.38.0 (two HIGH CVEs), and golang.org/x/image v0.43.0 (HIGH CVE-2026-46603). go.mod's only replace directive targets github.com/fsnotify/fsnotify, an unrelated local vendoring fix, so none of the flagged modules are pinned past their vulnerable versions. These modules are shipped in the release binary: golang.org/x/net is a direct/transitive dependency of the gateway's net/http-based HTTP client/server stack (internal/gateway/webhook.go, provider.go, api.go). The finding reflects confirmed presence of vulnerable resolved dependencies in the shipped build; it does not by itself establish that the specific vulnerable code path within x/net is exercised at runtime.
replace github.com/fsnotify/fsnotify => ./third_party/fsnotify
Show 2 more locations
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
+18 more locations in the finding JSON
Recommended fixRun
go get -u golang.org/x/net golang.org/x/mod golang.org/x/image golang.org/x/cryptoand update go.opentelemetry.io/otel/exporters/otlp/* to patched releases, thengo mod tidyand re-run trivy to confirm the HIGH findings clear. - 3MediumDoS Protections: Timeouts, Concurrency, Streaming LimitsRepository-wide · ATR-C0004The gateway's primary REST management API listener (internal/gateway/api.go) is constructed via a plain http.Server with no ReadHeaderTimeout, ReadTimeout, WriteTimeout, or IdleTimeout configured. APIBindHost() permits an explicitly configured non-loopback bind address, and while other paths (webhook dispatcher, secureDialContext, handleMCPScan, openshell CLI invocations) do apply timeouts and bounds, the main listener itself does not bound request or connection duration. The existing request-body-size middleware (apiBodyLimitMiddleware) only activates after headers are received and does not limit how long a slow client can hold open a connection or trickle a request, so a client can retain server resources (connections/goroutines) for an extended period, exhausting available handler capacity.
// gateway.api_bind, else loopback. Every listener, hook/plugin address, and // health probe (upgrade, watchdog, status, the Python CLI's api_bind_host) // derives the API host from here so they cannot disagree. func APIBindHost(cfg *Config) string { if cfg == nil || cfg.Gateway.APIBind == "" { return "127.0.0.1" } return cfg.Gateway.APIBind }Show 2 more locations
srv := &http.Server{ Addr: a.addr, Handler: handler, BaseContext: func(_ net.Listener) context.Context { return baseCtx }, ConnContext: acpPeerConnContext, } // Bind with a short retry instead of a bare ListenAndServe. Duringfunc bodyLimitMiddleware(next http.Handler, maxBytes func(*http.Request) int64) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch { r.Body = http.MaxBytesReader(w, r.Body, maxBytes(r)) } next.ServeHTTP(w, r) }) }Recommended fixSet ReadHeaderTimeout, ReadTimeout, WriteTimeout, and IdleTimeout on the http.Server instances in internal/gateway/api.go and internal/gateway/proxy.go to bound slow-client connections, in addition to the existing body-size and per-handler timeouts.
- 4MediumDenial of Wallet / Cost ControlsRepository-wide · ATR-C0012The LLM-judge invocation path (internal/gateway/llm_judge.go, inspect.go) bounds the byte size of each individual request (maxToolJudgeArgumentBytes, maxToolJudgePriorEvents, etc.) and limits in-flight concurrency to 16 simultaneous calls, but it imposes no limit on the total number or cadence of sequential paid-provider invocations: each eligible, varied tool-call sample triggers its own call to the configured LLM provider with no per-user, per-session, or per-day quota, and no spend budget or call-rate limiter. An agent capable of generating a long stream of distinct tool-call arguments can therefore drive an unbounded number of sequential billed provider requests.
// The generic /inspect handlers run under a 200ms deadline // (inspectScanTimeout) — no judge round-trip fits, and blowing // that deadline would 504 the whole verdict. Skip unless the // caller's remaining budget can plausibly carry an LLM call. The // tool lanes pass a deadline-free context.Background() so they are // never gated out here (J3-3c). if dl, ok := ctx.Deadline(); ok && time.Until(dl) < time.Second { return nil } a.hookJudgeSemOnce.Do(func() { a.hookJudgeSem = make(chan struct{}, maxConcurrentHookJudges) }) select { case a.hookJudgeSem <- struct{}{}: default: …Show 2 more locations
func (j *LLMJudge) RunToolJudge(ctx context.Context, toolName, args string) *ScanVerdict { sample, eligible := j.prepareToolJudgeSample(ctx, toolName, args) if !eligible { return allowVerdict("llm-judge-tool") } return j.runToolJudgeSample(ctx, toolName, args, sample) } // prepareToolJudgeSample records the call in arrival order and returns an // immutable sample. EventRouter calls this before launching asynchronous // provider work so concurrent calls from one session cannot reorder history. func (j *LLMJudge) prepareToolJudgeSample(ctx context.Context, toolName, args string) (string, bool) { if j == nil || j.cfg == nil { return "", false …} llmCtx, judgeTrace := j.startJudgeTrace( ctx, kind, "tool_call", maxTok, messages, time.Now().UTC(), ) defer judgeTrace.Abort() start := time.Now() resp, err := j.provider.ChatCompletion(llmCtx, j.judgeChatRequest(messages, maxTok, kind)) latencyMs := time.Since(start).Milliseconds() rawResponse := ""
+1 more location in the finding JSON
Recommended fixAdd a per-user/session request-rate limiter and/or a spend budget (daily call count or cost ceiling) on the tool-judge dispatch path in internal/gateway/llm_judge.go, so that a sustained stream of distinct tool calls cannot generate unbounded billed provider invocations.
- 5MediumTransport Security: TLS Everywhere (Remote)mcp-server (MCP server) · ATR-M0009The gateway's HTTP servers (management API on gateway.api_port, guardrail proxy on guardrail.port) are created with plain net/http.Server and no crypto/tls.Config, certificate, or ListenAndServeTLS call anywhere in internal/gateway. There is no TLS option exposed in config.yaml for these inbound listeners. By default both bind loopback-only, which is the primary mitigating control, but an operator who sets gateway.api_bind or guardrail.host to a non-loopback address (permitted on unmanaged/BYOD installs; only managed_enterprise is hard-blocked to 127.0.0.1) gets a plaintext HTTP listener carrying a bearer token in Authorization headers with no transport-encryption option.
srv := &http.Server{ Addr: a.addr, Handler: handler, BaseContext: func(_ net.Listener) context.Context { return baseCtx }, ConnContext: acpPeerConnContext, }Show 2 more locations
srv := &http.Server{Addr: addr, Handler: handler} errCh := make(chan error, 1) go func() { errCh <- srv.ListenAndServe() }()// validateManagedEnterpriseListenerBindings keeps every inbound enterprise // surface on loopback. ... This is enterprise-only: // canonical numeric IPv4, so the API listener must be exactly 127.0.0.1 rather // than another loopback spelling or address. This is enterprise-only: // unmanaged/BYOD deployments retain their existing remote-bind behavior. func validateManagedEnterpriseListenerBindings(cfg *Config) error { if cfg == nil || !managed.IsManagedEnterprise(cfg.DeploymentMode) {+1 more location in the finding JSON
Recommended fixAdd an optional TLS listener (cert/key config keys) for gateway.api_bind and guardrail.host, and require TLS (or refuse to start) whenever either is configured to a non-loopback address outside managed_enterprise.
What’s inside
BlueRock found 6 components in this repository. Each is checked against the rules for its type.
MCP servermcp-server.47Low trust1 critical1 medium
Its tools can
What stood out
- HIGH: The Go gateway/sidecar's HTTP management API and guardrail proxy listeners (internal/gateway/api.go, internal/gateway/proxy.go) are plain net/http servers with no TLS configuration surface anywhere in the codebase. An operator on an unmanaged/BYOD install who points gateway.api_bind or guardrail.host at a non-loopback address gets a cleartext channel carrying the shared management bearer token with no code path able to enable TLS to protect it (ATR-M0009, HIGH, VULNERABLE).
- HIGH: golang.org/x/net is pinned (go.sum) to a version trivy flags with a HIGH-severity CVE (CVE-2026-78669), underlying the gateway's own net/http client/server stack -- no replace/exclude directive upgrades past it, so the vulnerable code ships in the release binary (ATR-C0007 VULNERABLE).
- UNKNOWN: gosec flagged 10 unsafe-deserialization-interface sites and semgrep flagged dozens of Go ResponseWriter-write sites requiring deeper per-site source-to-sink tracing to resolve (ATR-C0009 Insecure Deserialization, ATR-C0010 Output Encoding).
- The repository's actual MCP-relevant attack surface is the Go gateway/sidecar's local REST management API rather than a classic tool-registering MCP server; most MCP-tool-specific rules (ATR-M0001/0002/0005/0006/0007/0011) are NOT_APPLICABLE as architected, though auth, CSRF, and origin handling on the API itself are strong.
- cmd_setup.py's _show_splunk_credentials() prints locally generated secrets to the terminal without an explicit --reveal/confirm flag, creating terminal-capture exposure risk.
MCP server rules 2 of 11 flagged
4 rules passed · 5 did not applyShow
PassedNo Authentication or Authorization Checks (General)ATR-M0010Details
Why it got this result
Every route registered in APIServer.Run is wrapped by tokenAuth, which requires a bearer credential (Authorization: Bearer / X-DefenseClaw-Token / X-DC-Auth) compared via constant-time SHA-256 digest comparison (constantTimeStringMatch) against the gateway token generated with crypto/rand at first boot (EnsureGatewayToken). The only unauthenticated route is GET /health. Fail-open is explicitly rejected: tokenAuth fails CLOSED (503) when no token is configured rather than falling back to 'trust loopback' (the code comment calls out that this was a prior local-IDOR risk it fixed). Default bind is loopback (127.0.0.1); a non-default remote bind still requires the same token. Scoped credential paths (OTLP, hook, ACP) each perform their own secret comparison (constantTimeStringMatch / HMAC) rather than accepting an identifier alone.
Why it matters
Prevents unauthorized tool use and data access.
Evidence
func (a *APIServer) tokenAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/health" && r.Method == http.MethodGet {
next.ServeHTTP(w, r)
return
}Show 2 more locations
handler := apiBodyLimitMiddleware(mux, apiRequestBodyMaxBytes, otlpRequestBodyMaxBytes) handler = a.apiCSRFProtect(handler) handler = a.tokenAuth(handler)
expected := ""
if a.scannerCfg != nil {
expected = a.scannerCfg.Gateway.Token
}
if expected == "" {
// Fail closed when no token is configured. ...
a.emitHTTPAuthFailure(ctx, r, route, gatewaylog.ErrCodeAuthMissingToken, "no_token_configured")
http.Error(w, `{"error":"sidecar misconfigured: no gateway token"}`, http.StatusServiceUnavailable)
return
}+2 more locations in the finding JSON
PassedCORS / Origin / Host Validation (DNS Rebinding Defense)ATR-M0003Details
Why it got this result
The gateway sidecar's HTTP management API (internal/gateway/api.go) binds loopback by default (viper default gateway.api_port with APIBindHost() returning 127.0.0.1 when unset) and validates Origin/Sec-Fetch-Site/Content-Type/X-DefenseClaw-Client on every mutating request. apiCSRFProtect rejects any Origin that is not localhost/127.0.0.1/::1 and rejects cross-site Sec-Fetch-Site values outright, which defeats DNS-rebinding/browser-hijack attempts even if a non-default bind were configured.
Why it matters
Prevents DNS rebinding/browser hijack of local or remote MCP.
Evidence
func (a *APIServer) apiCSRFProtect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
...
if sfs := r.Header.Get("Sec-Fetch-Site"); sfs != "" {
if sfs != "same-origin" && sfs != "none" {
a.emitHTTPAuthFailure(ctx, r, route, gatewaylog.ErrCodeAuthCSRFMismatch, "sec_fetch_site_rejected")
http.Error(w, `{"error":"cross-site request rejected"}`, http.StatusForbidden)
return
}
}Show 2 more locations
func isLocalhostOrigin(origin string) bool {
u, err := url.Parse(origin)
if err != nil {
return false
}
host := u.Hostname()
return host == "127.0.0.1" || host == "localhost" || host == "::1"
}func APIBindHost(cfg *Config) string {
if cfg == nil || cfg.Gateway.APIBind == "" {
return "127.0.0.1"
}
return cfg.Gateway.APIBind
}PassedBinding to 0.0.0.0 Without Auth/TLSATR-M0004Details
Why it got this result
Default bind for both the management API (gateway.api_bind) and the guardrail proxy (guardrail.host) is 127.0.0.1; guardrailListenAddr() canonicalizes any loopback-spelled host to 127.0.0.1. Every mutating / sensitive route requires a bearer token (tokenAuth) compared with constant-time SHA-256 compare, so even an operator-chosen non-default bind is not auth-less. Managed-enterprise deployments additionally hard-fail config load if the bind is not exactly 127.0.0.1 (validateManagedEnterpriseListenerBindings).
Why it matters
LAN/Internet exposure enables remote exploitation.
Evidence
func guardrailListenAddr(port int, effectiveHost string) string {
h := strings.TrimSpace(effectiveHost)
if h == "" {
h = "localhost"
}
switch strings.ToLower(h) {
case "localhost", "127.0.0.1", "::1", "[::1]":
return fmt.Sprintf("127.0.0.1:%d", port)
default:
return fmt.Sprintf("%s:%d", h, port)
}
}Show 2 more locations
func validateManagedEnterpriseListenerBindings(cfg *Config) error {
if cfg == nil || !managed.IsManagedEnterprise(cfg.DeploymentMode) {
return nil
}
apiBind := cfg.Gateway.APIBind
if apiBind == "" {
cfg.Gateway.APIBind = "127.0.0.1"
apiBind = cfg.Gateway.APIBind
}
if apiBind != "127.0.0.1" {
return fmt.Errorf(
"config: managed_enterprise gateway API must bind to exact canonical 127.0.0.1, got %q",
apiBind,
)
… expected := ""
if a.scannerCfg != nil {
expected = a.scannerCfg.Gateway.Token
}
if expected == "" {
a.emitHTTPAuthFailure(ctx, r, route, gatewaylog.ErrCodeAuthMissingToken, "no_token_configured")
http.Error(w, `{"error":"sidecar misconfigured: no gateway token"}`, http.StatusServiceUnavailable)
return
}PassedCORS Misuse: Wildcard with CredentialsATR-M0008Details
Why it got this result
No Access-Control-Allow-Origin header is ever set by the gateway (grepped repo-wide: zero occurrences in internal/gateway/*.go outside tests/vendor), so there is no wildcard-with-credentials CORS configuration to misuse. The gateway instead rejects any non-localhost Origin outright (see ATR-M0003 evidence) rather than reflecting/wildcarding it.
Why it matters
Prevents credentialed cross-origin abuse.
Evidence
if origin := r.Header.Get("Origin"); origin != "" {
if !isLocalhostOrigin(origin) {
a.emitHTTPAuthFailure(ctx, r, route, gatewaylog.ErrCodeAuthOriginBlocked, "origin_blocked")
http.Error(w, `{"error":"non-localhost Origin rejected"}`, http.StatusForbidden)
return
}
}Does not applyOAuth Proxy Per-Client Consent (Confused Deputy Defense)ATR-M0011Details
Why it got this result
The gateway does not implement an OAuth proxy / confused-deputy-prone token-exchange pattern on behalf of multiple downstream MCP clients; it has no OAuth client-registration or token-issuance code at all (see ATR-M0005). There is therefore no per-client consent surface for this rule to evaluate.
Why it matters
Shared upstream client + dynamic MCP-client registration + no per-client consent enables attacker-registered MCP clients to receive bridge codes carrying victim upstream tokens — the documented MCP Confused Deputy attack.
Evidence
func (a *APIServer) Run(ctx context.Context) error {
mux := http.NewServeMux()
mux.HandleFunc(enterprisepolicy.ForeignHookSessionPathPrefix+"{connector}", a.handleForeignHookSession)Does not applyTool Definition Integrity & Versioning (ETDI-style)ATR-M0001Details
Why it got this result
The gateway sidecar does not itself register/serve MCP tool definitions to an LLM client (no @mcp.tool/registerTool/server.tool pattern anywhere in the repo, confirmed by repo-wide grep). It is a REST management/inspection API consumed by DefenseClaw's own hook scripts, CLI, and TUI -- its job is to scan and govern OTHER agents' MCP tool definitions (via /v1/mcp/scan), not to publish its own tool manifest to a model. There is therefore no tool-definition hot-reload/signature surface for this rule to evaluate on this component.
Why it matters
Prevents rug-pull tool poisoning.
Evidence
func (a *APIServer) handleToolsCatalog(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
returnDoes not applyUnrestricted Tool Permissions (Overprivileged Tools)ATR-M0002Details
Why it got this result
This component exposes no MCP tools of its own to an LLM client (see ATR-M0001); its REST endpoints are a management surface for DefenseClaw's own CLI/hooks/TUI, all gated by a single shared bearer token with per-route scoping (hook-scoped, ACP-scoped, OTLP-scoped credentials) rather than per-tool capability scoping. The rule's 'overprivileged tool' model (generic exec tool reachable by any request) does not map onto this architecture.
Why it matters
Limits blast radius and privilege escalation.
Evidence
func (a *APIServer) Run(ctx context.Context) error {
mux := http.NewServeMux()
mux.HandleFunc(enterprisepolicy.ForeignHookSessionPathPrefix+"{connector}", a.handleForeignHookSession)Does not applyTool Name Collision / SpoofingATR-M0006Details
Why it got this result
This component publishes no MCP tool names to a model (see ATR-M0001); there is no tool namespace for a spoofing/collision attack to target. (Note: DefenseClaw's product purpose elsewhere is precisely to detect tool-name collision/spoofing in OTHER agents' MCP servers via its scanner, e.g. cli/defenseclaw/scanner/mcp.py, but that is the scanner CLI/feature, not this component's own tool surface.)
Why it matters
Prevents malicious tool shadowing.
Evidence
func (a *APIServer) handleMCPs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)Does not applyPrompt/Metadata Injection in Tool DescriptionsATR-M0007Details
Why it got this result
This component has no tool descriptions of its own presented to a model (see ATR-M0001); there is no tool-description metadata surface for prompt injection to target on this component.
Why it matters
Prevents covert model manipulation.
Evidence
func (a *APIServer) Run(ctx context.Context) error {
mux := http.NewServeMux()
mux.HandleFunc(enterprisepolicy.ForeignHookSessionPathPrefix+"{connector}", a.handleForeignHookSession)Show its 22 tools
| Tool | It can |
|---|---|
handleCodeScan (POST /api/v1/scan/code)Scans a code file path for CodeGuard rule violations (secrets, exec/SQL injection patterns, etc.); backs the codeguard skill's self-check. | Read data |
handleConfigPatch (POST /config/patch)Patches live gateway config (Secure Client profile only). | Create or change dataChange settings |
handleEnforceAllow (POST /enforce/allow)Adds an asset to the policy allow list. | Create or change dataChange settings |
handleEnforceBlock (POST /enforce/block)Adds an asset (skill/MCP/plugin) to the policy block list. | Create or change dataChange settings |
handleHealth (GET /health)Unauthenticated liveness probe for the gateway sidecar. | Read data |
handleInspectRequest (POST /api/v1/inspect/request)Guardrail inspection of an outbound LLM request body. | Read data |
handleInspectResponse (POST /api/v1/inspect/response)Guardrail inspection of an LLM response body. | Read data |
handleInspectTool (POST /api/v1/inspect/tool)Guardrail inspection of an agent tool-call request before it executes; returns an allow/alert/confirm/block verdict. | Read data |
handleInspectToolResponse (POST /api/v1/inspect/tool-response)Guardrail inspection of a tool-call result. | Read data |
handleMCPScan (POST /v1/mcp/scan)Triggers a security scan of a target MCP server (via the cisco-ai-mcp-scanner SDK) and returns findings. | Read dataReach any network address |
handleMCPs (GET /mcps)Returns the configured MCP server list read from the connector config (inventory only, no mutation). | Read data |
handlePluginDisable (POST /plugin/disable)Disables a plugin via the OpenClaw gateway bridge. | Create or change dataChange settings |
handlePluginEnable (POST /plugin/enable)Enables a plugin via the OpenClaw gateway bridge. | Create or change dataChange settings |
handlePluginScan (POST /v1/plugin/scan)Triggers a security scan of a plugin artifact. | Read data |
handlePolicyEvaluate (POST /policy/evaluate)Evaluates the current policy against supplied facts and returns a decision. | Read data |
handlePolicyReload (POST /policy/reload)Hot-reloads the gateway's policy/config from disk. | Change settings |
handleShutdown (POST /api/v1/admin/shutdown)Requests the sidecar process to shut down. | Change settingsDelete or overwrite data |
handleSkillDisable (POST /skill/disable)Disables a skill via the OpenClaw gateway bridge. | Create or change dataChange settings |
handleSkillEnable (POST /skill/enable)Enables a skill via the OpenClaw gateway bridge. | Create or change dataChange settings |
handleSkillScan (POST /v1/skill/scan)Triggers a security scan of a skill artifact. | Read data |
handleToolsCatalog (GET /tools/catalog)Proxies the upstream OpenClaw gateway's tool catalog read-only. | Read data |
registerSandboxRoutes (/api/v1/sandbox/*)REST API for managing OpenShell sandboxes (create/start/stop/inspect sandboxed agent execution environments). | Create or change dataChange settingsDelete or overwrite dataRun code or commands |
Before you put it to work
- Add an optional TLS listener (certificate/key config) for gateway.api_bind and guardrail.host, and require TLS or refuse to start whenever either is configured to a non-loopback address outside managed_enterprise, closing the one remaining gap on the gateway's HTTP surface (ATR-M0009).
- Document and consider hard-enforcing that gateway.api_bind/guardrail.host must stay loopback unless the operator has independently firewalled the host, since no in-process control currently prevents a non-loopback bind on unmanaged/BYOD installs.
- Upgrade golang.org/x/net, golang.org/x/mod, golang.org/x/image, golang.org/x/crypto, and go.opentelemetry.io/otel/exporters/otlp/* to patched versions (go get -u && go mod tidy) to clear the HIGH-severity trivy CVE findings (ATR-C0007).
- Complete deeper per-site tracing of the 10 gosec go-unsafe-deserialization-interface findings and the Go ResponseWriter-write semgrep findings to resolve the UNKNOWN verdicts (ATR-C0009, ATR-C0010).
- Consider gating cmd_setup.py's _show_splunk_credentials() behind an explicit --reveal/confirm flag to reduce terminal-capture exposure of locally generated secrets.
- No action needed on the evaluated skill/plugin artifacts (codeguard, defenseclaw-add-connector, defenseclaw-develop, defenseclaw-tui, rulepack_artifact_parity); maintain existing check_skill_fresh.py-style drift checks and PR review to preserve their no-exec/no-network/no-concealment posture.
Skillskills-plugins/.claude/skills/defenseclaw-add-connector.claude/skills/defenseclaw-add-connector100High trustNone flagged
What stood out
- HIGH: The Go gateway/sidecar's HTTP management API and guardrail proxy listeners (internal/gateway/api.go, internal/gateway/proxy.go) are plain net/http servers with no TLS configuration surface anywhere in the codebase. An operator on an unmanaged/BYOD install who points gateway.api_bind or guardrail.host at a non-loopback address gets a cleartext channel carrying the shared management bearer token with no code path able to enable TLS to protect it (ATR-M0009, HIGH, VULNERABLE).
- HIGH: golang.org/x/net is pinned (go.sum) to a version trivy flags with a HIGH-severity CVE (CVE-2026-78669), underlying the gateway's own net/http client/server stack -- no replace/exclude directive upgrades past it, so the vulnerable code ships in the release binary (ATR-C0007 VULNERABLE).
- UNKNOWN: gosec flagged 10 unsafe-deserialization-interface sites and semgrep flagged dozens of Go ResponseWriter-write sites requiring deeper per-site source-to-sink tracing to resolve (ATR-C0009 Insecure Deserialization, ATR-C0010 Output Encoding).
- The repository's actual MCP-relevant attack surface is the Go gateway/sidecar's local REST management API rather than a classic tool-registering MCP server; most MCP-tool-specific rules (ATR-M0001/0002/0005/0006/0007/0011) are NOT_APPLICABLE as architected, though auth, CSRF, and origin handling on the API itself are strong.
- cmd_setup.py's _show_splunk_credentials() prints locally generated secrets to the terminal without an explicit --reveal/confirm flag, creating terminal-capture exposure risk.
Skill rules 0 of 8 flagged
6 rules passed · 2 did not applyShow
PassedCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
This is a documentation-only contributor skill (SKILL.md + reference/*.md, no bundled scripts) describing how to add a DefenseClaw connector. It bundles no executable code at all, so there is no undisclosed runtime capability to evaluate -- its only 'behavior' is the text itself, which matches its declared purpose (engineering guidance).
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
--- name: defenseclaw-add-connector description: Use when adding or changing a DefenseClaw connector for a new AI agent ... ---
PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
The instruction body describes engineering rules for adding connectors (fail-closed behavior, enterprise guard requirements, never bypassing admin-owned hook paths); it never instructs the agent to suppress a confirmation, hide an action from the user, or subvert a guardrail -- if anything it repeatedly insists on fail-closed, admin-owned gates (e.g. rule 10: 'a guarded connector must run the admin-owned ... never a user-writable script').
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
10. **Enterprise guard:** a guarded connector must run the admin-owned `defenseclaw-hook ... --enterprise-managed` (or call `--foreign-hook-check`), never a user-writable script (Devin, `0f728f8b`).
PassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
No manifest permission/capability declaration exists beyond the SKILL.md frontmatter name/description (no ai-plugin.json-style permission list); it ships no executable code, so there is no capability for the declared scope to over-reach.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
--- name: defenseclaw-add-connector description: Use when adding or changing a DefenseClaw connector for a new AI agent ... ---
PassedInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
Scanned SKILL.md and all reference/*.md files for concealment codepoints (zero-width, bidi override, BOM mid-content) -- none found. No 'ignore previous instructions'-style injection language present.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
--- name: defenseclaw-add-connector description: Use when adding or changing a DefenseClaw connector for a new AI agent (a vendor command hook, in-agent plugin, policy bridge, LLM proxy or ACP route). ... ---
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
Shipped as static markdown inside the repo's own version control; no runtime instruction fetch, no self-updater, no external package registry dependency. Changes are reviewed through the same PR process as the rest of the codebase.
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
Verified against `fix/enterprise-hardening` at `fc9e12a2`. The references name files and symbols, not line numbers. ...
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
First-party contributor skill named after the project itself ('defenseclaw-add-connector'); no third-party brand impersonation.
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
name: defenseclaw-add-connector
Does not applyHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
This skill is documentation-only (SKILL.md + reference markdown, no bundled code) and implements no host-credential or conversation/memory read mechanism; ATR-P0003 defines applicability by the existence of such a harvest step, which this artifact does not have, so the rule does not apply to it.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
--- name: defenseclaw-add-connector description: Use when adding or changing a DefenseClaw connector ... ---
Does not applyFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
This skill ships no scripts, hooks, entry code, host configuration, dotenv files, or exec-enabling assignments -- only SKILL.md and reference markdown. The documented 'defenseclaw setup' flow describes the separate DefenseClaw product's own setup tooling, not a format-native execution or persistence primitive shipped by this skill itself, so the rule does not apply.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
defenseclaw setup <x> (Python) defenseclaw-gateway (Go) contract + platform check (hook_contracts.json) writes config.yaml + <data_dir>/picked_connector restarts the gateway ----------------------------> Connector.Setup(SetupOpts)
Before you put it to work
- Add an optional TLS listener (certificate/key config) for gateway.api_bind and guardrail.host, and require TLS or refuse to start whenever either is configured to a non-loopback address outside managed_enterprise, closing the one remaining gap on the gateway's HTTP surface (ATR-M0009).
- Document and consider hard-enforcing that gateway.api_bind/guardrail.host must stay loopback unless the operator has independently firewalled the host, since no in-process control currently prevents a non-loopback bind on unmanaged/BYOD installs.
- Upgrade golang.org/x/net, golang.org/x/mod, golang.org/x/image, golang.org/x/crypto, and go.opentelemetry.io/otel/exporters/otlp/* to patched versions (go get -u && go mod tidy) to clear the HIGH-severity trivy CVE findings (ATR-C0007).
- Complete deeper per-site tracing of the 10 gosec go-unsafe-deserialization-interface findings and the Go ResponseWriter-write semgrep findings to resolve the UNKNOWN verdicts (ATR-C0009, ATR-C0010).
- Consider gating cmd_setup.py's _show_splunk_credentials() behind an explicit --reveal/confirm flag to reduce terminal-capture exposure of locally generated secrets.
- No action needed on the evaluated skill/plugin artifacts (codeguard, defenseclaw-add-connector, defenseclaw-develop, defenseclaw-tui, rulepack_artifact_parity); maintain existing check_skill_fresh.py-style drift checks and PR review to preserve their no-exec/no-network/no-concealment posture.
Skillskills-plugins/.claude/skills/defenseclaw-develop.claude/skills/defenseclaw-develop100High trustNone flagged
Its tools can
What stood out
- HIGH: The Go gateway/sidecar's HTTP management API and guardrail proxy listeners (internal/gateway/api.go, internal/gateway/proxy.go) are plain net/http servers with no TLS configuration surface anywhere in the codebase. An operator on an unmanaged/BYOD install who points gateway.api_bind or guardrail.host at a non-loopback address gets a cleartext channel carrying the shared management bearer token with no code path able to enable TLS to protect it (ATR-M0009, HIGH, VULNERABLE).
- HIGH: golang.org/x/net is pinned (go.sum) to a version trivy flags with a HIGH-severity CVE (CVE-2026-78669), underlying the gateway's own net/http client/server stack -- no replace/exclude directive upgrades past it, so the vulnerable code ships in the release binary (ATR-C0007 VULNERABLE).
- UNKNOWN: gosec flagged 10 unsafe-deserialization-interface sites and semgrep flagged dozens of Go ResponseWriter-write sites requiring deeper per-site source-to-sink tracing to resolve (ATR-C0009 Insecure Deserialization, ATR-C0010 Output Encoding).
- The repository's actual MCP-relevant attack surface is the Go gateway/sidecar's local REST management API rather than a classic tool-registering MCP server; most MCP-tool-specific rules (ATR-M0001/0002/0005/0006/0007/0011) are NOT_APPLICABLE as architected, though auth, CSRF, and origin handling on the API itself are strong.
- cmd_setup.py's _show_splunk_credentials() prints locally generated secrets to the terminal without an explicit --reveal/confirm flag, creating terminal-capture exposure risk.
Skill rules 0 of 8 flagged
7 rules passed · 1 did not applyShow
PassedCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
The only bundled script, scripts/check_skill_fresh.py, performs exactly what its docstring/SKILL.md describe: read-only repo-local checks (git grep, Makefile parsing, markdown scanning) with 'Standard library only, no network' stated and verified by code -- its only subprocess calls are local git grep/git log invocations, no outbound network, no credential access, no eval/exec of remote content.
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
It also enforces the size limits (SKILL.md 200 lines, checklist 340, other reference files 300), the frontmatter name, that every reference file is linked from SKILL.md, and the ID rules: ... Exit codes: 0 no drift, 1 drift, 2 the script could not run. Standard library only, no network.
Show 1 more location
def preload_symbols(root: str, symbols: set[str]) -> None:
"""Find every symbol with one git grep (one process per symbol is too slow)."""
global _FOUND
_FOUND = set()
if not symbols:
return
cmd = ["git", "grep", "-h", "-o", "-F", "-w"]PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
The design principles explicitly mandate the OPPOSITE of consent suppression: 'Fail closed, never silently lenient', 'Managed hosts never allow what policy denies', and a stop-and-ask list for risky changes. No directive instructs the agent to skip user confirmation or hide a privileged action.
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
2. **Fail closed, never silently lenient.** A failed lookup, bad pack, missing scan result or unreachable gateway resolves to the stricter outcome, with a reason code. Removed keys say what replaced them. (POL, ID, CFG) 3. **Managed hosts never allow what policy denies.** ...
PassedFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
check_skill_fresh.py writes nothing to any host config/memory/control-plane file; it only reads the repo tree and prints a pass/fail exit code, with no self-update or persistence mechanism.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
Exit codes: 0 no drift, 1 drift, 2 the script could not run. Standard library only, no network.
PassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
No formal permission manifest; the sole bundled script's capability (local git/make subprocess calls, no network) is intrinsic to and narrower than its declared purpose of verifying the skill's own freshness against the repo.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
#!/usr/bin/env python3 # Copyright 2026 Cisco Systems, Inc. and its affiliates # SPDX-License-Identifier: Apache-2.0 """Check that the defenseclaw-develop skill still matches the repository.
PassedInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
Scanned SKILL.md and all reference/*.md files for concealment codepoints -- none found; no injected override directives present in the instruction body.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
--- name: defenseclaw-develop description: Use before writing or reviewing any DefenseClaw change that is not a new connector ... ---
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
Shipped as static markdown + a local verification script inside the repo's own version control; no runtime instruction fetch or self-updater. The bundled script's own purpose is to DETECT drift between this skill and the repo (anti-rug-pull tooling), reinforcing integrity rather than undermining it.
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
Source code and tests are authoritative. Last full sweep: not yet run (set by the first quarterly sweep). If this skill disagrees with them, trust the code and fix the skill (`reference/maintenance.md`).
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
First-party contributor skill named after the project itself; no third-party brand impersonation.
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
name: defenseclaw-develop
Does not applyHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
The bundled validator (check_skill_fresh.py) reads only repository files and local git/make command output to check skill freshness; it does not read host credentials, SSH keys, cloud metadata, or live conversation/memory context. Because ATR-P0003 is scoped to artifacts with an existing credential/conversation harvest step, and this script has none, the rule's harvest subject does not exist here.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
Standard library only, no network.
Show its 1 tool
| Tool | It can |
|---|---|
check_skill_fresh.pyContributor-facing drift checker: verifies the defenseclaw-develop SKILL.md and reference docs still match the repository (paths exist, make targets defined, symbols found via git grep, size limits). Local-only, no network. | Read data |
Before you put it to work
- Add an optional TLS listener (certificate/key config) for gateway.api_bind and guardrail.host, and require TLS or refuse to start whenever either is configured to a non-loopback address outside managed_enterprise, closing the one remaining gap on the gateway's HTTP surface (ATR-M0009).
- Document and consider hard-enforcing that gateway.api_bind/guardrail.host must stay loopback unless the operator has independently firewalled the host, since no in-process control currently prevents a non-loopback bind on unmanaged/BYOD installs.
- Upgrade golang.org/x/net, golang.org/x/mod, golang.org/x/image, golang.org/x/crypto, and go.opentelemetry.io/otel/exporters/otlp/* to patched versions (go get -u && go mod tidy) to clear the HIGH-severity trivy CVE findings (ATR-C0007).
- Complete deeper per-site tracing of the 10 gosec go-unsafe-deserialization-interface findings and the Go ResponseWriter-write semgrep findings to resolve the UNKNOWN verdicts (ATR-C0009, ATR-C0010).
- Consider gating cmd_setup.py's _show_splunk_credentials() behind an explicit --reveal/confirm flag to reduce terminal-capture exposure of locally generated secrets.
- No action needed on the evaluated skill/plugin artifacts (codeguard, defenseclaw-add-connector, defenseclaw-develop, defenseclaw-tui, rulepack_artifact_parity); maintain existing check_skill_fresh.py-style drift checks and PR review to preserve their no-exec/no-network/no-concealment posture.
Skillskills-plugins/.claude/skills/defenseclaw-tui.claude/skills/defenseclaw-tui100High trustNone flagged
Its tools can
What stood out
- HIGH: The Go gateway/sidecar's HTTP management API and guardrail proxy listeners (internal/gateway/api.go, internal/gateway/proxy.go) are plain net/http servers with no TLS configuration surface anywhere in the codebase. An operator on an unmanaged/BYOD install who points gateway.api_bind or guardrail.host at a non-loopback address gets a cleartext channel carrying the shared management bearer token with no code path able to enable TLS to protect it (ATR-M0009, HIGH, VULNERABLE).
- HIGH: golang.org/x/net is pinned (go.sum) to a version trivy flags with a HIGH-severity CVE (CVE-2026-78669), underlying the gateway's own net/http client/server stack -- no replace/exclude directive upgrades past it, so the vulnerable code ships in the release binary (ATR-C0007 VULNERABLE).
- UNKNOWN: gosec flagged 10 unsafe-deserialization-interface sites and semgrep flagged dozens of Go ResponseWriter-write sites requiring deeper per-site source-to-sink tracing to resolve (ATR-C0009 Insecure Deserialization, ATR-C0010 Output Encoding).
- The repository's actual MCP-relevant attack surface is the Go gateway/sidecar's local REST management API rather than a classic tool-registering MCP server; most MCP-tool-specific rules (ATR-M0001/0002/0005/0006/0007/0011) are NOT_APPLICABLE as architected, though auth, CSRF, and origin handling on the API itself are strong.
- cmd_setup.py's _show_splunk_credentials() prints locally generated secrets to the terminal without an explicit --reveal/confirm flag, creating terminal-capture exposure risk.
Skill rules 0 of 8 flagged
8 rules passedShow
PassedCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
SKILL.md explicitly discloses that the TUI runs the DefenseClaw CLI as a subprocess, and demo.py's --real-exec option is clearly labeled as an opt-in mode that isolates HOME while invoking confirmed CLI commands. This capability is therefore transparent and disclosed rather than a shadow feature: the skill's declared purpose (developing/testing the TUI, which wraps the CLI) matches its actual subprocess-execution behavior.
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
--- name: defenseclaw-tui description: Improve the DefenseClaw Textual TUI (cli/defenseclaw/tui) consistently. ...
PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
The skill's golden rules explicitly REQUIRE confirmation for destructive flows ('Destructive intents set risk="destructive" and go through ConsequenceModalScreen(danger=True)') and forbid the TUI from mutating state outside of previewed CLI intents -- this is a consent-preserving design, not a subversion of it.
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
2. **Mutate only through CLI intents.** Flow: `model.handle_key` -> action/intent -> `_confirm_and_run_intent` -> `CommandPreviewScreen` -> `CommandExecutor` -> `_handle_successful_command` refresh. Destructive intents set `risk="destructive"` and go through `ConsequenceModalScreen(danger=True)`.
PassedHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
Bundled scripts contain no network calls and no credential-file reads (grepped for requests/urllib/socket/os.environ secret patterns -- none found); the skill's own rule 3 explicitly protects secrets ('Send secrets over stdin, never argv ... Secrets never appear in the preview, the Activity log or argv'), the opposite of an exfiltration directive.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
3. **Send secrets over stdin, never argv.** Set `intent.secret_stdin` and use `--value-stdin` (for example `keys set NAME --value-stdin`). Secrets never appear in the preview, the Activity log or argv. `click.prompt(hide_input)` and getpass ignore piped stdin.
PassedFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
The skill's own golden rules require all mutation to go through previewed, confirmed CLI intents (CommandPreviewScreen before CommandExecutor); there is no covert or undisclosed self-persistence path, and the bundled scripts (render/demo/touchpoints/gap_audit) perform no writes to host control-plane files.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
2. **Mutate only through CLI intents.** Flow: `model.handle_key` -> action/intent -> `_confirm_and_run_intent` -> `CommandPreviewScreen` -> `CommandExecutor` -> `_handle_successful_command` refresh.
PassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
No formal permission manifest exists; the bundled helpers are not strictly read-only -- render.py can write a requested SVG file via its --svg option, and demo.py can execute confirmed DefenseClaw CLI actions under its opt-in --real-exec mode -- but both capabilities are explicitly documented and intrinsic to the contributor skill's declared purpose of developing and testing the TUI, not hidden privilege in a passive-looking package.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
--- name: defenseclaw-tui description: Improve the DefenseClaw Textual TUI (cli/defenseclaw/tui) consistently. ...
PassedInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
Scanned SKILL.md and all references/*.md files for concealment codepoints -- none found; no injected override directives present.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
--- name: defenseclaw-tui description: Improve the DefenseClaw Textual TUI (cli/defenseclaw/tui) consistently. ... ---
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
Shipped as static markdown + local scripts inside the repo's own version control; no runtime instruction fetch or self-updater present.
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
--- name: defenseclaw-tui description: Improve the DefenseClaw Textual TUI (cli/defenseclaw/tui) consistently. ... ---
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
First-party contributor skill named after the project itself; no third-party brand impersonation.
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
name: defenseclaw-tui
Show its 4 tools
| Tool | It can |
|---|---|
demo.pyRuns a fake-data demo of the TUI for local visual verification. | Create or change dataRun code or commands |
gap_audit.pyAudits CLI command coverage against the TUI to find gaps, for contributor review. | Read data |
render.pyHeadlessly renders a TUI screen for review during development of the DefenseClaw Textual TUI. | Read dataCreate or change data |
touchpoints.pyLists a TUI panel's CLI touchpoints/coverage for contributor review. | Read data |
Before you put it to work
- Add an optional TLS listener (certificate/key config) for gateway.api_bind and guardrail.host, and require TLS or refuse to start whenever either is configured to a non-loopback address outside managed_enterprise, closing the one remaining gap on the gateway's HTTP surface (ATR-M0009).
- Document and consider hard-enforcing that gateway.api_bind/guardrail.host must stay loopback unless the operator has independently firewalled the host, since no in-process control currently prevents a non-loopback bind on unmanaged/BYOD installs.
- Upgrade golang.org/x/net, golang.org/x/mod, golang.org/x/image, golang.org/x/crypto, and go.opentelemetry.io/otel/exporters/otlp/* to patched versions (go get -u && go mod tidy) to clear the HIGH-severity trivy CVE findings (ATR-C0007).
- Complete deeper per-site tracing of the 10 gosec go-unsafe-deserialization-interface findings and the Go ResponseWriter-write semgrep findings to resolve the UNKNOWN verdicts (ATR-C0009, ATR-C0010).
- Consider gating cmd_setup.py's _show_splunk_credentials() behind an explicit --reveal/confirm flag to reduce terminal-capture exposure of locally generated secrets.
- No action needed on the evaluated skill/plugin artifacts (codeguard, defenseclaw-add-connector, defenseclaw-develop, defenseclaw-tui, rulepack_artifact_parity); maintain existing check_skill_fresh.py-style drift checks and PR review to preserve their no-exec/no-network/no-concealment posture.
Skillskills-plugins/skills/codeguardskills/codeguard100High trustNone flagged
Its tools can
What stood out
- HIGH: The Go gateway/sidecar's HTTP management API and guardrail proxy listeners (internal/gateway/api.go, internal/gateway/proxy.go) are plain net/http servers with no TLS configuration surface anywhere in the codebase. An operator on an unmanaged/BYOD install who points gateway.api_bind or guardrail.host at a non-loopback address gets a cleartext channel carrying the shared management bearer token with no code path able to enable TLS to protect it (ATR-M0009, HIGH, VULNERABLE).
- HIGH: golang.org/x/net is pinned (go.sum) to a version trivy flags with a HIGH-severity CVE (CVE-2026-78669), underlying the gateway's own net/http client/server stack -- no replace/exclude directive upgrades past it, so the vulnerable code ships in the release binary (ATR-C0007 VULNERABLE).
- UNKNOWN: gosec flagged 10 unsafe-deserialization-interface sites and semgrep flagged dozens of Go ResponseWriter-write sites requiring deeper per-site source-to-sink tracing to resolve (ATR-C0009 Insecure Deserialization, ATR-C0010 Output Encoding).
- The repository's actual MCP-relevant attack surface is the Go gateway/sidecar's local REST management API rather than a classic tool-registering MCP server; most MCP-tool-specific rules (ATR-M0001/0002/0005/0006/0007/0011) are NOT_APPLICABLE as architected, though auth, CSRF, and origin handling on the API itself are strong.
- cmd_setup.py's _show_splunk_credentials() prints locally generated secrets to the terminal without an explicit --reveal/confirm flag, creating terminal-capture exposure risk.
Skill rules 0 of 8 flagged
7 rules passed · 1 did not applyShow
PassedCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
SKILL.md's compatibility field explicitly discloses: 'Optional self-check (main.py) calls the local DefenseClaw gateway over the loopback network'. main.py's only network action is a POST to a strictly-validated loopback/operator-configured HTTP(S) origin (DEFENSECLAW_SIDECAR_URL, default 127.0.0.1:18790) carrying a temp file path for a code scan, matching the declared purpose exactly. No credential harvesting, no undeclared subprocess/eval, no third-party egress -- the skill.yaml declares permissions: [read-only] and capabilities: [text-generation, code-generation], consistent with main.py's behavior (writes a temp file it owns and deletes, reads an HTTP JSON response).
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
--- name: codeguard description: "Security-aware code generation ..." license: Apache-2.0 compatibility: "Optional self-check (main.py) calls the local DefenseClaw gateway over the loopback network" ---
Show 1 more location
def _scan_via_sidecar(path: str) -> str:
endpoint = _validated_scan_url(SIDECAR_URL)
if endpoint is None:
return "error: DEFENSECLAW_SIDECAR_URL must be a valid HTTP(S) origin"
payload = json.dumps({"path": path}).encode("utf-8")
request = Request(
endpoint, data=payload,
headers={"Content-Type": "application/json", "X-DefenseClaw-Client": "codeguard-skill"},
method="POST",
)PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
SKILL.md explicitly states the scanner's findings are advisory ('This is an optional self-verification step; the sidecar's inspectToolPolicy enforces rules regardless'), so the skill cannot be used to suppress or bypass the host's own enforcement gate; it has no language instructing the agent to skip confirmations, hide actions, or override any human-in-the-loop control.
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
"""CodeGuard self-check tool for OpenClaw agents. Allows the agent to validate code against CodeGuard rules before writing it, via the DefenseClaw sidecar API. This is an optional self-verification step; the sidecar's inspectToolPolicy enforces rules regardless. """
PassedFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
main.py makes a single bounded HTTP POST to a validated local origin and reads/deletes its own temp file; it never writes to any host config/memory/control-plane file, never sets environment variables with command-exec semantics, and never persists a directive beyond the current call.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
with tempfile.NamedTemporaryFile(
mode="w", suffix=ext, delete=False, prefix="codeguard-check-"
) as tmp:
tmp.write(code)
tmp_path = tmp.name
try:
return _scan_via_sidecar(tmp_path)
finally:
try:
os.unlink(tmp_path)
except OSError:
passPassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
skill.yaml declares permissions: [read-only] and capabilities: [text-generation, code-generation]; the actual code performs one bounded, read-only-equivalent action (an HTTP POST of a temp-file path to a local scan endpoint, reading back JSON) with no filesystem write outside its own throwaway temp file, no shell exec, no broad network reach. Scope matches declaration.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
name: codeguard version: "1.0.0" description: Security-aware code generation ... capabilities: - text-generation - code-generation permissions: - read-only
PassedInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
Scanned SKILL.md for bidi-override/zero-width/Tag-block codepoints (U+200B/U+200C/U+200D/U+FEFF/U+202A-E/U+2066-9) -- none found. All example/control-token text sits inside fenced code blocks rendered as literal examples, not active instructions.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
--- name: codeguard description: "Security-aware code generation — teaches the agent CodeGuard rules to write secure code by default" license: Apache-2.0 compatibility: "Optional self-check (main.py) calls the local DefenseClaw gateway over the loopback network" ---
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
The skill is shipped fully declaratively inside this repo (SKILL.md + main.py + skill.yaml), versioned (1.0.0) in the same git history and release process as the rest of DefenseClaw; main.py fetches no instructions/code at runtime and has no self-update mechanism. The only runtime network action fetches scan RESULTS from the local sidecar for the data just submitted -- not a mutation of the skill's own instructions or manifest -- matching the false-positive guard for a runtime fetch of the tool's own declared-job data.
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
name: codeguard version: "1.0.0" description: Security-aware code generation — teaches the agent CodeGuard rules to write secure code by default author: defenseclaw
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
Name 'codeguard' with author 'defenseclaw' is the project's own first-party skill, published inside its own repository under its own brand; no similarity to or impersonation of a known third-party brand/tool name was found.
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
name: codeguard version: "1.0.0" description: Security-aware code generation — teaches the agent CodeGuard rules to write secure code by default author: defenseclaw
Does not applyHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
main.py's run(code, filename) tool accepts source code and a filename as explicit arguments and reads only the DEFENSECLAW_SIDECAR_URL environment variable (configuration, not a credential or conversation/memory source); it has no mechanism to obtain host credentials, SSH keys, cloud metadata, or live conversation/memory context. Because ATR-P0003 is scoped to artifacts with an existing credential/conversation harvest step, and this tool has none, no harvest/exfiltration pair can arise here.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
def run(code: str, filename: str = "check.py") -> str:
ext = os.path.splitext(filename)[1] or ".py"
with tempfile.NamedTemporaryFile(
mode="w", suffix=ext, delete=False, prefix="codeguard-check-"
) as tmp:
tmp.write(code)
tmp_path = tmp.name
try:
return _scan_via_sidecar(tmp_path)
finally:
try:
os.unlink(tmp_path)
except OSError:
passShow 1 more location
def _validated_scan_url(base_url: str) -> str | None:
...
if parsed.scheme.lower() not in {"http", "https"}:
return None
if not parsed.hostname or parsed.username is not None or parsed.password is not None:
return NoneShow its 1 tool
| Tool | It can |
|---|---|
runScans code content for security issues by writing it to a temp file and POSTing the path to the local DefenseClaw sidecar's /api/v1/scan/code endpoint; returns 'clean' or a formatted list of findings. Invoked by the agent as a self-check before writing code. | Read dataReach any network address |
Before you put it to work
- Add an optional TLS listener (certificate/key config) for gateway.api_bind and guardrail.host, and require TLS or refuse to start whenever either is configured to a non-loopback address outside managed_enterprise, closing the one remaining gap on the gateway's HTTP surface (ATR-M0009).
- Document and consider hard-enforcing that gateway.api_bind/guardrail.host must stay loopback unless the operator has independently firewalled the host, since no in-process control currently prevents a non-loopback bind on unmanaged/BYOD installs.
- Upgrade golang.org/x/net, golang.org/x/mod, golang.org/x/image, golang.org/x/crypto, and go.opentelemetry.io/otel/exporters/otlp/* to patched versions (go get -u && go mod tidy) to clear the HIGH-severity trivy CVE findings (ATR-C0007).
- Complete deeper per-site tracing of the 10 gosec go-unsafe-deserialization-interface findings and the Go ResponseWriter-write semgrep findings to resolve the UNKNOWN verdicts (ATR-C0009, ATR-C0010).
- Consider gating cmd_setup.py's _show_splunk_credentials() behind an explicit --reveal/confirm flag to reduce terminal-capture exposure of locally generated secrets.
- No action needed on the evaluated skill/plugin artifacts (codeguard, defenseclaw-add-connector, defenseclaw-develop, defenseclaw-tui, rulepack_artifact_parity); maintain existing check_skill_fresh.py-style drift checks and PR review to preserve their no-exec/no-network/no-concealment posture.
Skillskills-plugins/testdata/rulepack_artifact_parity/skilltestdata/rulepack_artifact_parity/skill100High trustNone flagged
Its tools can
What stood out
- HIGH: The Go gateway/sidecar's HTTP management API and guardrail proxy listeners (internal/gateway/api.go, internal/gateway/proxy.go) are plain net/http servers with no TLS configuration surface anywhere in the codebase. An operator on an unmanaged/BYOD install who points gateway.api_bind or guardrail.host at a non-loopback address gets a cleartext channel carrying the shared management bearer token with no code path able to enable TLS to protect it (ATR-M0009, HIGH, VULNERABLE).
- HIGH: golang.org/x/net is pinned (go.sum) to a version trivy flags with a HIGH-severity CVE (CVE-2026-78669), underlying the gateway's own net/http client/server stack -- no replace/exclude directive upgrades past it, so the vulnerable code ships in the release binary (ATR-C0007 VULNERABLE).
- UNKNOWN: gosec flagged 10 unsafe-deserialization-interface sites and semgrep flagged dozens of Go ResponseWriter-write sites requiring deeper per-site source-to-sink tracing to resolve (ATR-C0009 Insecure Deserialization, ATR-C0010 Output Encoding).
- The repository's actual MCP-relevant attack surface is the Go gateway/sidecar's local REST management API rather than a classic tool-registering MCP server; most MCP-tool-specific rules (ATR-M0001/0002/0005/0006/0007/0011) are NOT_APPLICABLE as architected, though auth, CSRF, and origin handling on the API itself are strong.
- cmd_setup.py's _show_splunk_credentials() prints locally generated secrets to the terminal without an explicit --reveal/confirm flag, creating terminal-capture exposure risk.
Skill rules 0 of 8 flagged
1 rule passed · 7 did not applyShow
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
The fixture declares an identity ('parity-fixture') in its SKILL.md, so the impersonation/typosquatting rule is applicable to it. The name is a neutral, descriptive fixture label bearing no resemblance to any known third-party brand or publisher, so the declared identity is benign.
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
name: parity-fixture
Does not applyCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
This is a test fixture (under testdata/) used by internal/guardrail/artifact_scan_test.go and cli/tests/test_rulepack_overlay_python_source.py to validate the AWS-key rule pack; it is not a shipped product skill (per scan-workflow guidance, test fixtures under testdata/ are not shipped components). Its deliberately-planted AWS-key-shaped strings (scripts/docstring.py, scripts/comment.py) are the intended test input for the scanner's rule pack, not a concealed capability of a real shipped skill.
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
--- name: parity-fixture description: Fixture for the rule-pack parity test of install admission and skill scan. --- # Parity fixture Runs `scripts/docstring.py` and `scripts/comment.py`.
Does not applyConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
Test fixture under testdata/, not a shipped skill (see ATR-P0001 rationale for this component). Its one-line instruction body ('Runs scripts/docstring.py and scripts/comment.py') contains no consent-related directive at all.
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
# Parity fixture Runs `scripts/docstring.py` and `scripts/comment.py`.
Does not applyHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
The documentation-example AWS-key-shaped string exists only in an inert fixture docstring (scripts/docstring.py) with no credential source, conversation read, or outbound exfiltration path; the fixture is test input for the rule pack, not a shipped skill with a harvest/exfiltration capability.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
"""Parity fixture: the example key sits only in this docstring.
Example access key from the AWS documentation: [REDACTED]
"""
def run():
return 0Does not applyFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
Test fixture under testdata/, not a shipped skill; its scripts only return 0 with no persistence or execution behavior.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
def run():
# Example access key from the AWS documentation: [REDACTED]
return 0Does not applyOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
Test fixture under testdata/, not a shipped skill; declares no permission manifest and ships only no-op scripts.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
--- name: parity-fixture description: Fixture for the rule-pack parity test of install admission and skill scan. ---
Does not applyInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
Test fixture under testdata/, not a shipped skill; scanned for concealment codepoints -- none found, and its one-line instruction body carries no directive at all.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
--- name: parity-fixture description: Fixture for the rule-pack parity test of install admission and skill scan. --- # Parity fixture Runs `scripts/docstring.py` and `scripts/comment.py`.
Does not applySkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
Test fixture under testdata/, not a shipped skill with any provenance/distribution concerns of its own.
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
--- name: parity-fixture description: Fixture for the rule-pack parity test of install admission and skill scan. ---
Show its 2 tools
| Tool | It can |
|---|---|
scripts/comment.py:runTest-fixture script that returns 0; used only to validate the rule pack's ability to detect an AWS-key-shaped string embedded in a Python comment. | Read data |
scripts/docstring.py:runTest-fixture script that returns 0; used only to validate the rule pack's ability to detect an AWS-key-shaped string embedded in a Python docstring. | Read data |
Before you put it to work
- Add an optional TLS listener (certificate/key config) for gateway.api_bind and guardrail.host, and require TLS or refuse to start whenever either is configured to a non-loopback address outside managed_enterprise, closing the one remaining gap on the gateway's HTTP surface (ATR-M0009).
- Document and consider hard-enforcing that gateway.api_bind/guardrail.host must stay loopback unless the operator has independently firewalled the host, since no in-process control currently prevents a non-loopback bind on unmanaged/BYOD installs.
- Upgrade golang.org/x/net, golang.org/x/mod, golang.org/x/image, golang.org/x/crypto, and go.opentelemetry.io/otel/exporters/otlp/* to patched versions (go get -u && go mod tidy) to clear the HIGH-severity trivy CVE findings (ATR-C0007).
- Complete deeper per-site tracing of the 10 gosec go-unsafe-deserialization-interface findings and the Go ResponseWriter-write semgrep findings to resolve the UNKNOWN verdicts (ATR-C0009, ATR-C0010).
- Consider gating cmd_setup.py's _show_splunk_credentials() behind an explicit --reveal/confirm flag to reduce terminal-capture exposure of locally generated secrets.
- No action needed on the evaluated skill/plugin artifacts (codeguard, defenseclaw-add-connector, defenseclaw-develop, defenseclaw-tui, rulepack_artifact_parity); maintain existing check_skill_fresh.py-style drift checks and PR review to preserve their no-exec/no-network/no-concealment posture.
Repository-wide rules
Checked once for the whole repository: dependencies, secrets, inputs, networking, CI and more. 3 of 15 flagged.
12 rules passedShow
PassedRuntime Command Injection via Process ExecutionATR-C0003Details
Why it got this result
Reviewed all non-test semgrep 'dangerous-exec-command' hits (40 total) across cmd/, internal/cli/, internal/gateway/, internal/openshell/. Every exec.Command/exec.CommandContext call found uses argument-vector execution with a fixed, hardcoded binary name/path (e.g. 'git', '/bin/ls', the gateway's own restart-helper executable) and a Go []string argv slice, with shell:false (Go's os/exec never invokes a shell unless the program explicitly execs /bin/sh -c). internal/openshell/cli.go's CLI.Command() builds argv for the upstream 'openshell' binary and documents 'Every invocation names the gateway and workspace explicitly, so an operator's OPENSHELL_GATEWAY or last-used sandbox can never redirect it' plus an SSHShim defense against PATH hijacking. internal/gateway/codex_hook.go's runGitList() invokes 'git' with a fixed arg vector and a hardened environment (GIT_CONFIG_NOSYSTEM, GIT_CONFIG_GLOBAL=/dev/null) to prevent config injection, and bounds output via io.LimitReader. No instance was found where a shell string is built via concatenation/formatting of attacker-controlled HTTP/JSON-RPC/tool-argument input and then passed to os/exec with a shell, sh -c, or eval.
Why it matters
Unsanitized command construction in shipped runtime code can give a remote attacker arbitrary code execution with the application's privileges.
Evidence
func (inv Invocation) Command(ctx context.Context) (*exec.Cmd, context.CancelFunc, error) {
if len(inv.Argv) == 0 {
return nil, nil, errors.New("openshell: empty invocation")
}
env := Environ(os.Environ())
shim, err := NewSSHShim(envValue(env, "PATH"))
...
if inv.Interactive {
cmd = exec.CommandContext(ctx, inv.Argv[0], inv.Argv[1:]...)
...
} else {
cmd = processutil.CommandContext(ctx, inv.Argv[0], inv.Argv[1:]...)Show 2 more locations
func runGitList(ctx context.Context, cwd string, args ...string) ([]string, error) {
if err := managed.RequireWindowsGatewayPathOutsideUserProfiles(cwd); err != nil {
return nil, err
}
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = cwd
cmd.Env = safeGitEnv()+37 more locations in the finding JSON
PassedWeaponization: Malicious-Purpose Artifact (Offensive Capability by Design)ATR-R0001Details
Why it got this result
DefenseClaw is a defensive security-governance product. The YARA scanner (timed out repo-wide) flagged dozens of files; manual review of representative files from each flagged cluster confirms consistently defensive/detective purpose: internal/gateway/llm_judge.go implements an LLM-based exfiltration/injection/destructive-command CLASSIFIER used to BLOCK malicious agent actions (not perform them); benchmark_normalize_cochise.py and its tests are dataset normalizers that label known-malicious trajectories for the product's OWN detector evaluation; cmd_setup.py is the product's own legitimate CLI configuration tool; internal/scanner/clawshield_secrets.go contains regex PATTERNS to DETECT private keys in scanned content (a secrets scanner), not a key itself; flagged test files contain malicious-looking strings solely as fixture/input data asserting the gateway correctly blocks/detects them. No file advertises an offensive purpose, directs generation of malware/exploits for a named target, or evades oversight/attribution of its own actions.
Why it matters
An artifact whose purpose is to weaponize the agent -- to produce malware, steal credentials, evade defenses, or run attacks at scale -- is dangerous regardless of how securely it is built; posture controls (auth, input validation, least privilege) cannot make a weapon safe, which is why a confirmed weaponization finding caps the trust verdict to `blocked` rather than being averaged against a good posture score. Catching malicious PURPOSE is a distinct safety control, and it must be conservative because offensive capability is dual-use: legitimate pentest, CTF, defensive, and research work must abstain (UNKNOWN), never block -- unless the artifact itself hides from the people who install, run or audit it.
Evidence
const exfilSystemPrompt = `You are a data-exfiltration safety classifier. Your task is to decide whether a USER prompt is trying to read or exfiltrate sensitive files, credentials, secrets, or system data...\n- Exfiltration Channel: requests to ship data to an attacker-controlled or out-of-band destination (curl/wget POST of file contents to an external host, base64+upload, dns-tunnel, "email me the file"). Normal API calls ... are NOT exfiltration.
Show 1 more location
[REDACTED]
{id: "CS-SEC-KEY-EC", provider: "generic", pattern: regexp.MustCompile(`-----BEGIN EC PRIVATE KEY-----`) ...PassedUnrestricted Network Fetch / SSRFATR-C0001Details
Why it got this result
DefenseClaw implements dedicated SSRF-safe dialing throughout its gateway/proxy/webhook code paths. internal/gateway/provider.go implements secureDialContext(), which re-resolves the destination host at connect time and explicitly refuses dials to unsafe/private IPs ('refusing dial to %s (resolved to unsafe IP %s)'). internal/gateway/webhook.go's WebhookDispatcher HTTP client is constructed with this same secureDialContext for every outbound webhook delivery. internal/openshell/egress/dial.go and proxy.go implement a 'netguard.V8SafeDialContext' egress-control path used by the sandbox/connector egress proxy, re-validating destinations per-request. The cli/defenseclaw/registries/ssrf.py module exists specifically to analyze/guard registry fetches. There is a dedicated DNS-rebinding-safe dial pattern (re-resolve then validate at actual TCP-connect time, not just at URL-parse time) consistently applied.
Why it matters
Enables internal pivoting and data exfiltration.
Evidence
// secureDialContext (tests map destination names to local servers).
...
// destination at connect time and refuses...
func (e ...) Error() string {
func secureDialResolver() netguard.V8Resolver {
if box := secureDialResolverOverride.Load(); box != nil {Show 1 more location
// - secureDialContext: every TCP dial re-resolves the destination and ... DialContext: secureDialContext(allowLoopback, 10*time.Second),
PassedStatic SecretsATR-C0002Details
Why it got this result
Manual review of the completed secret-scanner output found no live secret value in the repository: the representative non-test hits are mock-only placeholders (e.g., a documented stand-in credential that never authenticates) and regex detector signatures embedded in the scanner's own pattern definitions, not committed secrets.
Why it matters
Plaintext secrets are easily exfiltrated.
Evidence
Show 2 more locations
+37 more locations in the finding JSON
PassedMissing or Inadequate Input ValidationATR-C0008Details
Why it got this result
Reviewed the gateway's HTTP API surface (internal/gateway/api.go) and webhook/skill-fetch handlers: handleSkillFetch validates req.Target is non-empty, rejects Windows-unsafe paths via rejectWindowsGatewayPath, and enforces directory containment after symlink resolution (see ATR-C0011 finding) before any filesystem operation. JSON body decoding uses json.NewDecoder(r.Body).Decode into typed request structs (e.g. skillFetchRequest, mcpScanRequest) rather than accepting raw untyped maps, which constrains the shape of accepted input. Config-level credential names are validated against an explicit allowlist pattern (ValidEnterpriseCredentialName, internal/config/enterprise.go) rejecting any value that is not 'lowercase letters, digits and dashes'. No handler was found that passes raw, unvalidated request fields directly into exec/fetch/fs sinks (cross-referenced with ATR-C0001/C0003/C0011 findings).
Why it matters
Frontline control to prevent multiple classes of vulns.
Evidence
if ai.Enabled {
if !ValidEnterpriseCredentialName(ai.Credential) {
return fmt.Errorf("config: enterprise.inspection.ai_defense.credential %q must be a protected credential name (lowercase letters, digits and dashes)", ai.Credential)
}
} else if strings.TrimSpace(ai.Credential) != "" && !ValidEnterpriseCredentialName(ai.Credential) {
return fmt.Errorf("config: enterprise.inspection.ai_defense.credential %q is not a valid credential name", ai.Credential)PassedInsecure DeserializationATR-C0009Details
Why it got this result
Manual review of the gosec 'go-unsafe-deserialization-interface' findings and the plugin-manifest YAML loading path found no code-executing deserialization sink. The reviewed plugin-manifest loader (cli/defenseclaw/scanner/plugin_scanner/scanner.py) binds yaml.load to CSafeLoader/SafeLoader and only accepts dictionary results; no pickle.loads or unsafe PyYAML Loader is used against untrusted input. The Go gosec findings decode into ordinary typed data mappings rather than instantiating attacker-controlled interface values through gob or an equivalent gadget-capable mechanism.
Why it matters
Can lead to arbitrary code execution.
PassedPath Traversal & Symlink Bypass (Filesystem Tools)ATR-C0011Details
Why it got this result
The one HTTP endpoint found accepting a caller-supplied filesystem path, POST /v1/skill/fetch (internal/gateway/api.go handleSkillFetch), resolves symlinks on BOTH the caller-supplied target and every configured allowed root via filepath.EvalSymlinks + filepath.Abs, then requires the resolved target to equal or be a path-separator-prefixed child of one of the configured SkillDirs()/PluginDirs() roots -- rejecting any path outside those roots even via symlink indirection. A code comment references a prior finding (Avarice F-3287) that this exact containment was added after a previous version lacked it, confirming deliberate remediation. No other HTTP handler in internal/gateway/*.go was found reading/writing an arbitrary caller-supplied path without this pattern.
Why it matters
Traversal exposes arbitrary host files; can lead to code execution.
Evidence
resolvedTarget, err := filepath.EvalSymlinks(req.Target)
if err != nil { ... }
resolvedAbs, err := filepath.Abs(resolvedTarget)
...
for _, root := range allowedRoots {
...
rr, rerr := filepath.EvalSymlinks(root)
...
if resolvedAbs == rrAbs {
rootOK = true
break
}
if strings.HasPrefix(resolvedAbs, rrAbs+string(os.PathSeparator)) {
rootOK = true
…Show 2 more locations
+37 more locations in the finding JSON
PassedTool Output Sanitization & Injection ContainmentATR-C0013Details
Why it got this result
DefenseClaw's gateway is itself the injection-containment layer for third-party agent tool calls: internal/gateway/llm_judge.go implements a dedicated prompt-injection classifier (runExfilJudge, tool-call injection judge) that scans tool arguments/results for injection markers, bounds payload size before forwarding to an LLM judge (truncation markers 'tool arguments truncated; beginning and end retained'), and the toolJudgeSecurityExcerptTerms list specifically flags fake role markers, control sequences, and known injection phrasing for review/blocking. This is the product's core function (detecting and blocking un-contained tool-output-driven prompt injection for the agents it governs), not a gap in its own architecture.
Why it matters
Tool outputs are a primary prompt-injection vector. Third-party content (web pages, files, API responses) may contain text crafted to override instructions or impersonate system/tool messages. Without a host-enforced envelope, sanitization, and an explicit untrusted-return signal, the model can be coerced into executing attacker-controlled directives, leaking secrets, or chaining further tool calls on behalf of the adversary.
Evidence
const toolJudgeTruncationMarker = "\n...[tool arguments truncated; beginning and end retained]...\n" const toolJudgeUserIntentTruncationMarker = "\n...[user intent truncated; beginning and end retained]...\n" const toolJudgeMiddleExcerptMarker = "\n...[mechanically selected middle excerpts]...\n"
PassedCI/CD Workflow Script InjectionATR-C0014Details
Why it got this result
Semgrep flagged run-shell-injection in connector-live-e2e.yml (3x) and release.yaml (1x), plus mutable-action-tag in e2e.yml (6x) referencing maintainer-controlled float tags, not attacker-controlled data. The connector-live-e2e.yml workflow has pull_request and workflow_dispatch triggers, but the live-matrix jobs that contain the flagged shell-injection sites are gated to workflow_dispatch only (a manual trigger requiring repo write access) and interpolate only fixed/trusted values (github.sha, github.repository, prior step outputs such as steps.package.outputs.artifact_id) -- no attacker-controlled event field reaches a shell sink. The release.yaml hit is inside a workflow_dispatch-only release job using $VERSION/$GH_TOKEN env vars as quoted data, not re-parsed by eval/sh -c. No workflow uses pull_request_target, issue_comment, workflow_run, or repository_dispatch as an actual trigger on a privileged job.
Why it matters
CI/CD script injection can execute attacker-controlled commands with workflow tokens, repository secrets, trusted-runner access, artifact or cache authority, and release or deployment privileges.
Evidence
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: [self-hosted, Linux, ARM64, e2e]
timeout-minutes: 30
env:
DEFENSECLAW_GATEWAY_TOKEN: ${{ secrets.OPENCLAW_GATEWAY_TOKEN }}
...Show 1 more location
- name: Real official-client E2E
shell: pwsh
run: |
./scripts/live-connector-e2e/run-windows.ps1 `
-ExpectedPackageSourceCommit '${{ github.sha }}' `
-ExpectedPackageRunID '${{ inputs.windows_package_run_id }}' `
-ExpectedPackageArtifactID '${{ steps.package.outputs.artifact_id }}' `
-ExpectedWorkflowRepository '${{ github.repository }}' `PassedLogging Hygiene (No Secrets/PII/Tokens) & Audit TrailATR-C0005Details
Why it got this result
DefenseClaw ships a dedicated internal/redaction and internal/observability/redaction package set, plus a product_telemetry_sender/redaction_catalog generation pipeline (scripts/generate_observability_redaction_catalog.py), and cli/defenseclaw/logger.py's emit_cli_observability never logs the raw token itself (only uses it for an Authorization header on an internal call). Audit logging is pervasive via internal/audit and gatewaylog packages with structured, per-action invocation logging. Webhook endpoint URLs are stored alongside a separate 'logURL' field that is explicitly 'redactWebhookURL(url): the only form that may be logged', demonstrating deliberate separation of loggable vs. sensitive representations.
Why it matters
Prevents sensitive data leakage; enables forensics.
Evidence
type webhookEndpoint struct {
url string
logURL string // redactWebhookURL(url): the only form that may be loggedPassedContainer/Runtime HardeningATR-C0006Details
Why it got this result
The repository ships exactly one Dockerfile (bundles/splunk_local_bridge/s3_exporter/Dockerfile). It uses a slim base image, creates and chowns a dedicated non-root user (uid 10001), and switches to USER exporter before ENTRYPOINT. Trivy config scan flagged only one LOW misconfiguration (DS-0026: missing HEALTHCHECK), with 26 passing checks. No USER root, no privileged mode, no hostNetwork, no writable-rootfs issue found. Trivy's k8s config scans of test golden YAML files (internal/sensor/kernelpolicy/testdata/*.yaml) also passed all 61 checks each.
Why it matters
Reduces blast radius of exploits.
Evidence
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY export_splunk_to_s3.py . RUN useradd -r -u 10001 exporter && mkdir -p /state && chown -R exporter:exporter /state /app USER exporter ENTRYPOINT ["python", "/app/export_splunk_to_s3.py"]
PassedLack of Output Encoding/SanitizationATR-C0010Details
Why it got this result
The interactive code editor (docs-site/components/policy-creator/ui/code-editor.tsx) is the component handling attacker-controlled input among the flagged sites: its textarea value is tokenized by rego-highlight.ts, and each token's &, <, and > characters are escaped before the resulting markup reaches dangerouslySetInnerHTML. The remaining flagged Go and React sites construct static deployment/documentation data rather than reflecting request input, so no unescaped attacker-controlled content reaches an HTML sink.
Why it matters
Prevents UI-level injection and operator compromise.
You know what it is. Now see what it does.
A scan helps you understand what you’re building with. When your agent runs, BlueRock shows you what actually happens across tools and systems. Follow actions in context, understand behavior as it unfolds and add controls when you need them.
Results reflect the code at the commit and date shown.