diff --git a/docs/ASI_MAPPING.md b/docs/ASI_MAPPING.md index 28a844c..c0ca24d 100644 --- a/docs/ASI_MAPPING.md +++ b/docs/ASI_MAPPING.md @@ -17,17 +17,20 @@ REAP cites [OWASP Top 10 for Agentic Applications (2026)](https://genai.owasp.or | `mcp-redirect-uri-laxity` | ASI03 | Broad redirect registration enables confused-deputy and code-interception attacks against the agent's identity. | | `mcp-session-id-entropy` | ASI03 | A guessable session ID lets an attacker assume another caller's identity directly. | | `mcp-host-header-validation` | ASI03 | DNS rebinding lets a web page reach an agent endpoint and act with the victim's network position. | -| `transport-plaintext` | ASI04 | Agent traffic, tool arguments, and bearer tokens readable on the wire. | -| `transport-downgrade` | ASI04 | A plaintext listener on the same host defeats the TLS the endpoint otherwise offers. | +| `transport-plaintext` | ASI07 | Agent traffic, tool arguments, and bearer tokens readable on the wire: insecure communication on the agent's own channel. | +| `transport-downgrade` | ASI07 | A plaintext listener on the same host defeats the TLS the endpoint otherwise offers. | | `tls-cert-health` | ASI09 | Certificate and cipher problems undermine the assurance the transport is supposed to provide. | | `http-cors-wildcard` | ASI03 | Permissive CORS lets an arbitrary web origin act as the agent from a victim's browser. | -| `http-rate-limit-absence` | ASI06 | No advertised limiting means a caller can drive the agent (and everything downstream of it) without backpressure. | +| `http-rate-limit-absence` | ASI08 | No advertised limiting means a caller can drive the agent (and everything downstream of it) without backpressure: a cascading-failure concern. | +| `mcp-tmpl-high-risk-tool-names` (template) | ASI02, ASI05 | Tool names suggesting shell execution or raw filesystem access are the unexpected-code-execution case; the inventory itself is the tool-misuse reconnaissance step. | -## Two corrections +## Three corrections -**`mcp-host-header-validation` cited ASI05 (Memory & Context Poisoning).** DNS rebinding has nothing to do with an agent's memory or context. It is an identity and privilege problem: the attacker borrows the victim's network position to reach an endpoint that trusts it. Now ASI03. +**The reference table followed an earlier draft of the numbering.** `ASITitles` in `internal/report/report.go` carried pre-v1.0 titles for ASI04 to ASI09 (Insecure Inter-Agent Communication, Memory & Context Poisoning, Cascading Failures, Excessive Agency, Supply Chain & Dependency Risk, Observability & Auditability Gaps), and every `asi_refs` was coded against it. The table now matches the published v1.0 list and is pinned by `TestASITitles_MatchPublishedV1`; the codes below were renumbered to keep the meaning each check was given: `http-rate-limit-absence` ASI06 to ASI08, `transport-plaintext` and `transport-downgrade` ASI04 to ASI07, the high-risk-tool-names template ASI07 to ASI05. The four checks still citing ASI09 (Human-Agent Trust Exploitation in v1.0) record observability and disclosure facts that the draft filed under a category the final list dropped; they are left as they are pending a decision on whether to uncite them like `mcp-auth-posture`. -**`http-rate-limit-absence` cited ASI08 (Supply Chain & Dependency Risk).** Missing rate-limit headers say nothing about dependencies. Unbounded call volume against an agent is a cascading-failure and availability concern. Now ASI06. +**`mcp-host-header-validation` cited ASI05 (Memory & Context Poisoning in the draft numbering; ASI06 in v1.0).** DNS rebinding has nothing to do with an agent's memory or context. It is an identity and privilege problem: the attacker borrows the victim's network position to reach an endpoint that trusts it. Now ASI03. + +**`http-rate-limit-absence` cited ASI08 (Supply Chain & Dependency Risk in the draft numbering; ASI04 in v1.0).** Missing rate-limit headers say nothing about dependencies. Unbounded call volume against an agent is a cascading-failure and availability concern, which is ASI08 in v1.0. ## Severity calibration diff --git a/internal/probe/transport/checks.go b/internal/probe/transport/checks.go index 1a1ac61..5dc12a4 100644 --- a/internal/probe/transport/checks.go +++ b/internal/probe/transport/checks.go @@ -82,7 +82,7 @@ func (p *plaintextProbe) Run(ctx context.Context, s probe.Session, r *report.Rep Title: "Agent endpoint served over plaintext HTTP", Severity: report.SeverityMedium, Protocol: "*", - ASI: []string{"ASI04"}, + ASI: []string{"ASI07"}, Description: "Target URL uses http:// rather than https://. Tool calls, arguments, and any auth tokens are visible to on-path observers.", Evidence: map[string]any{"url": s.TargetURL()}, Remediation: "Serve agent endpoints over TLS only; redirect or refuse plaintext connections.", @@ -213,7 +213,7 @@ func (p *downgradeProbe) Run(ctx context.Context, s probe.Session, r *report.Rep Title: "Endpoint also responds over plaintext HTTP", Severity: report.SeverityMedium, Protocol: "*", - ASI: []string{"ASI04"}, + ASI: []string{"ASI07"}, Description: "The target host also accepted at least one plaintext HTTP path for agent traffic, which undermines TLS protections.", Evidence: map[string]any{"fallbacks": evidence}, Remediation: "Disable plaintext HTTP listeners for agent endpoints and accept traffic only over TLS.", @@ -410,10 +410,9 @@ func (p *rateLimitProbe) Run(ctx context.Context, s probe.Session, r *report.Rep Title: "No standard rate-limit headers observed", Severity: report.SeverityLow, Protocol: "*", - // ASI06 (Cascading Failures) rather than ASI08 (Supply Chain), which - // this has nothing to do with. Missing rate limiting is a - // cascading-failure and availability concern. - ASI: []string{"ASI06"}, + // Missing rate limiting is a cascading-failure and availability + // concern: ASI08 (Cascading Failures) in the published v1.0 numbering. + ASI: []string{"ASI08"}, Description: "The endpoint answered requests but sent no standard rate-limit response headers. This is a reconnaissance signal that the service may not be advertising rate limiting to clients; it is not proof that no limiting exists.", Evidence: map[string]any{"observations": observed}, Remediation: "Expose standard rate-limit headers such as Retry-After, RateLimit-Remaining, and RateLimit-Limit, or document the expected client behavior when limits are reached.", diff --git a/internal/probe/transport/checks_test.go b/internal/probe/transport/checks_test.go index 22cc80f..dd1e0a2 100644 --- a/internal/probe/transport/checks_test.go +++ b/internal/probe/transport/checks_test.go @@ -177,7 +177,7 @@ func TestRateLimitProbe(t *testing.T) { } }) - t.Run("headers absent: reports at ASI06", func(t *testing.T) { + t.Run("headers absent: reports at ASI08", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) @@ -190,10 +190,10 @@ func TestRateLimitProbe(t *testing.T) { if len(rep.Findings) != 1 { t.Fatalf("expected 1 finding, got %v", rep.Findings) } - // ASI08 (Supply Chain) was simply the wrong category for missing rate - // limiting; it is a cascading-failure concern. - if got := rep.Findings[0].ASI; len(got) != 1 || got[0] != "ASI06" { - t.Fatalf("expected ASI06, got %v", got) + // Missing rate limiting is a cascading-failure concern: ASI08 in the + // published v1.0 numbering (the draft table had it at ASI06). + if got := rep.Findings[0].ASI; len(got) != 1 || got[0] != "ASI08" { + t.Fatalf("expected ASI08, got %v", got) } }) } diff --git a/internal/report/report.go b/internal/report/report.go index 901ab6e..767e797 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -544,15 +544,18 @@ func joinASITitled(refs []string) string { // Kept here so probes/templates can cite by code without hardcoding titles // everywhere, and so this list has exactly one place to update. var ASITitles = map[string]string{ + // Published v1.0 numbering (December 2025). The previous table followed an + // earlier draft in which ASI04 to ASI09 carried different titles, and every + // probe's asi_refs was coded against that draft; see docs/ASI_MAPPING.md. "ASI01": "Agent Goal Hijack", - "ASI02": "Tool Misuse & Exploitation", - "ASI03": "Agent Identity & Privilege Abuse", - "ASI04": "Insecure Inter-Agent Communication", - "ASI05": "Memory & Context Poisoning", - "ASI06": "Cascading Failures", - "ASI07": "Excessive Agency", - "ASI08": "Supply Chain & Dependency Risk", - "ASI09": "Observability & Auditability Gaps", + "ASI02": "Tool Misuse and Exploitation", + "ASI03": "Identity and Privilege Abuse", + "ASI04": "Agentic Supply Chain Vulnerabilities", + "ASI05": "Unexpected Code Execution (RCE)", + "ASI06": "Memory & Context Poisoning", + "ASI07": "Insecure Inter-Agent Communication", + "ASI08": "Cascading Failures", + "ASI09": "Human-Agent Trust Exploitation", "ASI10": "Rogue Agents", } diff --git a/internal/report/report_test.go b/internal/report/report_test.go index 1f50af8..f31153a 100644 --- a/internal/report/report_test.go +++ b/internal/report/report_test.go @@ -310,6 +310,35 @@ func TestASITitles_AllTenPresent(t *testing.T) { } } +// TestASITitles_MatchPublishedV1 pins the table to the published OWASP Top 10 +// for Agentic Applications 2026 v1.0. The table previously followed an earlier +// draft whose ASI04 to ASI09 carried different titles, and every asi_refs in +// the probes was coded against that draft, so a drift here silently mislabels +// every finding. A title change in the standard must change this test on +// purpose, not the table by accident. +func TestASITitles_MatchPublishedV1(t *testing.T) { + want := map[string]string{ + "ASI01": "Agent Goal Hijack", + "ASI02": "Tool Misuse and Exploitation", + "ASI03": "Identity and Privilege Abuse", + "ASI04": "Agentic Supply Chain Vulnerabilities", + "ASI05": "Unexpected Code Execution (RCE)", + "ASI06": "Memory & Context Poisoning", + "ASI07": "Insecure Inter-Agent Communication", + "ASI08": "Cascading Failures", + "ASI09": "Human-Agent Trust Exploitation", + "ASI10": "Rogue Agents", + } + for id, title := range want { + if got := ASITitles[id]; got != title { + t.Errorf("%s: got %q, want %q (published v1.0)", id, got, title) + } + } + if len(ASITitles) != len(want) { + t.Errorf("ASITitles has %d entries, want %d", len(ASITitles), len(want)) + } +} + func findingIDs(fs []Finding) []string { out := make([]string, len(fs)) for i, f := range fs { diff --git a/templates/mcp/high-risk-tool-names.json b/templates/mcp/high-risk-tool-names.json index 36471ac..49a221d 100644 --- a/templates/mcp/high-risk-tool-names.json +++ b/templates/mcp/high-risk-tool-names.json @@ -4,7 +4,7 @@ "info": { "title": "Tool list includes names suggesting code execution or filesystem access", "severity": "medium", - "asi_refs": ["ASI02", "ASI07"], + "asi_refs": ["ASI02", "ASI05"], "description": "The tool list (requested with credentials, if any were supplied) includes a tool name matching patterns associated with code-execution or raw-filesystem primitives. This is a triage signal for closer review; verify the tool's actual capabilities and authorization model.", "remediation": "Review tool capabilities in context. If the tool performs code execution or unrestricted filesystem access, scope it behind explicit, auditable authorization separate from general tool listing. Consider narrower alternatives.", "tags": ["tool-misuse", "excessive-agency"],