detect

package
v0.47.0-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: MIT Imports: 6 Imported by: 0

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

View Source
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.

View Source
const (
	ThreatLevelDangerous = "dangerous" // any hard signal → auto-quarantine
	ThreatLevelWarning   = "warning"   // soft-only → review
	ThreatLevelInfo      = "info"
)

Threat levels — user-facing severity, mirrors scanner.ThreatLevel*.

View Source
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.

View Source
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

func CapEvidence(s string) string

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

func ClampConfidence(c float64) float64

ClampConfidence constrains a confidence to the valid [0,1] range.

func Normalize

func Normalize(s string) string

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

type Coverage struct {
	ChecksRun      int
	ChecksFailed   int
	FailedCheckIDs []string
}

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 NewEngine

func NewEngine(opts Options) *Engine

NewEngine builds an Engine from Options.

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

func ClassifyPosition(text string, matchStart int) Position

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.

func (Position) Discount

func (p Position) Discount() float64

Discount returns the confidence multiplier for a position: 1.0 for instruction-position, exampleDiscount for example-position.

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 Result

type Result struct {
	Findings []Finding
	Coverage Coverage
}

Result is the output of a scan.

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
)

func (Tier) String

func (t Tier) String() string

String renders the tier for logs and evidence.

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).

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL