Documentation
¶
Overview ¶
Package detect implements the deterministic, offline MCP tool-scanner v2 (Spec 076).
Contract (see specs/076-deterministic-tool-scanner/contracts/detect-engine.md):
Offline: this package performs NO I/O. It imports no networking (net, net/http), no process execution (os/exec), no filesystem access (os), and no HTTP/Docker client. Detection runs purely over in-memory tool definitions supplied by the caller. The offline guarantee is enforced by the standing import-guard test (imports_test.go) and backs FR-001.
Deterministic: identical input (a RegistryView) yields byte-identical output, including finding and signal ordering. No maps are iterated for output ordering; no clocks or randomness are consulted.
Total: every registered Check.Inspect call is run under recover(). A check that panics or errors is isolated, counted in Coverage, and never aborts the scan. A degraded scan still returns its other findings, the same way the existing scanner surfaces scanners_failed.
The engine aggregates per-tool Signals into the existing internal/security/scanner.ScanFinding type (now additively carrying Confidence and Signals), so all CLI/REST/MCP entry points keep their shapes.
Index ¶
Constants ¶
const ( SeverityCritical = "critical" SeverityHigh = "high" SeverityMedium = "medium" SeverityLow = "low" SeverityInfo = "info" )
Severity levels — string values mirror internal/security/scanner so a Finding maps onto scanner.ScanFinding without translation (the scanner wiring copies these strings verbatim). detect cannot import scanner (import cycle), so the vocabulary is mirrored here, not aliased.
const ( ThreatLevelDangerous = "dangerous" // any hard signal → auto-quarantine ThreatLevelWarning = "warning" // soft-only → review ThreatLevelInfo = "info" )
Threat levels — user-facing severity, mirrors scanner.ThreatLevel*.
const ( ThreatToolPoisoning = "tool_poisoning" ThreatPromptInjection = "prompt_injection" ThreatRugPull = "rug_pull" ThreatExfiltration = "exfiltration" ThreatMaliciousCode = "malicious_code" ThreatUncategorized = "uncategorized" )
Threat types — the report vocabulary, mirrors scanner.Threat* plus the exfiltration category from the Spec-076 data model.
const MaxEvidenceLen = 200
MaxEvidenceLen caps the rune length of rendered evidence before an ellipsis is appended, keeping reports bounded regardless of input size.
Variables ¶
This section is empty.
Functions ¶
func CapEvidence ¶
CapEvidence makes a string safe to render in CLI/HTML/JSON reports: control runes and zero-width/bidi format runes are escaped to a visible \uXXXX form (revealed, never silently dropped — the whole point is to expose smuggling), and the result is truncated to MaxEvidenceLen runes with an ellipsis.
func ClampConfidence ¶
ClampConfidence constrains a confidence to the valid [0,1] range.
func Normalize ¶
Normalize canonicalizes free text for semantic matching by the soft checks (FR-009). It applies, in order: NFKC, removal of zero-width / bidi / BOM format runes (Unicode category Cf), lowercasing, contraction expansion (so "don't disclose" and "do not tell" both yield a "do not …" form), whitespace collapse + trim, and light stemming.
It is pure and deterministic. The HARD unicode check (FR-007) deliberately runs on RAW text instead, so stripping format runes here never hides an attack — it only stabilizes phrase matching.
Types ¶
type Check ¶
type Check interface {
ID() string
Inspect(tool ToolView, reg RegistryView) []Signal
}
Check inspects one tool against the whole registry snapshot and emits zero or more signals. Implementations MUST be pure, total, and deterministic — the engine wraps every Inspect in recover(), but a well-behaved check never panics and returns its signals in a stable order.
type Coverage ¶
Coverage records how complete a scan was: a check whose Inspect panicked or errored is recovered, counted here, and never aborts the scan — mirroring the existing scanners_failed degradation path.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine runs all registered checks over a registry snapshot and aggregates per-tool signals into findings. Pure aside from the recover() isolation that keeps a misbehaving check from aborting the scan.
func (*Engine) Scan ¶
func (e *Engine) Scan(reg RegistryView) Result
Scan inspects every tool in the snapshot. The RegistryView is built once per scan (indexes + NormalizedText) if the caller passed an unindexed view, then shared with every check. A check that panics is isolated; the scan still returns its other findings. Output (findings and ordering) is deterministic for identical input.
type Finding ¶
type Finding struct {
RuleID string
Scanner string
ThreatType string
ThreatLevel string
Severity string
Category string
Title string
Description string
Location string
Evidence string
Confidence float64
Signals []string
}
Finding is the per-tool aggregation output. It is self-contained (no scanner import) and converts 1:1 to scanner.ScanFinding in the scanner wiring (T012); the additive Confidence/Signals fields already exist on ScanFinding (T004).
type Options ¶
type Options struct {
// Checks are run, in this order, against every tool. Order is part of the
// determinism contract.
Checks []Check
// ScannerID is stamped onto every finding's Scanner field. Defaults to
// "tpa-descriptions" when empty.
ScannerID string
}
Options configures an Engine.
type Position ¶
type Position int
Position classifies where a phrase match sits in the surrounding text, so the soft checks can tell a genuine embedded instruction ("ignore previous instructions") apart from a description that merely talks about such phrases ("detects prompts such as 'ignore previous instructions'"). This is the core false-positive control for legitimate security tooling (FR-009, US2).
const ( // PositionInstruction is an imperative/instruction-position match; full // confidence is kept. PositionInstruction Position = iota // PositionExample is an example/illustration-position match (after a cue // like "such as"/"e.g.", inside quotes, or in a "detects …" list); // confidence is discounted. PositionExample )
func ClassifyPosition ¶
ClassifyPosition decides whether the match starting at byte offset matchStart in text is in instruction- or example-position. text may be raw or normalized; matching is case-insensitive on the preceding window.
type RegistryView ¶
type RegistryView struct {
Tools []ToolView
ToolsByName map[string][]ToolView // name → tools with that name (collision detection)
ToolNames map[string]struct{} // fast membership for cross-tool references
}
RegistryView is a read-only snapshot of every server's current tools, built once per scan and passed to every check so cross-tool checks (shadowing) can reason about collisions and references.
func NewRegistryView ¶
func NewRegistryView(tools []ToolView) RegistryView
NewRegistryView builds the cross-tool indexes and precomputes each tool's NormalizedText (once). It preserves input ordering in Tools and in each ToolsByName slice, which keeps downstream findings deterministic.
type Signal ¶
type Signal struct {
CheckID string // stable identifier, e.g. "unicode.hidden"
Tier Tier // hard (auto-quarantine) vs soft (review)
ThreatType string // maps to ScanFinding.ThreatType
Confidence float64 // 0.0–1.0; already position-discounted for soft signals
Evidence string // render-safe; for payload.decoded this is the decoded content
Detail string // short human explanation
}
Signal is the output of a single Check for a single tool.
type Tier ¶
type Tier int
Tier ranks a signal's contribution to the risk decision.
const ( // TierSoft signals raise a finding for review but never auto-quarantine on // their own. Zero value, so an uninitialized Signal is conservatively soft. TierSoft Tier = iota // TierHard signals contribute to auto-quarantine; checks emitting them are // built for near-zero false positives. TierHard )
type ToolView ¶
type ToolView struct {
Server string
Name string
Description string // raw, un-normalized
InputSchema json.RawMessage
OutputSchema json.RawMessage
NormalizedText string // precomputed Normalize(description + schema text)
}
ToolView is a read-only projection of one tool supplied to checks. An empty description/schema is valid input and yields zero signals (no error).