Documentation
¶
Overview ¶
Package attack defines the Executor interface and shared utilities for all Batesian attack implementations.
Index ¶
- Constants
- Variables
- func Register(attackType string, c Constructor)
- func RegisteredTypes() []string
- func Transport(opts Options) http.RoundTripper
- type Artifact
- type ArtifactKind
- type Blackboard
- type ChainExecutor
- type ChainStep
- type Confidence
- type Constructor
- type Dependencies
- type Executor
- type Finding
- type HTTPClient
- func (c *HTTPClient) GET(ctx context.Context, urlTpl string, headers map[string]string) (*Response, error)
- func (c *HTTPClient) OPTIONS(ctx context.Context, urlTpl string, headers map[string]string) (*Response, error)
- func (c *HTTPClient) POST(ctx context.Context, urlTpl string, headers map[string]string, ...) (*Response, error)
- type Options
- type Principal
- type RecordedRequest
- type Recorder
- type Response
- func (r *Response) BodyString() string
- func (r *Response) ContainsAny(substrings ...string) bool
- func (r *Response) IsAccepted() bool
- func (r *Response) IsJSON() bool
- func (r *Response) IsSuccess() bool
- func (r *Response) JSONField(path string) string
- func (r *Response) NormalizeHeaders() map[string]string
- type RuleContext
- type Vars
Constants ¶
const DryRunOOBPlaceholderURL = "http://oob.batesian.invalid"
DryRunOOBPlaceholderURL is the base callback URL substituted for a real OOB listener during a dry run. SSRF executors normally bind a local listener to catch callbacks; a dry run must bind no socket, so they use this non-resolving placeholder instead. The .invalid TLD (RFC 6761) never resolves, and the recorded plan still shows a representative callback URL.
Variables ¶
var ErrInconclusive = errors.New("rule could not reach a testable endpoint")
ErrInconclusive signals that a rule could not reach a testable endpoint, so it was not exercised. The engine surfaces this as a skipped result (neither a finding nor an error) so reporting can distinguish "tested, nothing found" from "could not test". Wrap it with %w to attach detail.
var Version = "dev"
Version is the build-time version string injected from main via attack.Version. It is embedded in the User-Agent header on every outbound HTTP request. Defaults to "dev" so go run / unit tests have a useful value.
Functions ¶
func Register ¶ added in v1.0.0
func Register(attackType string, c Constructor)
Register associates an attack-type string with its Executor constructor. It panics on duplicate registration, which can only happen via a programming error (two executors claiming the same type) and is caught at startup.
func RegisteredTypes ¶ added in v1.0.0
func RegisteredTypes() []string
RegisteredTypes returns the sorted list of all registered attack types. Useful for diagnostics and for validating that every shipped rule has a matching executor.
func Transport ¶ added in v1.2.0
func Transport(opts Options) http.RoundTripper
Transport returns the RoundTripper for a scan-path HTTP client. In a dry run it returns a recording transport that sends nothing; otherwise a real *http.Transport honoring opts.SkipTLS. Routing every scan-path client through this one function is what makes the dry-run "send nothing" guarantee total.
Types ¶
type Artifact ¶ added in v1.0.0
type Artifact struct {
// Kind classifies the datum so consumers can query by type.
Kind ArtifactKind
// Value is the primary payload (token string, URL, capability name, ID, ...).
Value string
// Principal names the identity/tenant this artifact belongs to. Empty means
// anonymous / the default principal. Multi-principal and multi-tenant rules
// use this to keep one principal's artifacts distinct from another's.
Principal string
// Producer is the rule ID that published the artifact, for provenance.
Producer string
// Meta carries optional extra fields (granted scopes, aud, mime type, ...).
Meta map[string]string
}
Artifact is a single typed datum on the Blackboard.
type ArtifactKind ¶ added in v1.0.0
type ArtifactKind string
ArtifactKind classifies a piece of state discovered or produced during a scan. Chained executors publish artifacts of a given kind and consume the kinds they depend on, which lets the engine order producers before consumers and lets one rule build on another's results (e.g. a discovered capability driving a targeted follow-up, or an accepted token driving a downstream access test).
const ( // ArtifactToken is a bearer token (real, forged, or replayed) that downstream // rules can present to test access. Principal identifies whose token it is. ArtifactToken ArtifactKind = "token" // ArtifactEndpoint is a confirmed live protocol endpoint (e.g. the resolved // MCP JSON-RPC URL or an A2A RPC URL) so consumers need not re-discover it. ArtifactEndpoint ArtifactKind = "endpoint" // ArtifactCapability is a capability the target advertised (e.g. "prompts", // "resources", "push-notifications", "extended-agent-card"). ArtifactCapability ArtifactKind = "capability" // ArtifactTaskID is an A2A task identifier created during a scan. ArtifactTaskID ArtifactKind = "task-id" // ArtifactContextID is an A2A context identifier created during a scan. ArtifactContextID ArtifactKind = "context-id" // ArtifactSession is a transport session identifier (e.g. an MCP // Mcp-Session-Id) that a downstream rule can present to test whether the // session can be borrowed or replayed across principals. ArtifactSession ArtifactKind = "session-id" // ArtifactAudience is the resource server's expected JWT `aud` value, once // known (operator-supplied or discovered via RFC 9728). ArtifactAudience ArtifactKind = "audience" // ArtifactClient is an OAuth client registered during a scan (e.g. via DCR). ArtifactClient ArtifactKind = "registered-client" )
type Blackboard ¶ added in v1.0.0
type Blackboard struct {
// contains filtered or unexported fields
}
Blackboard is the concurrency-safe shared state for a single scan. Executors publish artifacts they discover and read artifacts published by earlier rules. Its zero value is not usable; construct it with NewBlackboard.
func NewBlackboard ¶ added in v1.0.0
func NewBlackboard() *Blackboard
NewBlackboard returns an empty, ready-to-use Blackboard.
func (*Blackboard) All ¶ added in v1.0.0
func (b *Blackboard) All() []Artifact
All returns a copy of every artifact on the blackboard, in publication order.
func (*Blackboard) ByKind ¶ added in v1.0.0
func (b *Blackboard) ByKind(kind ArtifactKind) []Artifact
ByKind returns all artifacts of the given kind, in publication order.
func (*Blackboard) Find ¶ added in v1.0.0
func (b *Blackboard) Find(pred func(Artifact) bool) []Artifact
Find returns all artifacts matching pred, in publication order. It lets consumers filter by principal, producer, or Meta beyond a simple kind lookup.
func (*Blackboard) First ¶ added in v1.0.0
func (b *Blackboard) First(kind ArtifactKind) (Artifact, bool)
First returns the first artifact of the given kind, if any.
func (*Blackboard) Publish ¶ added in v1.0.0
func (b *Blackboard) Publish(a Artifact)
Publish records an artifact on the blackboard.
type ChainExecutor ¶ added in v1.0.0
type ChainExecutor interface {
Executor
ExecuteChained(ctx context.Context, target string, opts Options, bb *Blackboard) ([]Finding, error)
}
ChainExecutor is the opt-in interface for rules that participate in stateful multi-step chaining. The engine calls ExecuteChained (passing the shared Blackboard) when an executor implements this interface, and falls back to the plain Executor.Execute otherwise. Existing single-shot rules are unaffected.
A ChainExecutor must tolerate an empty or partial blackboard: if the artifacts it needs were not produced (e.g. the producing rule was filtered out, or the target lacked the precondition), it should return no findings rather than an error, consistent with the project's clean-skip convention.
type ChainStep ¶ added in v1.0.0
type ChainStep struct {
// Hop is the 1-based position of this step in the chain.
Hop int
// Principal is the identity/tenant that performed the step (empty = default).
Principal string
// Action describes what was attempted (e.g. "authenticate as tenant A").
Action string
// Outcome describes the result (e.g. "token issued", "read of tenant B granted").
Outcome string
}
ChainStep is one hop in a multi-step attack's provenance trail, attached to a Finding so chain-of-custody and auditability are visible in output.
type Confidence ¶
type Confidence string
Confidence describes how certain the finding is. "confirmed" means the attack demonstrably succeeded (e.g., unauthenticated data returned). "indicator" means a suspicious pattern was detected but exploitability is not proven (e.g., heuristic scan).
const ( ConfirmedExploit Confidence = "confirmed" RiskIndicator Confidence = "indicator" ConfidenceDefault Confidence = "confirmed" // legacy - callers that don't set Confidence get confirmed )
type Constructor ¶ added in v1.0.0
type Constructor func(RuleContext) Executor
Constructor builds an Executor for a given rule context. Each attack implementation registers a Constructor under its attack-type string.
func Resolve ¶ added in v1.0.0
func Resolve(attackType string) (Constructor, bool)
Resolve returns the Constructor registered for attackType, if any.
type Dependencies ¶ added in v1.0.0
type Dependencies interface {
// Produces lists the artifact kinds this executor may publish.
Produces() []ArtifactKind
// Requires lists the artifact kinds this executor consumes from upstream.
Requires() []ArtifactKind
}
Dependencies is the opt-in interface an executor implements to declare the artifact kinds it produces and consumes. The engine uses these declarations to order execution so that producers of a kind run before its consumers. An executor that does not implement Dependencies is treated as having no declared dependencies and keeps its original position in the run order.
type Executor ¶
type Executor interface {
// Execute runs the attack and returns a (possibly empty) list of findings.
Execute(ctx context.Context, target string, opts Options) ([]Finding, error)
}
Executor runs a single attack rule against a target and returns findings.
type Finding ¶
type Finding struct {
RuleID string
RuleName string
Severity string
// Confidence describes whether the finding is a confirmed exploit or a risk indicator.
// Confirmed findings: the attack demonstrably succeeded (auth bypass proven, data returned).
// Indicator findings: a suspicious pattern was detected; manual verification recommended.
Confidence Confidence
Title string
Description string
Evidence string
Remediation string
TargetURL string
// Chain is the optional provenance trail for findings produced by a
// multi-step chained rule. Nil/empty for single-shot rules.
Chain []ChainStep
}
Finding represents a confirmed vulnerability or notable observation.
type HTTPClient ¶
type HTTPClient struct {
// contains filtered or unexported fields
}
HTTPClient is a thin wrapper around net/http.Client with helpers for attack requests.
func NewHTTPClient ¶
func NewHTTPClient(opts Options, vars Vars) *HTTPClient
NewHTTPClient creates an attack HTTP client.
func NewUnauthHTTPClient ¶
func NewUnauthHTTPClient(opts Options, vars Vars) *HTTPClient
NewUnauthHTTPClient creates an attack HTTP client with no bearer token. Use this for requests that are intentionally unauthenticated (e.g. baseline probes that test whether an endpoint can be reached without credentials). Using the standard NewHTTPClient would inject opts.Token, which would cause "no auth" tests to silently become authenticated when --token is set.
func (*HTTPClient) GET ¶
func (c *HTTPClient) GET(ctx context.Context, urlTpl string, headers map[string]string) (*Response, error)
GET sends a GET request to the expanded URL.
type Options ¶
type Options struct {
// OOBListenerURL is the base URL of the local OOB callback listener.
// Empty if OOB is not enabled.
OOBListenerURL string
// Token is an optional bearer token for authenticated requests.
Token string
// TimeoutSeconds is the per-request HTTP timeout.
TimeoutSeconds int
// SkipTLS disables TLS certificate verification.
SkipTLS bool
// Verbose enables debug logging.
Verbose bool
// AudienceClaim is the operator-supplied expected JWT `aud` value for the
// target MCP resource server. Currently consumed only by mcp-oauth-audience-002,
// which derives canary-mismatch probes (substring/case/array-shape) from this
// value. When empty, that rule attempts RFC 9728 protected-resource-metadata
// auto-discovery and otherwise reports Inconclusive.
AudienceClaim string
// Principals are additional authenticated identities (each with its own token
// and optional tenant) that multi-step chained rules can act as. Cross-tenant
// and handoff rules need at least two principals to prove that one identity
// cannot reach another's objects. Empty for single-principal scans, where
// Token is the only identity.
Principals []Principal
// DryRun, when true, records every outbound request instead of sending it, so
// an operator can review the exact traffic a scan would generate before
// authorizing it. Every scan-path HTTP client honors this via Transport.
DryRun bool
// Recorder collects the requests captured during a dry run. It must be non-nil
// when DryRun is set; the engine stamps the active rule onto it per rule.
Recorder *Recorder
}
Options carries per-scan configuration into each executor.
type Principal ¶ added in v1.0.0
type Principal struct {
// Name is a logical label for the identity, e.g. "tenant-a". Used in evidence
// and ChainStep provenance.
Name string
// Token is the bearer token authenticating this principal.
Token string
// Tenant is the tenant/org identifier this principal belongs to, when the
// target models multi-tenancy. May equal Name.
Tenant string
// Headers are extra request headers a server uses to route or scope the
// principal's tenant (e.g. {"X-Tenant-Id": "A"}). Optional.
Headers map[string]string
}
Principal is an authenticated identity a chained rule can act as. Multi-tenant isolation and session-handoff rules use two or more principals to attempt cross-principal access that must hard-fail.
type RecordedRequest ¶ added in v1.2.0
type RecordedRequest struct {
RuleID string
Method string
URL string
Headers map[string]string // Authorization value is redacted
Body string
}
RecordedRequest is one outbound HTTP request captured during a dry run.
type Recorder ¶ added in v1.2.0
type Recorder struct {
// contains filtered or unexported fields
}
Recorder collects the requests a dry run would have sent instead of sending them. It is safe for concurrent use, though the engine drives rules sequentially and stamps the active rule via SetCurrentRule.
func (*Recorder) Requests ¶ added in v1.2.0
func (r *Recorder) Requests() []RecordedRequest
Requests returns a copy of the recorded requests in capture order.
func (*Recorder) SetCurrentRule ¶ added in v1.2.0
SetCurrentRule labels subsequently recorded requests with ruleID. The engine calls this before running each rule so the dry-run plan can group by rule.
type Response ¶
type Response struct {
URL string
StatusCode int
Headers http.Header
Body []byte
Elapsed time.Duration
}
Response captures an HTTP response for assertion evaluation.
func (*Response) BodyString ¶
BodyString returns the response body as a string.
func (*Response) ContainsAny ¶
ContainsAny returns true if the body contains any of the given substrings. Empty substrings are skipped: strings.Contains(body, "") is always true, so an empty needle (e.g. an optional, absent value like a missing contextId) must not be treated as a match against any body.
func (*Response) IsAccepted ¶ added in v1.5.0
IsAccepted reports whether the response represents a successful JSON-RPC result: an HTTP 2xx whose body is valid JSON carrying a "result" envelope and no "error" envelope. This is the canonical "the JSON-RPC call succeeded" oracle.
It exists because the older idiom IsSuccess() && !isJSONRPCError(body) treats any 2xx that is not a JSON-RPC error envelope as success - including an HTML login page, an empty body, "{}", or a bare object. Those are not results, and judging them as "accepted" produces false positives whenever a target answers an unauthenticated probe with a 2xx non-JSON body (common: redirects to a login page, generic 200 acks, HTML error interstitials).
A JSON-null or empty-object result ({"result":null}, {"result":{}}) still counts as accepted: both are valid JSON-RPC success shapes, and rejecting them would risk false negatives on methods that legitimately return an empty result (e.g. logging/setLevel).
func (*Response) IsJSON ¶ added in v1.5.0
IsJSON reports whether the response body is a JSON object. Use it for raw HTTP responses that are not JSON-RPC result envelopes (for example an A2A extended agent card fetched over HTTP GET) to reject HTML, empty, or non-JSON bodies before applying a structural shape check. Prefer IsAccepted for JSON-RPC method calls, which additionally requires a result envelope.
func (*Response) JSONField ¶
JSONField extracts a nested field from the response body using a dot-path. Example: JSONField("scope") returns the "scope" value from a flat JSON object. Returns empty string if the field is absent or the body is not valid JSON.
func (*Response) NormalizeHeaders ¶
NormalizeHeaders returns a lowercase-keyed map of the response headers. Multiple values for the same header are joined with ", ".
type RuleContext ¶
RuleContext carries the metadata from a rule that executors need to populate findings. This avoids importing the rules package inside executor packages (preventing import cycles).
Directories
¶
| Path | Synopsis |
|---|---|
|
Package a2a contains attack executors for the A2A protocol.
|
Package a2a contains attack executors for the A2A protocol. |
|
Package mcp contains attack executors for the MCP (Model Context Protocol).
|
Package mcp contains attack executors for the MCP (Model Context Protocol). |