/** * Security Scanner Helper Functions * @module @skillsmith/core/security/scanner/SecurityScanner.helpers */ import type { SecurityFinding, SecurityFindingType, EvidenceType } from './types.js'; /** * Context information for each line in markdown content */ export interface LineContext { lineNumber: number; inCodeBlock: boolean; inTable: boolean; isIndentedCode: boolean; isInlineCode: boolean; /** * SMI-4396 Wave 2: line falls within a YAML frontmatter block * (between opening `---` at file start and the next `---`). SKILL.md * authors legitimately include domain keywords (`password`, `secrets`, * `privilege escalation`) in `description:` fields — findings in * this context are documentation, not code. */ inFrontmatter: boolean; } /** * Analyze markdown content and return context for each line * Used to reduce false positives in documentation/examples * * SMI-4396 Wave 2: tracks YAML frontmatter context (the `---`-fenced block * at the top of a SKILL.md). Opening `---` must be at line 0 (ignoring * leading blank lines); closing `---` ends the block. Lines within are * marked inFrontmatter=true so their keyword matches downgrade to * documentation severity. * * SMI-6033 Wave 2 (Gap 8) fix (2026-08-17): `isMarkdown` defaults `true`, * preserving byte-identical behavior for every existing caller. Pass `false` * when scanning content that is NOT markdown — real source files (the Gap 8 * extended siblings scanned via `bundled-sibling-scan.ts`'s * `collectExecutableCodeFiles`). The indented-code-block heuristic below * (4+ spaces / tab = "documentation example", a real markdown convention) * otherwise silently misclassifies essentially ALL indented Python/Ruby/etc. * control-flow bodies as documentation — verified via the edge-twin repro * (scripts/indexer/_shared/security-scanner-edge.context.ts's identical * fix): a real multi-signal backdoor failed to escalate to critical purely * because the whole function body was 4-space indented. */ export declare function analyzeMarkdownContext(content: string, isMarkdown?: boolean): LineContext[]; /** * Check if a line is in a documentation context (code block, table, example). * Note: isInlineCode is intentionally excluded — it marks the entire line, * but only specific match positions within backtick spans should reduce severity. * Use isWithinInlineCode() for per-span granularity (SMI-3521). * * SMI-4396 Wave 2: inFrontmatter also counts as documentation context. * SKILL.md authors legitimately include domain keywords in description: * fields (1Password integrations, security-research skills, etc.). */ export declare function isDocumentationContext(ctx: LineContext): boolean; /** * SMI-3521: Check if a match position falls within an inline code span (backtick-delimited). * Unlike the line-level isInlineCode flag, this provides per-span granularity: * only content actually between backticks is considered inline code. */ export declare function isWithinInlineCode(line: string, matchIndex: number): boolean; /** * SMI-5879 (design §3.3.4): correctness ceiling on distinct lines recorded per * pattern in the pass-1 full-content scan — score-neutral by proof (Lemma * 3.3-B): once one pattern alone has pushed its category's raw subtotal past * the per-category `Math.min(100, …)` cap, no further line from that SAME * pattern can change the post-cap value. Derived floor: * `ceil(100 / min_raw_per_finding)` where `min_raw_per_finding` is the * smallest (severity × category-weight × confidence) reachable from the * multiline pass — today an `ai_defence` `mention` in either context: * `low(5) × 1.9 × low(0.3) = 2.85`, giving `ceil(100/2.85) = 36`. Set to 64 * (1.78x headroom over the derived floor) so a future weight change that * lowers the minimum per-finding contribution doesn't immediately breach it; * `scanner-multiline-cap.test.ts` recomputes the floor from the live weight * tables and asserts this constant stays `>=` it. */ export declare const MAX_MULTILINE_LINES_PER_PATTERN = 64; /** * SMI-5879 (design §3.3.3/3.3.6): wall-clock LIVENESS bound on a single * pattern's pass-1 loop — NOT score-neutral (unlike the line cap above). A * same-line repetition (e.g. 200 matches on one line) costs one iteration per * match even though `seenLines` never grows past 1, so this bounds worst-case * iteration count on a pathological same-line-repetition input. Binding marks * the scan `multilineTruncated`; a truncated scan may only ever RAISE a * verdict, never lower one (design §3.3.6) — enforced by the write path, not * here. */ export declare const MAX_MULTILINE_ITERATIONS_PER_PATTERN = 10000; interface MultilineScanConfig { type: SecurityFindingType; messagePrefix: string; patterns: RegExp[]; /** * SMI-5876: replaces the flat `[docContext, normal]` severity pair — returns * the evidence tier for a given pattern (by object identity) so * severity/confidence can be resolved per-line via `resolveEvidenceSeverity` * once the strongest tier for that line is known. */ classify: (pattern: RegExp) => EvidenceType; } /** * Scan content for patterns that may span multiple lines. * Multi-line patterns are tested against full content; single-line patterns per-line. * * SMI-5876: patterns within a single category (jailbreak / ai_defence) can now * carry DIFFERENT evidence tiers (a bare "jailbreak" mention vs. an explicit * "ignore all previous instructions" override), so array-declaration order is * no longer sufficient to decide which match wins on a line where multiple * patterns fire — the STRONGEST evidence tier per line wins, computed via a * merge across both passes (`bestByLine`), not "first match, in array order." * * Two hazards this closes (see the SMI-5876 design doc §5 for the full * argument): * Hazard A — pass 2 used to `break` on the FIRST matching pattern * regardless of tier, so a weaker mention declared earlier in the array * could shadow a stronger directive declared later on the same line. * Hazard B — a multiline pattern match used to suppress ALL single-line * patterns on that line (`flaggedLines`), so a mention-tier multiline * match could hide a directive-tier single-line match on the same line. * `bestByLine` SEEDS pass 2 with pass 1's result instead of skipping the * line outright, so pass 2 can still find something stronger. */ export interface MultilineScanResult { findings: SecurityFinding[]; /** * SMI-5879 (design §3.3.6): true when at least one pattern's pass-1 loop * hit MAX_MULTILINE_ITERATIONS_PER_PATTERN before exhausting its matches. * NOT provably score-neutral (unlike the line cap) — a truncated scan may * under-count. The write path must treat a truncated scan as authoritative * for RAISING a verdict only, never for lowering one or clearing an * existing quarantine. */ truncated: boolean; } export declare function scanPatternsWithMultilineSupport(content: string, config: MultilineScanConfig, lineContexts?: LineContext[], /** * SMI-5881: explicit cap (UTF-16 code units) for the pass-1 full-content * regex scan — `Math.min(MAX_CONTENT_LENGTH_FOR_REGEX, maxContentLength)`, * computed once by SecurityScanner.scan() and threaded through so the * truncation finding it emits matches what actually gets scanned. Omitted * (undefined) falls back to safeRegexTest's own default. */ maxLength?: number): MultilineScanResult; export {}; //# sourceMappingURL=SecurityScanner.helpers.d.ts.map