Skip to content

Detection Engine

The detection engine lives in packages/detections. It is intentionally pure — no I/O, no Node-specific APIs, no network calls. It runs identically in the plugin (in-process) and the backend (for server-side scanning).

Public API

import { scan, redact, registerPack, getLoadedRules } from '@akasecurity/detections';

// Scan a text string against all loaded rules (or a specific set)
const findings = scan(text, rules?);

// Replace matched spans with [REDACTED:CATEGORY] placeholders
const sanitized = redact(text, findings);

// Register a rule pack at startup
registerPack({ id: 'core-pii', rules: [...] });

// Inspect what's loaded
const rules = getLoadedRules();

How scan() works

scan(text, rules)
  ├─ for each rule:
  │    ├─ rule.matcher.type === 'keyword' → KeywordMatcher.match()
  │    ├─ rule.matcher.type === 'regex'   → RegexMatcher.match()
  │    └─ rule.matcher.type === 'validator' → (future)
  └─ returns MatchResult[] with:
       ruleId, category, severity, span {start, end}, rawMatch, confidence

Matchers return zero or more {start, end} spans within the text. The engine wraps each span into a MatchResult with the rule's metadata attached.

Matcher types

keyword

Checks for literal keyword presence. Case-insensitive by default.

{
  "type": "keyword",
  "keywords": ["password", "secret", "api_key", "private_key"],
  "caseSensitive": false
}

regex

Applies a regular expression with optional flags.

{
  "type": "regex",
  "pattern": "\\b(AKIA|ASIA)[A-Z0-9]{16}\\b",
  "flags": "g"
}

Statement-level patterns

Use \b word boundaries in regex patterns to avoid false positives from substrings.

validator (coming soon)

Runs a named validator function (Luhn check for card numbers, Shannon entropy for secrets, SSN checksum). The current schema accepts postValidators as an array of validator names — the engine will apply them as post-match filters in a future release.

How redact() works

Spans are sorted in descending order before replacement so that earlier replacements don't shift the indices of later ones:

const sorted = findings.sort((a, b) => b.span.start - a.span.start);
for (const f of sorted) {
  const placeholder = `[REDACTED:${f.category.toUpperCase()}]`;
  result = result.slice(0, f.span.start) + placeholder + result.slice(f.span.end);
}

The placeholder encodes the category (PII, SECRET, FINANCIAL, etc.) so downstream systems know what was removed.

MatchResult shape

interface MatchResult {
  ruleId: string; // e.g. "secrets/aws-access-key"
  category: string; // e.g. "secret"
  severity: string; // "critical" | "high" | "medium" | "low"
  span: { start: number; end: number };
  rawMatch: string; // the actual matched text
  confidence: number; // 0.0–1.0 (regex matches default to 0.9)
}

Loading rule packs

Rule packs are loaded at startup. In the plugin runtime (packages/plugin-sdk/src/runtime.ts), packs are registered from the backend's policy bundle response. In standalone tests (like the fixture-driven Vitest suite), packs are loaded directly from the rules/ directory.

// From the detection engine test setup:
const rule = JSON.parse(fs.readFileSync('rules/core-pii/email.json'));
const findings = scan(text, [Rule.parse(rule)]);

Fixture-driven testing

Every rule must have a fixtures/ directory with positive and negative test cases. The Vitest test at packages/detections/src/engine.test.ts auto-discovers all rule packs and runs their fixtures.

A fixture file is a JSON array:

[
  { "label": "plain email", "text": "contact user@example.com", "shouldMatch": true },
  { "label": "no TLD", "text": "not-an-email@localhost", "shouldMatch": false }
]

CI fails if any fixture assertion fails. See Writing Rules for the full format.

Adding a new matcher type

  1. Create packages/detections/src/matchers/<type>.ts implementing match(text, rule): Span[]
  2. Register it in engine.ts under the appropriate rule.matcher.type branch
  3. Add a corresponding type literal to the Matcher discriminated union in packages/schema/src/zod/rule.ts
  4. Write fixtures

Validators (built-in, not yet wired)

The validator functions exist in packages/detections/src/validators/:

Validator File What it checks
Luhn luhn.ts Credit/debit card number checksum
Shannon entropy entropy.ts High-entropy strings (likely secrets)

These are used today as standalone utilities. Post-match validator wiring is on the roadmap.