/** * ATR Engine - Evaluates agent events against ATR rules * * Core detection engine that: * 1. Loads ATR YAML rules from disk * 2. Evaluates agent events (LLM I/O, tool calls, behaviors) against rules * 3. Returns matched rules with confidence scores * 4. Supports two condition formats: * - Array format: conditions is an array of {field, operator, value} objects * - Named format: conditions is an object map of named condition blocks * * @module agent-threat-rules/engine */ import type { ATRRule, ATRMatch, AgentEvent, ATRVerdict, ActionResult, ScanResult, ATRLanguage, ATRSemanticJudge } from './types.js'; import { type Lane } from './quality/rule-contract.js'; import type { SessionTracker } from './session-tracker.js'; import { type ModelDecisions } from './verdict.js'; import type { ActionExecutor } from './action-executor.js'; import type { SkillFingerprintStore } from './skill-fingerprint.js'; import type { SemanticLayerConfig } from './layer-integration.js'; import type { InvariantChecker } from './tier0-invariant.js'; import type { BlacklistProvider } from './tier1-blacklist.js'; import type { EmbeddingModule } from './modules/embedding.js'; export { needsUnicodeFlag, regexConditionSpec } from './regex-dialect.js'; export type { RegexConditionSpec } from './regex-dialect.js'; /** * Detect and decode base64-encoded blocks in content. * Bounds: max 1 level decode, max 5 blocks, min 32 chars per block, * MAX_EVAL_LENGTH per decoded block. Returns decoded text fragments. * The result depends on `content` alone (SPEC §6.5). */ export declare function decodeBase64Blocks(content: string): string[]; /** Map agent event types to ATR source types */ export declare const EVENT_TYPE_TO_SOURCE: Readonly>; /** * agent_source types that name what a rule reads rather than a stream of * events of their own (spec/atr-schema.yaml), mapped to the source type whose * events carry that data: a context window reaches the model as LLM input or * as tool output, so those rules run where llm_io rules run; a skill is * installed, granted permissions and chained through tool calls, so those * rules run where tool_call rules run. memory_access is left out for now * (WITHHELD_SOURCE_TYPES). * * No event type maps to them in EVENT_TYPE_TO_SOURCE, so the source filter used * to skip them on every typed event: on the tool_call and tool_response events * src/hook-handler.ts builds, such a rule never ran, while `atr test` mapped its * type to a base type and passed its cases. Only an event with no source type * (mcp_exchange) or a SKILL.md scan reached it. `atr test` now leaves the type * alone: what it runs is what the engine admits. */ export declare const RUNTIME_SOURCE_TYPE: Readonly>; /** * Extended agent_source types left out of RUNTIME_SOURCE_TYPE for now, each * with the reason. A rule of such a type still runs only on an event with no * source type and in a SKILL.md scan. `atr test` reports its cases as * unevaluable with this reason instead of passing them on an event the engine * skips the rule on. * * memory_access reads what context_window reads, an agent's memory as it * reaches the model, and would map to llm_io. Its rules (ATR-2026-00075 and * -01155 today) were written for memory writes, and 00075 also matches standing * instructions in an ordinary chat turn ("From now on, always answer in * French."). On llm_input events the adapters that block by default would * refuse those messages: the Mastra ATRProcessor aborts on high and critical * in the hunt lane, and the OpenShell filter denies at high. Map it once those * rules match memory writes only, measured on chat-turn data. */ export declare const WITHHELD_SOURCE_TYPES: Readonly>; /** * Whether a rule of this agent_source.type runs on an event of this source * type: the rule's source type (after RUNTIME_SOURCE_TYPE) is the event's, or * one of two documented exceptions holds. An event type with no source type * (mcp_exchange) admits every rule, whatever its agent_source holds; on a typed * event a type that is not a string is admitted by nothing. * sourceAdmits() adds the trace exception. */ export declare function sourceTypeAdmits(ruleSourceType: unknown, eventSourceType: string | undefined): boolean; /** * Detection reporter — opt-in callback for feeding scan results back to * ATR Threat Cloud. Every integration endpoint that enables this becomes * a sensor in the global detection network. Anonymized detection events * are aggregated across all endpoints to crystallize new rules. * * Privacy: only ruleId, severity, and scanTarget are reported by default. * No raw content, no PII, no file paths. Platforms can override onDetection * to control exactly what is sent. */ export interface ATRReporter { /** Called for every match. Implement to POST anonymized data to TC. */ readonly onDetection: (report: ATRDetectionReport) => void | Promise; /** Called when a scan completes with zero matches (opt-in). */ readonly onClean?: (report: ATRCleanReport) => void | Promise; } export interface ATRDetectionReport { readonly ruleId: string; readonly severity: string; readonly scanTarget: string; readonly category: string; readonly confidence: number; readonly timestamp: string; /** Content hash (SHA-256) — identifies the scanned artifact without revealing content */ readonly contentHash: string; } export interface ATRCleanReport { readonly rulesEvaluated: number; readonly scanTarget: string; readonly timestamp: string; readonly contentHash: string; } export interface ATREngineConfig { /** Directory containing ATR rule YAML files */ rulesDir?: string; /** Pre-loaded rules (for testing or embedding) */ rules?: ATRRule[]; /** Optional session tracker for behavioral detection across events */ sessionTracker?: SessionTracker; /** Optional Tier 0: Invariant enforcement (hard boundaries, pre-check) */ invariantChecker?: InvariantChecker; /** Optional Tier 1: Skill blacklist provider (known-bad lookup) */ blacklistProvider?: BlacklistProvider; /** Optional Layer 2: Skill behavioral fingerprinting (no LLM required) */ fingerprintStore?: SkillFingerprintStore; /** Optional Tier 2.5: Embedding similarity module (requires @xenova/transformers) */ embeddingModule?: EmbeddingModule; /** Optional Layer 3: Semantic LLM-as-judge analysis (requires API key) */ semanticModule?: SemanticLayerConfig; /** Optional rule-level semantic judge for detection.method=semantic rules */ semanticJudge?: ATRSemanticJudge; /** Optional: detection reporter for feeding results to ATR Threat Cloud */ reporter?: ATRReporter; /** * Detection lane — controls which rule maturities are allowed to fire. * 'enforce' : only maturity=stable rules (auto-block lane, lowest FP). * 'alert' : maturity stable + test (analyst/correlation lane). * 'hunt' : all maturities (advisory/eval; default, backward-compatible). * Deprecated/draft rules are always skipped regardless of lane. * * This field is the ONLY input: the engine does not read `ATR_LANE` or any * other environment variable. Leaving it undefined pins the lane to 'hunt', * the widest and most advisory setting, no matter what the surrounding shell * says. A CLI that wants environment support resolves it itself and passes * the result here (src/cli.ts, resolveEnforcementPolicy). * * An unrecognised value throws at construction time — that is a caller bug, * not ambient noise. */ lane?: 'enforce' | 'alert' | 'hunt'; /** * Embedding-confirm threshold for rules flagged `confirm: embedding`. * In enforce/alert lanes, such a rule's match is dropped unless the input's * cosine similarity to the known-attack reference >= this value. Requires an * embeddingModule; if absent, confirm-rules are dropped from enforce/alert * (they are too broad to block without confirmation). Default 0.6. */ confirmThreshold?: number; /** * What a match a model decided may do in evaluateWithVerdict(): a semantic * rule's semanticJudge, the embeddingModule signal, the semanticModule layer. * 'advisory' (default): reported, observe-tier actions only, no part in the * verdict's outcome. * 'enforce': counts like a rule's own match. No benign-corpus measurement * stands behind a model's decision; choose it only if you accept that. * Any other value throws at construction time. */ modelDecisions?: ModelDecisions; /** * Every event this engine evaluates is an MCP tool definition: an entry of a `tools/list` * document, which defines a tool and calls none (`atr scan` on such a document sets it). Rules * written for a tool being called then do not run: a rule with agent_source.type `tool_call` runs * only if one of its conditions reads `tool_description`, or if its subject is the tool's identity * (TOOL_IDENTITY_RULES). Every other rule runs as without the option. See * src/tool-definition-scope.ts. Default false: other producers (hooks, adapters, event files) are * unaffected. */ toolDefinitions?: boolean; } export declare class ATREngine { private readonly config; private rules; /** * id -> the FIRST loaded rule carrying that id, i.e. exactly what * `this.rules.find(r => r.id === id)` returns. * * The per-condition paths below used to run that find() on every regex * condition they evaluated: a linear walk over every loaded rule, repeated * thousands of times per event. Measured 2026-10-15 on the benign-corpus FP * scan (scripts/gate-promotion-fp.ts, 829 rules, 13,848 samples), that walk * was ~11% of all CPU and replacing it was a 1.18x speed-up on its own, with * the matched-rule set identical for every sample. */ private readonly ruleById; private readonly compiledPatterns; private readonly semanticModuleInstance; /** * Active detection lane, resolved ONCE at construction from config alone. * Held as a field so every gate in this engine sees the same lane for its * lifetime. */ private readonly lane; private readonly modelDecisions; /** * Find bundled rules directory shipped with the npm package. * Checks: ../rules (from dist/), ./rules (repo root) */ private findBundledRulesDir; constructor(config?: ATREngineConfig); /** * Load rules from configured directory and/or pre-loaded rules. */ loadRules(): Promise; /** First rule wins, so the index answers what Array.find() would. */ private indexRule; /** * Lane gate: does this rule's maturity qualify for the configured detection * lane? 'enforce' = stable only; 'alert' = stable+test; 'hunt'/unset = all. * Keeps experimental content out of the auto-block path without deleting it. */ private passesLane; /** * Whether this engine evaluates `rule` at all. A draft or deprecated status, a * maturity outside the lane, or (on an engine constructed with * `toolDefinitions: true`) a rule written for a tool being called, stops a rule * before any of its conditions run, * so nothing about it — its false positives included — can be observed through * this engine. evaluateRaw() and evaluateAsync() both ask this, and a * measurement harness must ask it too before recording a number for a rule: * a rule that is never evaluated scores 0 by construction, and that 0 is not * a measurement. */ evaluates(rule: Pick & Partial>): boolean; /** * On an engine constructed with `toolDefinitions: true`: how many loaded rules it evaluates, and how * many it would evaluate (status and lane allow them) but leaves out because they were written for * a tool being called. Null on any other engine. Lets a report say "not run" instead of reading as * "ran and found nothing". */ getToolDefinitionScope(): { rulesEvaluated: number; rulesSkippedByScope: number; } | null; /** Active detection lane (config, else 'hunt'). Diagnostics/tests. */ getLane(): Lane; /** * What a model-decided match may do (config, else 'advisory'). A caller that * runs evaluateAsync() and computes the verdict itself passes this on: * computeVerdict(matches, { modelDecisions: engine.getModelDecisions() }). */ getModelDecisions(): ModelDecisions; /** * Load a single rule file and add it to the engine. */ addRuleFile(filePath: string): void; /** * Add a pre-parsed rule to the engine. */ addRule(rule: ATRRule): void; /** * Evaluate an agent event against all loaded ATR rules (synchronous, regex only). * In enforce/alert lanes, rules flagged `confirm: embedding` are EXCLUDED here — * embedding confirmation is asynchronous and cannot run in this path, so a broad * confirm-rule must not fire unconfirmed in a blocking lane. Use * evaluateWithVerdict() for confirmed enforce/alert detection. */ evaluate(event: AgentEvent): ATRMatch[]; /** * Raw synchronous evaluation: returns all lane-passing matches including * UNCONFIRMED `confirm: embedding` rules. Used ONLY internally by the verdict * path, which then applies the async embedding-confirm gate. * * INTERNAL — do not call from outside evaluateWithVerdict. Calling this directly * bypasses the embedding-confirm gate, letting broad confirm-rules fire * unconfirmed in enforce/alert. The public sync entry point is evaluate(). */ private evaluateRaw; /** * Async evaluation path that supports rule-level method=semantic dispatch. * * The synchronous evaluate() method remains pattern-only for compatibility. * Consumers that configure semanticJudge should call evaluateAsync() or * evaluateWithVerdict(), which delegates here when a semantic judge exists. */ evaluateAsync(event: AgentEvent): Promise; /** * Evaluate a single rule against an event. * Supports both array-format and named-map-format conditions. */ private evaluateRule; /** Evaluate a rule using pattern-mode conditions, regardless of detection.method. */ private evaluatePatternRule; /** * Async variant that supports method=semantic with an injected judge. * For trace/pattern/signature/behavioral methods, defers to the sync path. */ private evaluateRuleAsync; /** * Minimal signature-method evaluator (atr-method-v1.1.md §5). * Walks detection.signature.indicators against event.fields with the * specified match_logic. Hash-typed indicators expect event.fields to * contain pre-computed hex hashes at the indicated target_field. */ private evaluateSignatureMethod; /** * Evaluate array-format conditions: [{field, operator, value}, ...] * with condition: "any" | "all" */ private evaluateArrayConditions; /** * Evaluate a single array-format condition {field, operator, value}. */ private evaluateArrayCondition; /** * Evaluate named-map-format conditions: {name: {field, patterns, match_type}, ...} * with condition: "name1 AND name2" | "name1 OR name2" | "name1" */ private evaluateNamedConditions; /** * Evaluate a single named condition against an event. */ private evaluateNamedCondition; /** * Evaluate a pattern matching condition (named format with patterns array). */ private evaluatePatternCondition; /** * Determine if a rule should suppress matches inside markdown code blocks. * Rules that commonly false-positive on documentation (shell commands, file paths, * code examples) are suppressed. Prompt injection rules are NEVER suppressed * because attackers deliberately hide payloads in code blocks. */ private shouldSuppressInCodeBlocks; /** * Evaluate a behavioral threshold condition. * When a session tracker is available and the event has a sessionId, * supports session-derived metrics: call_frequency, pattern_frequency, event_count. */ private evaluateBehavioralCondition; /** * Resolve a metric value from event metrics or session tracker. * Session-derived metrics use the format: "call_frequency:toolName" or "pattern_frequency:pattern". */ private resolveMetricValue; /** * Parse a window string (e.g. "5m", "1h", "30s") to milliseconds. * Defaults to 5 minutes if not specified or unparseable. */ private parseWindowMs; /** * Evaluate a sequence condition against the current event. * * Two modes: * 1. Session-aware (when SessionTracker + sessionId available): * Checks patterns across historical events in the session. * Respects `ordered` flag and `within` time window. * 2. Single-event fallback: checks if patterns co-occur in one event. */ private evaluateSequenceCondition; /** * Cross-event sequence detection using SessionTracker. * Checks if step patterns have been seen across events in order. */ private evaluateSequenceAcrossSession; /** * Single-event fallback: check if step patterns co-occur in one event. */ private evaluateSequenceSingleEvent; /** * Resolve a field value from an agent event. */ private resolveField; /** * Evaluate a boolean expression string against condition results. * Supports AND, OR, NOT operators. */ private evaluateExpression; /** * Split expression by operator, respecting parentheses. */ private splitByOperator; /** * Pre-compile regex patterns for a rule (performance optimization). * Supports both array-format and named-map-format conditions. */ private compilePatterns; /** * Evaluate an event and compute a verdict with optional action execution. * * Combines evaluate() + computeVerdict() + optional ActionExecutor * into a single call for convenience. */ evaluateWithVerdict(event: AgentEvent, executor?: ActionExecutor): Promise<{ verdict: ATRVerdict; actionResults: readonly ActionResult[]; layersUsed: readonly string[]; }>; /** Get loaded rule count */ getRuleCount(): number; /** Get all loaded rules */ getRules(): readonly ATRRule[]; /** Get a rule by ID */ getRuleById(id: string): ATRRule | undefined; /** Get rules by category */ getRulesByCategory(category: string): ATRRule[]; /** * Scan SKILL.md content for threats. * All rules fire with scanContext='skill': * - skill/both rules: native context, full confidence * - MCP-only rules: cross-context, confidence * 0.6 * Also decodes base64 blocks and scans decoded content. * Code-block suppression and FP denylist applied in evaluate(). */ scanSkill(content: string): ATRMatch[]; /** * Async SKILL.md scan that supports method=semantic rules through semanticJudge. */ scanSkillAsync(content: string): Promise; /** Scan a SKILL.md file and return a unified ScanResult with content_hash. */ scanSkillFull(content: string, filePath?: string): ScanResult; /** Async SKILL.md scan result with semantic rule support. */ scanSkillFullAsync(content: string, filePath?: string): Promise; /** Evaluate an MCP agent event and return a unified ScanResult with content_hash. */ evaluateFull(event: AgentEvent, filePath?: string): ScanResult; /** Async MCP event scan result with semantic rule support. */ evaluateFullAsync(event: AgentEvent, filePath?: string): Promise; } /** * Compile an array-format `operator: regex` value the way the engine does: the * leading flag group handled by regexConditionSpec(), then the ReDoS gate in * safeCompile(). Returns null for a pattern the gate rejects or that does not * compile; such a condition never matches. Exported for code outside * ATREngine, such as ShadowEvaluator and RuleScaffolder, that must compile a * condition exactly as the engine does. * * @internal Not re-exported from the package entry point. */ export declare function compileRegexCondition(value: string, ruleId?: string): RegExp | null; export declare function normalizeUnicode(text: string): string; export declare function foldConfusables(text: string): string; /** * Heuristic dominant-script detection for v3.0 multilingual dispatch. * * Counts Unicode-block code points and returns the BCP-47 tag of the * dominant script. Used to skip language-tagged conditions whose * declared language does not match the input \u2014 pure optimisation, never * affects correctness of language-untagged rules. * * Disambiguation: * - Han script: split via simplified-only vs traditional-only indicator * char sets. Tie / both zero \u2192 defaults to 'zh-Hant'. * - Latin script: 'es' if Spanish-specific punctuation/diacritics * (\u00F1, \u00BF, \u00A1) detected, else 'en'. * - Empty / pure ASCII without Spanish markers \u2192 'en'. * * Exported for unit testing; not part of the public API surface. */ export declare function detectInputLanguage(text: string): ATRLanguage; /** * Decide whether a condition with `language: condLang` should be evaluated * against an input whose detected language is `inputLang`. * * Rules: * - condLang undefined (no language field) \u2192 always evaluate (v2.x compat) * - condLang === inputLang \u2192 evaluate * - Han-script ambiguity: if input is Han and condLang is the other * Chinese variant, still evaluate (the cheap detector cannot reliably * split zh-Hant vs zh-Hans, so we err on inclusion) * - Otherwise \u2192 skip (return false) * * Exported for unit testing; not part of the public API surface. */ export declare function conditionLanguageMatches(condLang: ATRLanguage | undefined, inputLang: ATRLanguage): boolean; /** * Safely test a regex pattern against input with length limits. * Returns false if input exceeds MAX_EVAL_LENGTH to prevent ReDoS. * Exported so ShadowEvaluator and RuleScaffolder apply the same limit as the engine. * * @internal Not re-exported from the package entry point. */ export declare function safeRegexTest(regex: RegExp, input: string): boolean; /** * Heuristic catastrophic-backtracking (ReDoS) detector for rule-derived * patterns. * * Why compile-time rejection is the ONLY real defense: a synchronous RegExp * cannot be interrupted once it starts. Node runs it to completion on the * single event-loop thread, so the hook-handler's withTimeout() (a setTimeout * race) can never fire while a pathological match is spinning — the timer * callback is queued behind the very work it is meant to abort. The length cap * in safeRegexTest bounds one axis; this bounds the other by refusing to * compile a pattern whose STRUCTURE permits exponential backtracking, before * any attacker-controlled input can reach it. * * Mirrors scan-core's isSafeRegex, which has run against the full bundled rule * corpus without dropping a single legitimate rule. */ export declare function isReDoSSafe(source: string): boolean; //# sourceMappingURL=engine.d.ts.map