@resq-systems/security 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/controls/address.d.mts +8 -8
- package/lib/controls/address.d.mts.map +1 -1
- package/lib/controls/address.mjs.map +1 -1
- package/lib/controls/csrf.d.mts +7 -7
- package/lib/controls/csrf.d.mts.map +1 -1
- package/lib/controls/csrf.mjs +5 -2
- package/lib/controls/csrf.mjs.map +1 -1
- package/lib/controls/origin.d.mts +6 -6
- package/lib/controls/origin.d.mts.map +1 -1
- package/lib/controls/origin.mjs +1 -0
- package/lib/controls/origin.mjs.map +1 -1
- package/lib/controls/payload.d.mts +4 -4
- package/lib/controls/payload.d.mts.map +1 -1
- package/lib/controls/payload.mjs.map +1 -1
- package/lib/controls/query.d.mts +8 -8
- package/lib/controls/query.d.mts.map +1 -1
- package/lib/controls/query.mjs +1 -0
- package/lib/controls/query.mjs.map +1 -1
- package/lib/controls/redirect.d.mts +5 -5
- package/lib/controls/redirect.d.mts.map +1 -1
- package/lib/controls/redirect.mjs.map +1 -1
- package/lib/controls/upload.d.mts +7 -7
- package/lib/controls/upload.d.mts.map +1 -1
- package/lib/controls/upload.mjs.map +1 -1
- package/lib/crypto.d.mts +20 -21
- package/lib/crypto.d.mts.map +1 -1
- package/lib/crypto.mjs +3 -1
- package/lib/crypto.mjs.map +1 -1
- package/lib/hash.d.mts +5 -5
- package/lib/hash.d.mts.map +1 -1
- package/lib/hash.mjs +1 -0
- package/lib/hash.mjs.map +1 -1
- package/lib/paths.d.mts +5 -5
- package/lib/paths.d.mts.map +1 -1
- package/lib/paths.mjs +1 -0
- package/lib/paths.mjs.map +1 -1
- package/lib/sanitize.d.mts +36 -37
- package/lib/sanitize.d.mts.map +1 -1
- package/lib/sanitize.mjs.map +1 -1
- package/lib/threats/capec.generated.d.mts +4 -4
- package/lib/threats/capec.generated.d.mts.map +1 -1
- package/lib/threats/capec.generated.mjs.map +1 -1
- package/lib/threats/engine.d.mts +4 -5
- package/lib/threats/engine.d.mts.map +1 -1
- package/lib/threats/engine.mjs +1 -0
- package/lib/threats/engine.mjs.map +1 -1
- package/lib/threats/rules/datastore.d.mts +4 -5
- package/lib/threats/rules/datastore.d.mts.map +1 -1
- package/lib/threats/rules/datastore.mjs.map +1 -1
- package/lib/threats/rules/index.d.mts +5 -5
- package/lib/threats/rules/index.d.mts.map +1 -1
- package/lib/threats/rules/index.mjs.map +1 -1
- package/lib/threats/rules/markup.d.mts +4 -5
- package/lib/threats/rules/markup.d.mts.map +1 -1
- package/lib/threats/rules/markup.mjs +1 -0
- package/lib/threats/rules/markup.mjs.map +1 -1
- package/lib/threats/rules/protocol.d.mts +4 -5
- package/lib/threats/rules/protocol.d.mts.map +1 -1
- package/lib/threats/rules/protocol.mjs.map +1 -1
- package/lib/threats/rules/system.d.mts +4 -5
- package/lib/threats/rules/system.d.mts.map +1 -1
- package/lib/threats/rules/system.mjs.map +1 -1
- package/lib/threats/rules/web.d.mts +5 -6
- package/lib/threats/rules/web.d.mts.map +1 -1
- package/lib/threats/rules/web.mjs +1 -1
- package/lib/threats/rules/web.mjs.map +1 -1
- package/lib/threats/scoring.d.mts +5 -6
- package/lib/threats/scoring.d.mts.map +1 -1
- package/lib/threats/scoring.mjs +1 -0
- package/lib/threats/scoring.mjs.map +1 -1
- package/lib/threats/types.d.mts +18 -18
- package/lib/threats/types.d.mts.map +1 -1
- package/lib/threats/types.mjs.map +1 -1
- package/lib/threats/variants.d.mts +3 -4
- package/lib/threats/variants.d.mts.map +1 -1
- package/lib/threats/variants.mjs.map +1 -1
- package/lib/unicode/confusables.d.mts +5 -5
- package/lib/unicode/confusables.d.mts.map +1 -1
- package/lib/unicode/confusables.mjs +1 -0
- package/lib/unicode/confusables.mjs.map +1 -1
- package/lib/unicode/index.d.mts +11 -11
- package/lib/unicode/index.d.mts.map +1 -1
- package/lib/unicode/index.mjs +1 -0
- package/lib/unicode/index.mjs.map +1 -1
- package/lib/validators.d.mts +89 -39
- package/lib/validators.d.mts.map +1 -1
- package/lib/validators.mjs +285 -21
- package/lib/validators.mjs.map +1 -1
- package/package.json +7 -7
package/lib/sanitize.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitize.mjs","names":["S"],"sources":["../src/sanitize.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Type-safe input sanitization built on Effect Schema — HTML escaping\n * and DOMPurify-backed HTML sanitization, safe-URL validation, prototype-pollution-\n * hardened JSON parsing, PII redaction, and branded validators for email, phone, SSN,\n * and more. Supports NIST 800-53 SI-10 (input validation) and AU-3 (audit content).\n *\n * @module @resq-systems/security/sanitize\n */\n\nimport type { Brand } from \"@resq-systems/types\";\nimport { Exit, Option, Schema as S } from \"effect\";\nimport DOMPurify from \"dompurify\";\nimport type { Config, WindowLike } from \"dompurify\";\n\n//#region Types\n\n/**\n * A Schema whose decoding services are constrained to `never`, allowing synchronous\n * decoding without an Effect runtime.\n */\ntype SyncSchema<T> = S.Codec<T, unknown, never>;\n//#endregion\n\n//#region Schemas\n\n/**\n * Schema constraining a URL protocol to the recognized safe set.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const UrlProtocolSchema = S.Literals([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"ftp:\"]);\n/** One of the protocols accepted by {@link UrlProtocolSchema}. */\nexport type UrlProtocol = typeof UrlProtocolSchema.Type;\n\n/**\n * Schema for the per-category toggles that drive {@link redactPII}.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nexport const PIIRedactionOptionsSchema = S.Struct({\n\tredactEmails: S.optional(S.Boolean),\n\tredactPhones: S.optional(S.Boolean),\n\tredactSSN: S.optional(S.Boolean),\n\tredactCreditCards: S.optional(S.Boolean),\n\tredactIPs: S.optional(S.Boolean),\n\tredactDates: S.optional(S.Boolean),\n});\n/** Decoded options accepted by {@link redactPIIEffect} / {@link redactPII}. */\nexport type PIIRedactionOptions = typeof PIIRedactionOptionsSchema.Type;\n\n/**\n * Schema for the options controlling {@link validateUserInputEffect}.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const UserInputOptionsSchema = S.Struct({\n\tmaxLength: S.optional(S.Int.check(S.isGreaterThan(0))),\n\tallowHtml: S.optional(S.Boolean),\n\tallowNewlines: S.optional(S.Boolean),\n\ttrimWhitespace: S.optional(S.Boolean),\n});\n/** Decoded options accepted by {@link validateUserInputEffect}. */\nexport type UserInputOptions = typeof UserInputOptionsSchema.Type;\n\n/**\n * Schema for a safe URL — validates URL format and restricts to safe protocols.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\n/**\n * Whether a reference opens a URL *authority* component — i.e. can name a different\n * host once resolved against a base.\n *\n * `//evil.example` resolved against `https://trusted.example/page` yields host\n * `evil.example`, and so do `///evil.example` and `/\\evil.example`: the WHATWG URL\n * parser treats a backslash as a slash in the relative-slash state.\n *\n * This must be checked **before** the root-relative fast path. A `startsWith(\"//\")`\n * guard alone is insufficient twice over: it misses the backslash forms entirely, and\n * `//evil.example` that falls through to `new URL()` throws without a base and lands in\n * the catch branch, whose `[a-zA-Z0-9/_.-]` character class happily accepts it.\n *\n * @internal\n */\nconst opensAuthority = (url: string): boolean => /^[/\\\\]{2}/.test(url.trim());\n\nexport const SafeUrlSchema = S.String.check(\n\tS.makeFilter(\n\t\t(url: string) => {\n\t\t\tif (!url || url.trim() === \"\") return false;\n\t\t\tif (opensAuthority(url)) return false;\n\t\t\tif (url.startsWith(\"/\")) return true;\n\t\t\ttry {\n\t\t\t\tconst parsed = new URL(url);\n\t\t\t\tconst safeProtocols = [\"http:\", \"https:\", \"mailto:\"];\n\t\t\t\treturn safeProtocols.includes(parsed.protocol);\n\t\t\t} catch {\n\t\t\t\treturn /^[a-zA-Z0-9/_.-]+$/.test(url);\n\t\t\t}\n\t\t},\n\t\t{ message: \"Invalid or unsafe URL\" },\n\t),\n);\n/**\n * A URL string vouched safe against scheme-based injection. Mint one by\n * narrowing through the {@link isValidUrl} type guard (backed by\n * {@link SafeUrlSchema}); the brand guarantees the value is either a\n * root-relative path or an absolute URL restricted to `http:`/`https:`/\n * `mailto:`. An authority-opening reference (`//host`, `///host`, `/\\host`) is\n * rejected, since resolving one against a base yields a different host.\n *\n * It does **not** guarantee the host is reachable or trusted — for that, validate the\n * resolved origin with `isAllowedOrigin` from `@resq-systems/security/controls`.\n */\nexport type SafeUrl = Brand<string, \"SafeUrl\">;\n\n/**\n * Schema for a sanitized HTML-safe string — validates the value is a string; the\n * actual escaping is applied at runtime by the sanitization helpers.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SanitizedStringSchema = S.String;\n/**\n * A string carrying the {@link SanitizedStringSchema} contract. The schema is\n * `S.String` alone, so decoding asserts only that the value is a string —\n * the actual escaping is applied separately by the sanitization helpers\n * (e.g. {@link escapeHtml}). The type name signals intent, not a proof of\n * escaping.\n */\nexport type SanitizedString = typeof SanitizedStringSchema.Type;\n\n/**\n * Schema for email address validation.\n *\n * Accepts a 2+ character alphabetic TLD or a Punycode/IDN `xn--…` TLD (e.g.\n * `.xn--p1ai` for `.рф`) so internationalized domains are not rejected. Kept in\n * sync with `@resq-systems/email-templates`'s `EmailAddress` brand.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const EmailSchema = S.String.check(\n\tS.isPattern(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.(?:[A-Za-z]{2,}|xn--[A-Za-z0-9-]+)$/),\n);\n/**\n * An email address that matches {@link EmailSchema}. Mint one by narrowing\n * through the {@link isValidEmail} type guard. The brand guarantees only\n * syntactic well-formedness (including IDN/Punycode TLDs) — not that the\n * mailbox exists or is deliverable.\n */\nexport type Email = Brand<string, \"Email\">;\n\n/**\n * Schema for phone number validation (US format).\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const PhoneNumberSchema = S.String.check(\n\tS.isPattern(/^(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}$/),\n);\n/**\n * A US-format phone number matching {@link PhoneNumberSchema}. Mint one by\n * narrowing through the {@link isValidPhone} type guard. The brand asserts\n * the digit/separator shape only; it neither normalizes formatting nor\n * confirms the number is assigned.\n */\nexport type PhoneNumber = Brand<string, \"PhoneNumber\">;\n\n/**\n * Schema for SSN validation (US format).\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SSNSchema = S.String.check(S.isPattern(/^\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{4}$/));\n/**\n * A US Social Security Number matching {@link SSNSchema}. Mint one by\n * narrowing through the {@link isValidSSN} type guard. The brand asserts\n * the `NNN-NN-NNNN` shape only — it does not validate area/group ranges or\n * confirm the number was ever issued. Treat any value as sensitive PII.\n */\nexport type SSN = Brand<string, \"SSN\">;\n\n/**\n * Schema for credit card number validation.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const CreditCardSchema = S.String.check(\n\tS.isPattern(/^(?:\\d{4}[-\\s]?){3}\\d{4}$|^\\d{15,16}$/),\n);\n/**\n * A card number matching the {@link CreditCardSchema} pattern (13–16 digits\n * with optional group separators). No exported type guard mints this brand;\n * decode {@link CreditCardSchema} directly at the boundary. The pattern is a\n * shape check only — it performs **no** Luhn checksum and does not identify\n * the issuer. Treat any value as sensitive PII.\n */\nexport type CreditCard = Brand<string, \"CreditCard\">;\n\n/**\n * Schema for IPv4 address validation.\n */\nexport const IPv4Schema = S.String.check(S.isPattern(/^(?:\\d{1,3}\\.){3}\\d{1,3}$/));\n/**\n * A dotted-quad string matching {@link IPv4Schema}. No exported type guard\n * mints this brand; decode {@link IPv4Schema} directly. The pattern checks\n * four dot-separated groups of 1–3 digits only — it does **not** bound each\n * octet to `0–255`, so `999.0.0.1` still matches.\n */\nexport type IPv4 = Brand<string, \"IPv4\">;\n//#endregion\n\n//#region Sanitization\n\n/**\n * Escapes special HTML characters in a string to their corresponding HTML entities,\n * preventing direct injection of HTML and JavaScript when rendering untrusted content.\n *\n * @param text - The plain text to escape.\n * @returns The escaped string safe for HTML rendering.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * escapeHtml('<script>alert(\"xss\")</script>');\n * // \"<script>alert("xss")</script>\"\n * ```\n */\nexport const escapeHtml = (text: string): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\treturn text\n\t\t.replaceAll(\"&\", \"&\")\n\t\t.replaceAll(\"<\", \"<\")\n\t\t.replaceAll(\">\", \">\")\n\t\t.replaceAll('\"', \""\")\n\t\t.replaceAll(\"'\", \"'\");\n};\n\n/**\n * Validates and sanitizes a user-supplied URL using Effect Schema.\n * Returns an Exit with the sanitized URL or an error.\n *\n * Pure and total — failure is encoded as a resolved {@link Exit.Exit} failure\n * (an `Exit.fail` carrying a {@link S.SchemaError}), never a thrown exception.\n *\n * @param url - The URL to be validated and sanitized.\n * @param allowedProtocols - Allowed URL protocols; a root-relative path (`/foo`) is\n * always accepted regardless of this list. An authority-opening reference — `//host`,\n * `///host`, or `/\\host` — is always **rejected**, because it names a different host\n * once resolved and would otherwise bypass this list entirely.\n * @returns An {@link Exit.Exit}: success carries the accepted URL string,\n * failure carries a {@link S.SchemaError}.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const result = sanitizeUrlEffect('https://example.com');\n * // Exit.succeed('https://example.com')\n *\n * const invalid = sanitizeUrlEffect('javascript:alert(1)');\n * // Exit.fail(...)\n * ```\n */\nexport const sanitizeUrlEffect = (\n\turl: string,\n\tallowedProtocols: readonly UrlProtocol[] = [\"http:\", \"https:\", \"mailto:\"],\n): Exit.Exit<string, S.SchemaError> => {\n\tconst CustomSafeUrlSchema = S.String.check(\n\t\tS.makeFilter(\n\t\t\t(u: string) => {\n\t\t\t\tif (!u || u.trim() === \"\") return false;\n\t\t\t\tconst trimmed = u.trim();\n\t\t\t\t// Before the root-relative fast path: an authority-opening reference can\n\t\t\t\t// name a different host and bypasses `allowedProtocols` entirely.\n\t\t\t\tif (opensAuthority(trimmed)) return false;\n\t\t\t\tif (trimmed.startsWith(\"/\")) return true;\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = new URL(trimmed);\n\t\t\t\t\tif (!allowedProtocols.includes(parsed.protocol)) return false;\n\t\t\t\t\tif (parsed.hostname.includes(\"javascript:\") || parsed.hostname.includes(\"data:\")) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t} catch {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t/^[a-zA-Z0-9/_.-]+$/.test(trimmed) &&\n\t\t\t\t\t\t!trimmed.includes(\"javascript:\") &&\n\t\t\t\t\t\t!trimmed.includes(\"data:\")\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ message: \"Invalid or unsafe URL\" },\n\t\t),\n\t);\n\n\treturn S.decodeUnknownExit(CustomSafeUrlSchema)(url);\n};\n\n/**\n * Validates and sanitizes a user-supplied URL, ensuring it conforms to allowed protocols\n * and is not a vector for injection attacks like `javascript:` or `data:`.\n *\n * @param url - The URL to be validated and sanitized.\n * @param allowedProtocols - Array of allowed URL protocols.\n * @returns The sanitized URL if valid, or an empty string if unsafe.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * sanitizeUrl('https://example.com'); // 'https://example.com'\n * sanitizeUrl('javascript:alert(1)'); // ''\n * ```\n */\nexport const sanitizeUrl = (\n\turl: string,\n\tallowedProtocols: readonly UrlProtocol[] = [\"http:\", \"https:\", \"mailto:\"],\n): string => {\n\tconst result = sanitizeUrlEffect(url, allowedProtocols);\n\treturn Exit.isSuccess(result) ? result.value : \"\";\n};\n\nlet purifyInstance: typeof DOMPurify | null | undefined;\n\n/**\n * Add `rel=\"noopener noreferrer\"` to any link that opens a new browsing context.\n *\n * Without it the opened page receives a live `window.opener` handle and can navigate\n * the original tab to a phishing page — reverse tabnabbing, WSTG-CLNT-14. DOMPurify\n * does not add this by default, and unlike most of CLNT-14 the fix lives inside code\n * this package already owns, so a weakness no signature can detect becomes one that is\n * simply prevented.\n *\n * Modern browsers imply `noopener` for `target=\"_blank\"`; this covers older engines and\n * the named-target case (`target=\"win1\"`), which remains exploitable everywhere.\n *\n * Note that DOMPurify's default configuration strips `target` outright, so this hook is\n * a no-op unless the caller opts back in with `ADD_ATTR: [\"target\"]` or a custom\n * `ALLOWED_ATTR` — which is precisely the configuration that reintroduces the risk.\n *\n * @internal\n */\nconst addNoopenerToTargetedLinks = (node: Element): void => {\n\tif (typeof node.hasAttribute !== \"function\" || !node.hasAttribute(\"target\")) return;\n\n\tconst target = node.getAttribute(\"target\");\n\t// `_self`, `_parent`, and `_top` stay in the current context and grant no handle.\n\tif (target === null || target === \"_self\" || target === \"_parent\" || target === \"_top\") return;\n\n\tconst existing = (node.getAttribute(\"rel\") ?? \"\").split(/\\s+/).filter(Boolean);\n\tfor (const required of [\"noopener\", \"noreferrer\"]) {\n\t\tif (!existing.includes(required)) existing.push(required);\n\t}\n\tnode.setAttribute(\"rel\", existing.join(\" \"));\n};\n\n/** Register the reverse-tabnabbing hook on a DOMPurify instance. */\nconst withHooks = (purify: typeof DOMPurify): typeof DOMPurify => {\n\tpurify.addHook(\"afterSanitizeAttributes\", (node) => {\n\t\taddNoopenerToTargetedLinks(node as unknown as Element);\n\t});\n\treturn purify;\n};\n\nconst getPurify = (): typeof DOMPurify | null => {\n\tif (purifyInstance !== undefined) return purifyInstance;\n\n\tif (typeof window !== \"undefined\") {\n\t\tpurifyInstance = withHooks(DOMPurify);\n\t} else {\n\t\ttry {\n\t\t\t// Resolve `node:module` at runtime (server-side only) via\n\t\t\t// process.getBuiltinModule so browser bundlers never see a static\n\t\t\t// `node:module` import. Available on Node >=20.16 and Bun; absent in\n\t\t\t// browsers, where the `window` branch above is taken instead. jsdom is an\n\t\t\t// optional peer dependency — install it for server-side HTML sanitization.\n\t\t\tconst proc = (globalThis as { process?: { getBuiltinModule?: (m: string) => unknown } })\n\t\t\t\t.process;\n\t\t\tconst nodeModule = proc?.getBuiltinModule?.(\"module\") as\n\t\t\t\t| { createRequire(path: string | URL): (id: string) => unknown }\n\t\t\t\t| undefined;\n\t\t\tif (!nodeModule) {\n\t\t\t\tpurifyInstance = null;\n\t\t\t\treturn purifyInstance;\n\t\t\t}\n\t\t\tconst req = nodeModule.createRequire(import.meta.url);\n\t\t\tconst { JSDOM } = req(\"jsdom\") as {\n\t\t\t\tJSDOM: new (\n\t\t\t\t\thtml?: string,\n\t\t\t\t) => {\n\t\t\t\t\twindow: WindowLike;\n\t\t\t\t};\n\t\t\t};\n\t\t\tconst dom = new JSDOM(\"\");\n\t\t\tpurifyInstance = withHooks(DOMPurify(dom.window));\n\t\t} catch {\n\t\t\tpurifyInstance = null;\n\t\t}\n\t}\n\treturn purifyInstance;\n};\n\n/**\n * Sanitizes HTML to prevent XSS attacks.\n * Uses DOMPurify under the hood. If DOM is not available (e.g. server-side without JSDOM),\n * it falls back to escaping all HTML characters for safety.\n *\n * NOTE: Server-side HTML sanitization requires `jsdom` to be installed in the consuming application\n * environment; otherwise, it will fall back to escaping HTML characters.\n *\n * On the first server-side call this lazily resolves `node:module` and\n * `require`s `jsdom` to build a DOMPurify instance; the instance is cached at\n * module scope, so subsequent calls incur no further module loading. Returns\n * `\"\"` for non-string or empty input and never throws — any loader failure is\n * swallowed and downgraded to {@link escapeHtml}.\n *\n * @param html - The HTML string to sanitize.\n * @param options - Optional DOMPurify configuration.\n * @returns The sanitized HTML string, or the escaped string when no DOM is\n * available.\n */\nexport const sanitizeHtml = (html: string, options?: Config): string => {\n\tif (!html || typeof html !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst purify = getPurify();\n\tif (purify) {\n\t\treturn purify.sanitize(html, options) as string;\n\t}\n\n\treturn escapeHtml(html);\n};\n\n/**\n * Validates user input using Effect Schema and returns an Exit.\n *\n * Strips HTML (unless `allowHtml`), collapses whitespace, and repeatedly\n * removes dangerous URI schemes and inline handlers until the string\n * stabilizes — the fixed-point loop defeats nested-payload bypasses such as\n * `javascrjavascript:ipt:`. Failure is a resolved {@link Exit.Exit} failure,\n * never a throw; the only failure path is a non-string reaching `S.String`.\n *\n * @param input - User input to validate and sanitize.\n * @param options - Validation options. Defaults: `maxLength` 500, `allowHtml`\n * false, `allowNewlines` false, `trimWhitespace` true.\n * @returns An {@link Exit.Exit}: success carries the sanitized string truncated\n * to `maxLength`; failure carries a {@link S.SchemaError}.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const validateUserInputEffect = (\n\tinput: string,\n\toptions: UserInputOptions = {},\n): Exit.Exit<string, S.SchemaError> => {\n\tconst {\n\t\tmaxLength = 500,\n\t\tallowHtml = false,\n\t\tallowNewlines = false,\n\t\ttrimWhitespace = true,\n\t} = options;\n\n\tconst parsed = S.decodeUnknownExit(S.String)(input);\n\tif (Exit.isFailure(parsed)) return parsed;\n\n\tlet result = parsed.value;\n\n\tif (trimWhitespace) {\n\t\tresult = result.trim();\n\t}\n\n\tif (!allowHtml) {\n\t\tlet prev: string;\n\t\tdo {\n\t\t\tprev = result;\n\t\t\tresult = result.replaceAll(/<[^>]*>/g, \"\");\n\t\t} while (result !== prev);\n\t} else {\n\t\tresult = sanitizeHtml(result);\n\t}\n\n\tif (!allowNewlines) {\n\t\tresult = result.replaceAll(/[\\r\\n]+/g, \" \");\n\t}\n\n\tresult = result.replaceAll(/\\s+/g, \" \");\n\n\t// Loop until stable to prevent bypass via nested patterns (e.g. \"javascrjavascript:ipt:\")\n\tlet prevScheme: string;\n\tdo {\n\t\tprevScheme = result;\n\t\tresult = result\n\t\t\t.replaceAll(/javascript:/gi, \"\")\n\t\t\t.replaceAll(/data:/gi, \"\")\n\t\t\t.replaceAll(/vbscript:/gi, \"\")\n\t\t\t.replaceAll(/on\\w+=/gi, \"\");\n\t} while (result !== prevScheme);\n\n\treturn Exit.succeed(result.slice(0, maxLength));\n};\n\n/**\n * Validates and sanitizes generic user input by trimming, removing HTML tags (unless allowed),\n * normalizing whitespace, and removing dangerous patterns to prevent XSS and basic injection flaws.\n *\n * @param input - User input to validate and sanitize.\n * @param maxLength - Maximum allowed input length. Excess will be truncated.\n * @param allowHtml - If true, HTML tags are preserved; otherwise, all tags are stripped.\n * @returns Sanitized input string with length at most `maxLength`.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * validateUserInput('<p>Hello!</p>', 50); // \"Hello!\"\n * validateUserInput('<script>alert(1)</script>test', 100); // \"test\"\n * ```\n */\nexport const validateUserInput = (input: string, maxLength = 500, allowHtml = false): string => {\n\tif (!input || typeof input !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst result = validateUserInputEffect(input, { maxLength, allowHtml });\n\treturn Exit.isSuccess(result) ? result.value : \"\";\n};\n\n/** Own keys removed at every level, because assigning them reaches the prototype. */\nconst DANGEROUS_KEYS = [\"__proto__\", \"constructor\", \"prototype\"] as const;\n\n/**\n * Strip prototype-pollution keys (`__proto__`, `constructor`, `prototype`) from\n * every node of a parsed JSON value.\n *\n * **Mutates `val` in place** (deletes offending keys) and returns nothing;\n * callers pass a freshly `JSON.parse`d value they own. The walk is iterative and\n * unbounded in depth: every node is visited, however deeply nested.\n *\n * @internal\n */\nconst sanitizeObject = (val: unknown): void => {\n\t// Walked with an explicit stack rather than by recursion. The previous version\n\t// bounded itself at depth 50 to avoid overflowing the call stack and returned\n\t// early past that, so wrapping the payload in 51 layers carried `__proto__`\n\t// through untouched — and feeding the resulting leaf to an ordinary recursive\n\t// deep-merge set `Object.prototype.isAdmin`. The cap was never cycle protection:\n\t// both callers parse JSON first, and JSON cannot express a cycle. Dropping the\n\t// recursion removes the reason for the cap, so every node is now visited.\n\tconst stack: unknown[] = [val];\n\n\twhile (stack.length > 0) {\n\t\tconst current = stack.pop();\n\n\t\tif (typeof current !== \"object\" || current === null) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (Array.isArray(current)) {\n\t\t\tfor (const item of current) {\n\t\t\t\tstack.push(item);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst obj = current as Record<string, unknown>;\n\t\tfor (const key of DANGEROUS_KEYS) {\n\t\t\tif (key in obj) {\n\t\t\t\tdelete obj[key];\n\t\t\t}\n\t\t}\n\t\tfor (const key of Object.keys(obj)) {\n\t\t\tstack.push(obj[key]);\n\t\t}\n\t}\n};\n\n/**\n * Safely parses JSON with Effect Schema validation and prototype pollution protection.\n *\n * Never throws: malformed JSON, a non-string argument, and schema-validation\n * failure all resolve to {@link Option.none} rather than a thrown error, so the\n * failure channel is the `Option` itself. As a side effect the parsed value is\n * stripped of prototype-pollution keys in place before validation (the value is\n * freshly created by `JSON.parse`, so no caller state is mutated).\n *\n * @template A - The decoded value type the `schema` produces on success; the\n * returned `Option` carries this type.\n * @param jsonString - The JSON string to parse.\n * @param schema - Effect Schema to validate against; its decode must require no\n * services ({@link SyncSchema}) so parsing stays synchronous.\n * @returns {@link Option.some} with the parsed, validated value, or\n * {@link Option.none} on any parse or validation failure.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const UserSchema = S.Struct({ name: S.String, age: S.Number });\n * const result = parseJsonWithSchema('{\"name\":\"John\",\"age\":30}', UserSchema);\n * // Option.some({ name: 'John', age: 30 })\n * ```\n */\nexport const parseJsonWithSchema = <A>(\n\tjsonString: string,\n\tschema: SyncSchema<A>,\n): Option.Option<A> => {\n\tif (!jsonString || typeof jsonString !== \"string\") {\n\t\treturn Option.none();\n\t}\n\n\ttry {\n\t\tconst sanitized = jsonString\n\t\t\t.replaceAll(/\\)\\s*\\{/g, \") {}\")\n\t\t\t.replaceAll(/\\]\\s*\\{/g, \"] {}\")\n\t\t\t.replaceAll(/\\}\\s*\\{/g, \"} {}\");\n\n\t\tconst parsed = JSON.parse(sanitized);\n\n\t\tsanitizeObject(parsed);\n\n\t\tconst result = S.decodeUnknownExit(schema)(parsed);\n\t\treturn Exit.isSuccess(result) ? Option.some(result.value as A) : Option.none();\n\t} catch {\n\t\treturn Option.none();\n\t}\n};\n\n/**\n * Sanitizes and safely parses a JSON string, removing suspicious syntax elements that could\n * potentially result in JSON polyglot exploits or prototype pollution.\n *\n * The result is returned as `unknown` — this function performs **no** schema\n * validation, so it cannot honestly promise any concrete shape for\n * attacker-controlled input. Narrow the result yourself, or prefer\n * {@link parseJsonWithSchema}, which validates against an Effect Schema and\n * returns a typed `Option`.\n *\n * @param jsonString - The JSON string to sanitize and parse.\n * @returns The parsed value (as `unknown`) if valid, or `null` if invalid.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const obj = sanitizeJson('{\"foo\":\"bar\"}');\n * // obj: unknown — narrow before use, or use parseJsonWithSchema\n * ```\n */\nexport const sanitizeJson = (jsonString: string): unknown => {\n\tif (!jsonString || typeof jsonString !== \"string\") {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst sanitized = jsonString\n\t\t\t.replaceAll(/\\)\\s*\\{/g, \") {}\")\n\t\t\t.replaceAll(/\\]\\s*\\{/g, \"] {}\")\n\t\t\t.replaceAll(/\\}\\s*\\{/g, \"} {}\");\n\n\t\tconst parsed: unknown = JSON.parse(sanitized);\n\n\t\tsanitizeObject(parsed);\n\n\t\treturn parsed;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/**\n * Strips ANSI escape sequences from a string.\n *\n * Removes CSI sequences (colour, cursor movement, screen and line erasure, mode\n * switches), OSC sequences (window title and similar), two-character escapes, and\n * any bare ESC left over. This is the control named by `LOG-ANSI-ESCAPE-001`: a\n * terminal-backed log sink treats these as commands, so an attacker who lands them\n * in a log can scroll earlier entries away or overwrite them (CWE-117).\n *\n * Removal is destructive by design — the sequence goes, and any text it carried\n * goes with it. Where the record matters more than the rendering, escape the\n * characters instead of deleting them.\n *\n * @param text - The text potentially containing ANSI codes.\n * @returns The text with ANSI codes removed.\n *\n * @example\n * ```typescript\n * stripAnsi('\\x1b[31mRed text\\x1b[0m'); // 'Red text'\n * ```\n */\nexport const stripAnsi = (text: string): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\t// Every escape sequence, not just the colour ones. The previous pattern was\n\t// `/\\\\x1b\\\\[[0-9;]*m/` — SGR only — so `ESC[2J` (erase display), `ESC[?1049h`\n\t// (alternate screen buffer), `ESC[5A` (cursor up, which overwrites the audit\n\t// lines already written) and `ESC]0;...` (set window title) all survived the\n\t// function that `LOG-ANSI-ESCAPE-001` names as its control. Colour was the one\n\t// case handled, and the only one that is merely cosmetic.\n\t//\n\t// Alternatives are ordered CSI, OSC, then the general ECMA-48 form (ESC,\n\t// intermediates 0x20-0x2F, final 0x30-0x7E), which covers Fs escapes such as\n\t// ESC c. Every quantifier is bounded and none is nested, so nothing backtracks.\n\t// A trailing `?` also removes a bare ESC that begins no valid sequence.\n\treturn text.replaceAll(\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI requires matching control characters\n\t\t/\\u001b(?:\\[[0-?]{0,32}[ -/]{0,8}[@-~]|\\][^\\u0007\\u001b]{0,512}(?:\\u0007|\\u001b\\\\)|[ -/]{0,8}[0-~])?/g,\n\t\t\"\",\n\t);\n};\n\n//#endregion\n\n//#region PII Redaction\n\n/**\n * PII pattern catalog: each entry pairs a global-match regex with the marker that\n * replaces every hit during redaction.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nconst PII_PATTERNS = {\n\tssn: { pattern: /\\b\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{4}\\b/g, marker: \"[SSN]\" },\n\tcreditCard: { pattern: /\\b(?:\\d{4}[-\\s]?){3}\\d{4}\\b/g, marker: \"[CREDIT_CARD]\" },\n\tcreditCardAlt: { pattern: /\\b\\d{15,16}\\b/g, marker: \"[CREDIT_CARD]\" },\n\t// TLD alternation mirrors `EmailSchema` so IDN/Punycode addresses\n\t// (e.g. `user@example.xn--p1ai`) are redacted, not leaked. The `xn--` branch\n\t// is tried first: unlike the anchored (`$`) validators, this pattern ends in\n\t// `\\b`, so `[A-Za-z]{2,}` would otherwise match just `xn` and stop at the\n\t// hyphen, leaving `--p1ai` unredacted. (Also drops the stray `|` from the\n\t// former `[A-Z|a-z]` class.)\n\temail: {\n\t\tpattern: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.(?:xn--[A-Za-z0-9-]+|[A-Za-z]{2,})\\b/g,\n\t\tmarker: \"[EMAIL]\",\n\t},\n\tphone: { pattern: /\\b(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}\\b/g, marker: \"[PHONE]\" },\n\tipv4: { pattern: /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, marker: \"[IP_ADDRESS]\" },\n\tipv6: { pattern: /\\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\b/g, marker: \"[IP_ADDRESS]\" },\n\tdate: {\n\t\tpattern: /\\b(?:\\d{1,2}[/-]\\d{1,2}[/-]\\d{2,4}|\\d{4}[/-]\\d{1,2}[/-]\\d{1,2})\\b/g,\n\t\tmarker: \"[DATE]\",\n\t},\n} as const;\n\n/**\n * Redacts PII from text using Effect Schema validated options.\n *\n * Total — never throws. Failure to decode `options` is returned as a resolved\n * {@link Exit.Exit} failure carrying the {@link S.SchemaError}. By default\n * dates are **not** redacted (`redactDates` defaults to `false`); every other\n * category defaults to `true`.\n *\n * @param text - The text to redact PII from.\n * @param options - Configuration options for redaction.\n * @returns An {@link Exit.Exit}: success carries the redacted text, failure\n * carries a {@link S.SchemaError} from invalid `options`.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nexport const redactPIIEffect = (\n\ttext: string,\n\toptions: PIIRedactionOptions = {},\n): Exit.Exit<string, S.SchemaError> => {\n\tconst parsed = S.decodeUnknownExit(PIIRedactionOptionsSchema)(options);\n\tif (Exit.isFailure(parsed)) return Exit.failCause(parsed.cause);\n\n\tconst {\n\t\tredactEmails = true,\n\t\tredactPhones = true,\n\t\tredactSSN = true,\n\t\tredactCreditCards = true,\n\t\tredactIPs = true,\n\t\tredactDates = false,\n\t} = parsed.value;\n\n\tlet result = text;\n\n\tif (redactSSN) {\n\t\tresult = result.replaceAll(PII_PATTERNS.ssn.pattern, PII_PATTERNS.ssn.marker);\n\t}\n\n\tif (redactCreditCards) {\n\t\tresult = result.replaceAll(PII_PATTERNS.creditCard.pattern, PII_PATTERNS.creditCard.marker);\n\t\tresult = result.replaceAll(\n\t\t\tPII_PATTERNS.creditCardAlt.pattern,\n\t\t\tPII_PATTERNS.creditCardAlt.marker,\n\t\t);\n\t}\n\n\tif (redactEmails) {\n\t\tresult = result.replaceAll(PII_PATTERNS.email.pattern, PII_PATTERNS.email.marker);\n\t}\n\n\tif (redactPhones) {\n\t\tresult = result.replaceAll(PII_PATTERNS.phone.pattern, PII_PATTERNS.phone.marker);\n\t}\n\n\tif (redactIPs) {\n\t\tresult = result.replaceAll(PII_PATTERNS.ipv4.pattern, PII_PATTERNS.ipv4.marker);\n\t\tresult = result.replaceAll(PII_PATTERNS.ipv6.pattern, PII_PATTERNS.ipv6.marker);\n\t}\n\n\tif (redactDates) {\n\t\tresult = result.replaceAll(PII_PATTERNS.date.pattern, PII_PATTERNS.date.marker);\n\t}\n\n\treturn Exit.succeed(result);\n};\n\n/**\n * Redacts common PII patterns in a string for safe logging.\n * Detects and masks SSNs, credit cards, emails, phone numbers, etc.\n *\n * @param text - The text to redact PII from. Non-string input yields `\"\"`.\n * @param options - Configuration options for redaction, plus optional\n * `customPatterns` applied after the built-ins. Each `pattern` **must** be a\n * global (`/g`) RegExp.\n * @returns The text with PII patterns replaced with redaction markers, or `\"\"`\n * for non-string input. If the built-in options fail schema validation the\n * original `text` is returned unredacted rather than throwing.\n * @throws {TypeError} If any `customPatterns` entry uses a non-global RegExp —\n * `String.prototype.replaceAll` rejects non-global patterns.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n *\n * @example\n * ```typescript\n * redactPII('Contact john@example.com or call 555-123-4567');\n * // 'Contact [EMAIL] or call [PHONE]'\n *\n * redactPII('SSN: 123-45-6789');\n * // 'SSN: [SSN]'\n * ```\n */\nexport const redactPII = (\n\ttext: string,\n\toptions: PIIRedactionOptions & {\n\t\tcustomPatterns?: Array<{ pattern: RegExp; replacement: string }>;\n\t} = {},\n): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst {\n\t\tredactEmails = true,\n\t\tredactPhones = true,\n\t\tredactSSN = true,\n\t\tredactCreditCards = true,\n\t\tredactIPs = true,\n\t\tredactDates = false,\n\t\tcustomPatterns = [],\n\t} = options;\n\n\tconst result = redactPIIEffect(text, {\n\t\tredactEmails,\n\t\tredactPhones,\n\t\tredactSSN,\n\t\tredactCreditCards,\n\t\tredactIPs,\n\t\tredactDates,\n\t});\n\n\tlet output = Exit.isSuccess(result) ? result.value : text;\n\n\tfor (const { pattern, replacement } of customPatterns) {\n\t\toutput = output.replaceAll(pattern, replacement);\n\t}\n\n\treturn output;\n};\n\n/**\n * Creates a safe string representation of an object for logging,\n * automatically redacting sensitive fields.\n *\n * Never throws: any serialization failure — a circular reference, a `BigInt`\n * value, a throwing `toJSON` — is caught and returned as the sentinel string\n * `\"[Unable to stringify object]\"`. Key matching is case-insensitive and\n * compares the full key name (not substrings), so `apiKey` matches only the\n * literal `\"apiKey\"`, not `\"apiKeyId\"`.\n *\n * @param obj - The object to stringify.\n * @param sensitiveKeys - Key names to redact, compared case-insensitively.\n * @param indent - JSON indentation (default: 2).\n * @returns A JSON string with sensitive values redacted, or the sentinel\n * `\"[Unable to stringify object]\"` when serialization fails.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n *\n * @example\n * ```typescript\n * safeStringify({ user: 'john', password: 'secret123' }, ['password']);\n * // '{\\n \"user\": \"john\",\\n \"password\": \"[REDACTED]\"\\n}'\n * ```\n */\nexport const safeStringify = (\n\tobj: unknown,\n\tsensitiveKeys: string[] = [\n\t\t\"password\",\n\t\t\"token\",\n\t\t\"apiKey\",\n\t\t\"secret\",\n\t\t\"authorization\",\n\t\t\"cookie\",\n\t\t\"ssn\",\n\t\t\"creditCard\",\n\t],\n\tindent = 2,\n): string => {\n\tconst sensitiveKeysLower = new Set(sensitiveKeys.map((k) => k.toLowerCase()));\n\n\tconst replacer = (_key: string, value: unknown): unknown => {\n\t\tif (_key && sensitiveKeysLower.has(_key.toLowerCase())) {\n\t\t\treturn \"[REDACTED]\";\n\t\t}\n\t\treturn value;\n\t};\n\n\ttry {\n\t\treturn JSON.stringify(obj, replacer, indent);\n\t} catch {\n\t\treturn \"[Unable to stringify object]\";\n\t}\n};\n\n//#endregion\n\n//#region Validation Helpers\n\n/**\n * Validates if a string is a valid email address using Effect Schema.\n *\n * Narrows the input to {@link Email} on success, so validated call sites\n * carry the brand into downstream code.\n *\n * @param email - The string to validate.\n * @returns true if valid email, false otherwise.\n */\nexport const isValidEmail = (email: string): email is Email => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(EmailSchema)(email));\n};\n\n/**\n * Validates if a string is a valid phone number using Effect Schema.\n *\n * Narrows the input to {@link PhoneNumber} on success.\n *\n * @param phone - The string to validate.\n * @returns true if valid phone number, false otherwise.\n */\nexport const isValidPhone = (phone: string): phone is PhoneNumber => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(PhoneNumberSchema)(phone));\n};\n\n/**\n * Validates if a string is a valid SSN using Effect Schema.\n *\n * Narrows the input to {@link SSN} on success.\n *\n * @param ssn - The string to validate.\n * @returns true if valid SSN, false otherwise.\n */\nexport const isValidSSN = (ssn: string): ssn is SSN => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(SSNSchema)(ssn));\n};\n\n/**\n * Validates if a string is a safe URL using Effect Schema.\n *\n * Narrows the input to {@link SafeUrl} on success.\n *\n * @param url - The string to validate.\n * @returns true if valid and safe URL, false otherwise.\n */\nexport const isValidUrl = (url: string): url is SafeUrl => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(SafeUrlSchema)(url));\n};\n//#endregion\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sanitize.mjs","names":["S"],"sources":["../src/sanitize.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n * SPDX-License-Identifier: Apache-2.0\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Type-safe input sanitization built on Effect Schema — HTML escaping\n * and DOMPurify-backed HTML sanitization, safe-URL validation, prototype-pollution-\n * hardened JSON parsing, PII redaction, and branded validators for email, phone, SSN,\n * and more. Supports NIST 800-53 SI-10 (input validation) and AU-3 (audit content).\n *\n * @module @resq-systems/security/sanitize\n */\n\nimport type { Brand } from \"@resq-systems/types\";\nimport { Exit, Option, Schema as S } from \"effect\";\nimport DOMPurify from \"dompurify\";\nimport type { Config, WindowLike } from \"dompurify\";\n\n//#region Types\n\n/**\n * A Schema whose decoding services are constrained to `never`, allowing synchronous\n * decoding without an Effect runtime.\n */\ntype SyncSchema<T> = S.Codec<T, unknown, never>;\n//#endregion\n\n//#region Schemas\n\n/**\n * Schema constraining a URL protocol to the recognized safe set.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const UrlProtocolSchema = S.Literals([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"ftp:\"]);\n/** One of the protocols accepted by {@link UrlProtocolSchema}. */\nexport type UrlProtocol = typeof UrlProtocolSchema.Type;\n\n/**\n * Schema for the per-category toggles that drive {@link redactPII}.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nexport const PIIRedactionOptionsSchema = S.Struct({\n\tredactEmails: S.optional(S.Boolean),\n\tredactPhones: S.optional(S.Boolean),\n\tredactSSN: S.optional(S.Boolean),\n\tredactCreditCards: S.optional(S.Boolean),\n\tredactIPs: S.optional(S.Boolean),\n\tredactDates: S.optional(S.Boolean),\n});\n/** Decoded options accepted by {@link redactPIIEffect} / {@link redactPII}. */\nexport type PIIRedactionOptions = typeof PIIRedactionOptionsSchema.Type;\n\n/**\n * Schema for the options controlling {@link validateUserInputEffect}.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const UserInputOptionsSchema = S.Struct({\n\tmaxLength: S.optional(S.Int.check(S.isGreaterThan(0))),\n\tallowHtml: S.optional(S.Boolean),\n\tallowNewlines: S.optional(S.Boolean),\n\ttrimWhitespace: S.optional(S.Boolean),\n});\n/** Decoded options accepted by {@link validateUserInputEffect}. */\nexport type UserInputOptions = typeof UserInputOptionsSchema.Type;\n\n/**\n * Schema for a safe URL — validates URL format and restricts to safe protocols.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\n/**\n * Whether a reference opens a URL *authority* component — i.e. can name a different\n * host once resolved against a base.\n *\n * `//evil.example` resolved against `https://trusted.example/page` yields host\n * `evil.example`, and so do `///evil.example` and `/\\evil.example`: the WHATWG URL\n * parser treats a backslash as a slash in the relative-slash state.\n *\n * This must be checked **before** the root-relative fast path. A `startsWith(\"//\")`\n * guard alone is insufficient twice over: it misses the backslash forms entirely, and\n * `//evil.example` that falls through to `new URL()` throws without a base and lands in\n * the catch branch, whose `[a-zA-Z0-9/_.-]` character class happily accepts it.\n *\n * @internal\n */\nconst opensAuthority = (url: string): boolean => /^[/\\\\]{2}/.test(url.trim());\n\nexport const SafeUrlSchema = S.String.check(\n\tS.makeFilter(\n\t\t(url: string) => {\n\t\t\tif (!url || url.trim() === \"\") return false;\n\t\t\tif (opensAuthority(url)) return false;\n\t\t\tif (url.startsWith(\"/\")) return true;\n\t\t\ttry {\n\t\t\t\tconst parsed = new URL(url);\n\t\t\t\tconst safeProtocols = [\"http:\", \"https:\", \"mailto:\"];\n\t\t\t\treturn safeProtocols.includes(parsed.protocol);\n\t\t\t} catch {\n\t\t\t\treturn /^[a-zA-Z0-9/_.-]+$/.test(url);\n\t\t\t}\n\t\t},\n\t\t{ message: \"Invalid or unsafe URL\" },\n\t),\n);\n/**\n * A URL string vouched safe against scheme-based injection. Mint one by\n * narrowing through the {@link isValidUrl} type guard (backed by\n * {@link SafeUrlSchema}); the brand guarantees the value is either a\n * root-relative path or an absolute URL restricted to `http:`/`https:`/\n * `mailto:`. An authority-opening reference (`//host`, `///host`, `/\\host`) is\n * rejected, since resolving one against a base yields a different host.\n *\n * It does **not** guarantee the host is reachable or trusted — for that, validate the\n * resolved origin with `isAllowedOrigin` from `@resq-systems/security/controls`.\n */\nexport type SafeUrl = Brand<string, \"SafeUrl\">;\n\n/**\n * Schema for a sanitized HTML-safe string — validates the value is a string; the\n * actual escaping is applied at runtime by the sanitization helpers.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SanitizedStringSchema = S.String;\n/**\n * A string carrying the {@link SanitizedStringSchema} contract. The schema is\n * `S.String` alone, so decoding asserts only that the value is a string —\n * the actual escaping is applied separately by the sanitization helpers\n * (e.g. {@link escapeHtml}). The type name signals intent, not a proof of\n * escaping.\n */\nexport type SanitizedString = typeof SanitizedStringSchema.Type;\n\n/**\n * Schema for email address validation.\n *\n * Accepts a 2+ character alphabetic TLD or a Punycode/IDN `xn--…` TLD (e.g.\n * `.xn--p1ai` for `.рф`) so internationalized domains are not rejected. Kept in\n * sync with `@resq-systems/email-templates`'s `EmailAddress` brand.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const EmailSchema = S.String.check(\n\tS.isPattern(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.(?:[A-Za-z]{2,}|xn--[A-Za-z0-9-]+)$/),\n);\n/**\n * An email address that matches {@link EmailSchema}. Mint one by narrowing\n * through the {@link isValidEmail} type guard. The brand guarantees only\n * syntactic well-formedness (including IDN/Punycode TLDs) — not that the\n * mailbox exists or is deliverable.\n */\nexport type Email = Brand<string, \"Email\">;\n\n/**\n * Schema for phone number validation (US format).\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const PhoneNumberSchema = S.String.check(\n\tS.isPattern(/^(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}$/),\n);\n/**\n * A US-format phone number matching {@link PhoneNumberSchema}. Mint one by\n * narrowing through the {@link isValidPhone} type guard. The brand asserts\n * the digit/separator shape only; it neither normalizes formatting nor\n * confirms the number is assigned.\n */\nexport type PhoneNumber = Brand<string, \"PhoneNumber\">;\n\n/**\n * Schema for SSN validation (US format).\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const SSNSchema = S.String.check(S.isPattern(/^\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{4}$/));\n/**\n * A US Social Security Number matching {@link SSNSchema}. Mint one by\n * narrowing through the {@link isValidSSN} type guard. The brand asserts\n * the `NNN-NN-NNNN` shape only — it does not validate area/group ranges or\n * confirm the number was ever issued. Treat any value as sensitive PII.\n */\nexport type SSN = Brand<string, \"SSN\">;\n\n/**\n * Schema for credit card number validation.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const CreditCardSchema = S.String.check(\n\tS.isPattern(/^(?:\\d{4}[-\\s]?){3}\\d{4}$|^\\d{15,16}$/),\n);\n/**\n * A card number matching the {@link CreditCardSchema} pattern (13–16 digits\n * with optional group separators). No exported type guard mints this brand;\n * decode {@link CreditCardSchema} directly at the boundary. The pattern is a\n * shape check only — it performs **no** Luhn checksum and does not identify\n * the issuer. Treat any value as sensitive PII.\n */\nexport type CreditCard = Brand<string, \"CreditCard\">;\n\n/**\n * Schema for IPv4 address validation.\n */\nexport const IPv4Schema = S.String.check(S.isPattern(/^(?:\\d{1,3}\\.){3}\\d{1,3}$/));\n/**\n * A dotted-quad string matching {@link IPv4Schema}. No exported type guard\n * mints this brand; decode {@link IPv4Schema} directly. The pattern checks\n * four dot-separated groups of 1–3 digits only — it does **not** bound each\n * octet to `0–255`, so `999.0.0.1` still matches.\n */\nexport type IPv4 = Brand<string, \"IPv4\">;\n//#endregion\n\n//#region Sanitization\n\n/**\n * Escapes special HTML characters in a string to their corresponding HTML entities,\n * preventing direct injection of HTML and JavaScript when rendering untrusted content.\n *\n * @param text - The plain text to escape.\n * @returns The escaped string safe for HTML rendering.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * escapeHtml('<script>alert(\"xss\")</script>');\n * // \"<script>alert("xss")</script>\"\n * ```\n */\nexport const escapeHtml = (text: string): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\treturn text\n\t\t.replaceAll(\"&\", \"&\")\n\t\t.replaceAll(\"<\", \"<\")\n\t\t.replaceAll(\">\", \">\")\n\t\t.replaceAll('\"', \""\")\n\t\t.replaceAll(\"'\", \"'\");\n};\n\n/**\n * Validates and sanitizes a user-supplied URL using Effect Schema.\n * Returns an Exit with the sanitized URL or an error.\n *\n * Pure and total — failure is encoded as a resolved {@link Exit.Exit} failure\n * (an `Exit.fail` carrying a {@link S.SchemaError}), never a thrown exception.\n *\n * @param url - The URL to be validated and sanitized.\n * @param allowedProtocols - Allowed URL protocols; a root-relative path (`/foo`) is\n * always accepted regardless of this list. An authority-opening reference — `//host`,\n * `///host`, or `/\\host` — is always **rejected**, because it names a different host\n * once resolved and would otherwise bypass this list entirely.\n * @returns An {@link Exit.Exit}: success carries the accepted URL string,\n * failure carries a {@link S.SchemaError}.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const result = sanitizeUrlEffect('https://example.com');\n * // Exit.succeed('https://example.com')\n *\n * const invalid = sanitizeUrlEffect('javascript:alert(1)');\n * // Exit.fail(...)\n * ```\n */\nexport const sanitizeUrlEffect = (\n\turl: string,\n\tallowedProtocols: readonly UrlProtocol[] = [\"http:\", \"https:\", \"mailto:\"],\n): Exit.Exit<string, S.SchemaError> => {\n\tconst CustomSafeUrlSchema = S.String.check(\n\t\tS.makeFilter(\n\t\t\t(u: string) => {\n\t\t\t\tif (!u || u.trim() === \"\") return false;\n\t\t\t\tconst trimmed = u.trim();\n\t\t\t\t// Before the root-relative fast path: an authority-opening reference can\n\t\t\t\t// name a different host and bypasses `allowedProtocols` entirely.\n\t\t\t\tif (opensAuthority(trimmed)) return false;\n\t\t\t\tif (trimmed.startsWith(\"/\")) return true;\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = new URL(trimmed);\n\t\t\t\t\tif (!allowedProtocols.includes(parsed.protocol)) return false;\n\t\t\t\t\tif (parsed.hostname.includes(\"javascript:\") || parsed.hostname.includes(\"data:\")) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t} catch {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t/^[a-zA-Z0-9/_.-]+$/.test(trimmed) &&\n\t\t\t\t\t\t!trimmed.includes(\"javascript:\") &&\n\t\t\t\t\t\t!trimmed.includes(\"data:\")\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ message: \"Invalid or unsafe URL\" },\n\t\t),\n\t);\n\n\treturn S.decodeUnknownExit(CustomSafeUrlSchema)(url);\n};\n\n/**\n * Validates and sanitizes a user-supplied URL, ensuring it conforms to allowed protocols\n * and is not a vector for injection attacks like `javascript:` or `data:`.\n *\n * @param url - The URL to be validated and sanitized.\n * @param allowedProtocols - Array of allowed URL protocols.\n * @returns The sanitized URL if valid, or an empty string if unsafe.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * sanitizeUrl('https://example.com'); // 'https://example.com'\n * sanitizeUrl('javascript:alert(1)'); // ''\n * ```\n */\nexport const sanitizeUrl = (\n\turl: string,\n\tallowedProtocols: readonly UrlProtocol[] = [\"http:\", \"https:\", \"mailto:\"],\n): string => {\n\tconst result = sanitizeUrlEffect(url, allowedProtocols);\n\treturn Exit.isSuccess(result) ? result.value : \"\";\n};\n\nlet purifyInstance: typeof DOMPurify | null | undefined;\n\n/**\n * Add `rel=\"noopener noreferrer\"` to any link that opens a new browsing context.\n *\n * Without it the opened page receives a live `window.opener` handle and can navigate\n * the original tab to a phishing page — reverse tabnabbing, WSTG-CLNT-14. DOMPurify\n * does not add this by default, and unlike most of CLNT-14 the fix lives inside code\n * this package already owns, so a weakness no signature can detect becomes one that is\n * simply prevented.\n *\n * Modern browsers imply `noopener` for `target=\"_blank\"`; this covers older engines and\n * the named-target case (`target=\"win1\"`), which remains exploitable everywhere.\n *\n * Note that DOMPurify's default configuration strips `target` outright, so this hook is\n * a no-op unless the caller opts back in with `ADD_ATTR: [\"target\"]` or a custom\n * `ALLOWED_ATTR` — which is precisely the configuration that reintroduces the risk.\n *\n * @internal\n */\nconst addNoopenerToTargetedLinks = (node: Element): void => {\n\tif (typeof node.hasAttribute !== \"function\" || !node.hasAttribute(\"target\")) return;\n\n\tconst target = node.getAttribute(\"target\");\n\t// `_self`, `_parent`, and `_top` stay in the current context and grant no handle.\n\tif (target === null || target === \"_self\" || target === \"_parent\" || target === \"_top\") return;\n\n\tconst existing = (node.getAttribute(\"rel\") ?? \"\").split(/\\s+/).filter(Boolean);\n\tfor (const required of [\"noopener\", \"noreferrer\"]) {\n\t\tif (!existing.includes(required)) existing.push(required);\n\t}\n\tnode.setAttribute(\"rel\", existing.join(\" \"));\n};\n\n/** Register the reverse-tabnabbing hook on a DOMPurify instance. */\nconst withHooks = (purify: typeof DOMPurify): typeof DOMPurify => {\n\tpurify.addHook(\"afterSanitizeAttributes\", (node) => {\n\t\taddNoopenerToTargetedLinks(node as unknown as Element);\n\t});\n\treturn purify;\n};\n\nconst getPurify = (): typeof DOMPurify | null => {\n\tif (purifyInstance !== undefined) return purifyInstance;\n\n\tif (typeof window !== \"undefined\") {\n\t\tpurifyInstance = withHooks(DOMPurify);\n\t} else {\n\t\ttry {\n\t\t\t// Resolve `node:module` at runtime (server-side only) via\n\t\t\t// process.getBuiltinModule so browser bundlers never see a static\n\t\t\t// `node:module` import. Available on Node >=20.16 and Bun; absent in\n\t\t\t// browsers, where the `window` branch above is taken instead. jsdom is an\n\t\t\t// optional peer dependency — install it for server-side HTML sanitization.\n\t\t\tconst proc = (globalThis as { process?: { getBuiltinModule?: (m: string) => unknown } })\n\t\t\t\t.process;\n\t\t\tconst nodeModule = proc?.getBuiltinModule?.(\"module\") as\n\t\t\t\t| { createRequire(path: string | URL): (id: string) => unknown }\n\t\t\t\t| undefined;\n\t\t\tif (!nodeModule) {\n\t\t\t\tpurifyInstance = null;\n\t\t\t\treturn purifyInstance;\n\t\t\t}\n\t\t\tconst req = nodeModule.createRequire(import.meta.url);\n\t\t\tconst { JSDOM } = req(\"jsdom\") as {\n\t\t\t\tJSDOM: new (\n\t\t\t\t\thtml?: string,\n\t\t\t\t) => {\n\t\t\t\t\twindow: WindowLike;\n\t\t\t\t};\n\t\t\t};\n\t\t\tconst dom = new JSDOM(\"\");\n\t\t\tpurifyInstance = withHooks(DOMPurify(dom.window));\n\t\t} catch {\n\t\t\tpurifyInstance = null;\n\t\t}\n\t}\n\treturn purifyInstance;\n};\n\n/**\n * Sanitizes HTML to prevent XSS attacks.\n * Uses DOMPurify under the hood. If DOM is not available (e.g. server-side without JSDOM),\n * it falls back to escaping all HTML characters for safety.\n *\n * NOTE: Server-side HTML sanitization requires `jsdom` to be installed in the consuming application\n * environment; otherwise, it will fall back to escaping HTML characters.\n *\n * On the first server-side call this lazily resolves `node:module` and\n * `require`s `jsdom` to build a DOMPurify instance; the instance is cached at\n * module scope, so subsequent calls incur no further module loading. Returns\n * `\"\"` for non-string or empty input and never throws — any loader failure is\n * swallowed and downgraded to {@link escapeHtml}.\n *\n * @param html - The HTML string to sanitize.\n * @param options - Optional DOMPurify configuration.\n * @returns The sanitized HTML string, or the escaped string when no DOM is\n * available.\n */\nexport const sanitizeHtml = (html: string, options?: Config): string => {\n\tif (!html || typeof html !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst purify = getPurify();\n\tif (purify) {\n\t\treturn purify.sanitize(html, options) as string;\n\t}\n\n\treturn escapeHtml(html);\n};\n\n/**\n * Validates user input using Effect Schema and returns an Exit.\n *\n * Strips HTML (unless `allowHtml`), collapses whitespace, and repeatedly\n * removes dangerous URI schemes and inline handlers until the string\n * stabilizes — the fixed-point loop defeats nested-payload bypasses such as\n * `javascrjavascript:ipt:`. Failure is a resolved {@link Exit.Exit} failure,\n * never a throw; the only failure path is a non-string reaching `S.String`.\n *\n * @param input - User input to validate and sanitize.\n * @param options - Validation options. Defaults: `maxLength` 500, `allowHtml`\n * false, `allowNewlines` false, `trimWhitespace` true.\n * @returns An {@link Exit.Exit}: success carries the sanitized string truncated\n * to `maxLength`; failure carries a {@link S.SchemaError}.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n */\nexport const validateUserInputEffect = (\n\tinput: string,\n\toptions: UserInputOptions = {},\n): Exit.Exit<string, S.SchemaError> => {\n\tconst {\n\t\tmaxLength = 500,\n\t\tallowHtml = false,\n\t\tallowNewlines = false,\n\t\ttrimWhitespace = true,\n\t} = options;\n\n\tconst parsed = S.decodeUnknownExit(S.String)(input);\n\tif (Exit.isFailure(parsed)) return parsed;\n\n\tlet result = parsed.value;\n\n\tif (trimWhitespace) {\n\t\tresult = result.trim();\n\t}\n\n\tif (!allowHtml) {\n\t\tlet prev: string;\n\t\tdo {\n\t\t\tprev = result;\n\t\t\tresult = result.replaceAll(/<[^>]*>/g, \"\");\n\t\t} while (result !== prev);\n\t} else {\n\t\tresult = sanitizeHtml(result);\n\t}\n\n\tif (!allowNewlines) {\n\t\tresult = result.replaceAll(/[\\r\\n]+/g, \" \");\n\t}\n\n\tresult = result.replaceAll(/\\s+/g, \" \");\n\n\t// Loop until stable to prevent bypass via nested patterns (e.g. \"javascrjavascript:ipt:\")\n\tlet prevScheme: string;\n\tdo {\n\t\tprevScheme = result;\n\t\tresult = result\n\t\t\t.replaceAll(/javascript:/gi, \"\")\n\t\t\t.replaceAll(/data:/gi, \"\")\n\t\t\t.replaceAll(/vbscript:/gi, \"\")\n\t\t\t.replaceAll(/on\\w+=/gi, \"\");\n\t} while (result !== prevScheme);\n\n\treturn Exit.succeed(result.slice(0, maxLength));\n};\n\n/**\n * Validates and sanitizes generic user input by trimming, removing HTML tags (unless allowed),\n * normalizing whitespace, and removing dangerous patterns to prevent XSS and basic injection flaws.\n *\n * @param input - User input to validate and sanitize.\n * @param maxLength - Maximum allowed input length. Excess will be truncated.\n * @param allowHtml - If true, HTML tags are preserved; otherwise, all tags are stripped.\n * @returns Sanitized input string with length at most `maxLength`.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * validateUserInput('<p>Hello!</p>', 50); // \"Hello!\"\n * validateUserInput('<script>alert(1)</script>test', 100); // \"test\"\n * ```\n */\nexport const validateUserInput = (input: string, maxLength = 500, allowHtml = false): string => {\n\tif (!input || typeof input !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst result = validateUserInputEffect(input, { maxLength, allowHtml });\n\treturn Exit.isSuccess(result) ? result.value : \"\";\n};\n\n/** Own keys removed at every level, because assigning them reaches the prototype. */\nconst DANGEROUS_KEYS = [\"__proto__\", \"constructor\", \"prototype\"] as const;\n\n/**\n * Strip prototype-pollution keys (`__proto__`, `constructor`, `prototype`) from\n * every node of a parsed JSON value.\n *\n * **Mutates `val` in place** (deletes offending keys) and returns nothing;\n * callers pass a freshly `JSON.parse`d value they own. The walk is iterative and\n * unbounded in depth: every node is visited, however deeply nested.\n *\n * @internal\n */\nconst sanitizeObject = (val: unknown): void => {\n\t// Walked with an explicit stack rather than by recursion. The previous version\n\t// bounded itself at depth 50 to avoid overflowing the call stack and returned\n\t// early past that, so wrapping the payload in 51 layers carried `__proto__`\n\t// through untouched — and feeding the resulting leaf to an ordinary recursive\n\t// deep-merge set `Object.prototype.isAdmin`. The cap was never cycle protection:\n\t// both callers parse JSON first, and JSON cannot express a cycle. Dropping the\n\t// recursion removes the reason for the cap, so every node is now visited.\n\tconst stack: unknown[] = [val];\n\n\twhile (stack.length > 0) {\n\t\tconst current = stack.pop();\n\n\t\tif (typeof current !== \"object\" || current === null) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (Array.isArray(current)) {\n\t\t\tfor (const item of current) {\n\t\t\t\tstack.push(item);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst obj = current as Record<string, unknown>;\n\t\tfor (const key of DANGEROUS_KEYS) {\n\t\t\tif (key in obj) {\n\t\t\t\tdelete obj[key];\n\t\t\t}\n\t\t}\n\t\tfor (const key of Object.keys(obj)) {\n\t\t\tstack.push(obj[key]);\n\t\t}\n\t}\n};\n\n/**\n * Safely parses JSON with Effect Schema validation and prototype pollution protection.\n *\n * Never throws: malformed JSON, a non-string argument, and schema-validation\n * failure all resolve to {@link Option.none} rather than a thrown error, so the\n * failure channel is the `Option` itself. As a side effect the parsed value is\n * stripped of prototype-pollution keys in place before validation (the value is\n * freshly created by `JSON.parse`, so no caller state is mutated).\n *\n * @template A - The decoded value type the `schema` produces on success; the\n * returned `Option` carries this type.\n * @param jsonString - The JSON string to parse.\n * @param schema - Effect Schema to validate against; its decode must require no\n * services ({@link SyncSchema}) so parsing stays synchronous.\n * @returns {@link Option.some} with the parsed, validated value, or\n * {@link Option.none} on any parse or validation failure.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const UserSchema = S.Struct({ name: S.String, age: S.Number });\n * const result = parseJsonWithSchema('{\"name\":\"John\",\"age\":30}', UserSchema);\n * // Option.some({ name: 'John', age: 30 })\n * ```\n */\nexport const parseJsonWithSchema = <A>(\n\tjsonString: string,\n\tschema: SyncSchema<A>,\n): Option.Option<A> => {\n\tif (!jsonString || typeof jsonString !== \"string\") {\n\t\treturn Option.none();\n\t}\n\n\ttry {\n\t\tconst sanitized = jsonString\n\t\t\t.replaceAll(/\\)\\s*\\{/g, \") {}\")\n\t\t\t.replaceAll(/\\]\\s*\\{/g, \"] {}\")\n\t\t\t.replaceAll(/\\}\\s*\\{/g, \"} {}\");\n\n\t\tconst parsed = JSON.parse(sanitized);\n\n\t\tsanitizeObject(parsed);\n\n\t\tconst result = S.decodeUnknownExit(schema)(parsed);\n\t\treturn Exit.isSuccess(result) ? Option.some(result.value as A) : Option.none();\n\t} catch {\n\t\treturn Option.none();\n\t}\n};\n\n/**\n * Sanitizes and safely parses a JSON string, removing suspicious syntax elements that could\n * potentially result in JSON polyglot exploits or prototype pollution.\n *\n * The result is returned as `unknown` — this function performs **no** schema\n * validation, so it cannot honestly promise any concrete shape for\n * attacker-controlled input. Narrow the result yourself, or prefer\n * {@link parseJsonWithSchema}, which validates against an Effect Schema and\n * returns a typed `Option`.\n *\n * @param jsonString - The JSON string to sanitize and parse.\n * @returns The parsed value (as `unknown`) if valid, or `null` if invalid.\n * @compliance NIST 800-53 SI-10 (Information Input Validation)\n *\n * @example\n * ```typescript\n * const obj = sanitizeJson('{\"foo\":\"bar\"}');\n * // obj: unknown — narrow before use, or use parseJsonWithSchema\n * ```\n */\nexport const sanitizeJson = (jsonString: string): unknown => {\n\tif (!jsonString || typeof jsonString !== \"string\") {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst sanitized = jsonString\n\t\t\t.replaceAll(/\\)\\s*\\{/g, \") {}\")\n\t\t\t.replaceAll(/\\]\\s*\\{/g, \"] {}\")\n\t\t\t.replaceAll(/\\}\\s*\\{/g, \"} {}\");\n\n\t\tconst parsed: unknown = JSON.parse(sanitized);\n\n\t\tsanitizeObject(parsed);\n\n\t\treturn parsed;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/**\n * Strips ANSI escape sequences from a string.\n *\n * Removes CSI sequences (colour, cursor movement, screen and line erasure, mode\n * switches), OSC sequences (window title and similar), two-character escapes, and\n * any bare ESC left over. This is the control named by `LOG-ANSI-ESCAPE-001`: a\n * terminal-backed log sink treats these as commands, so an attacker who lands them\n * in a log can scroll earlier entries away or overwrite them (CWE-117).\n *\n * Removal is destructive by design — the sequence goes, and any text it carried\n * goes with it. Where the record matters more than the rendering, escape the\n * characters instead of deleting them.\n *\n * @param text - The text potentially containing ANSI codes.\n * @returns The text with ANSI codes removed.\n *\n * @example\n * ```typescript\n * stripAnsi('\\x1b[31mRed text\\x1b[0m'); // 'Red text'\n * ```\n */\nexport const stripAnsi = (text: string): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\t// Every escape sequence, not just the colour ones. The previous pattern was\n\t// `/\\\\x1b\\\\[[0-9;]*m/` — SGR only — so `ESC[2J` (erase display), `ESC[?1049h`\n\t// (alternate screen buffer), `ESC[5A` (cursor up, which overwrites the audit\n\t// lines already written) and `ESC]0;...` (set window title) all survived the\n\t// function that `LOG-ANSI-ESCAPE-001` names as its control. Colour was the one\n\t// case handled, and the only one that is merely cosmetic.\n\t//\n\t// Alternatives are ordered CSI, OSC, then the general ECMA-48 form (ESC,\n\t// intermediates 0x20-0x2F, final 0x30-0x7E), which covers Fs escapes such as\n\t// ESC c. Every quantifier is bounded and none is nested, so nothing backtracks.\n\t// A trailing `?` also removes a bare ESC that begins no valid sequence.\n\treturn text.replaceAll(\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI requires matching control characters\n\t\t/\\u001b(?:\\[[0-?]{0,32}[ -/]{0,8}[@-~]|\\][^\\u0007\\u001b]{0,512}(?:\\u0007|\\u001b\\\\)|[ -/]{0,8}[0-~])?/g,\n\t\t\"\",\n\t);\n};\n\n//#endregion\n\n//#region PII Redaction\n\n/**\n * PII pattern catalog: each entry pairs a global-match regex with the marker that\n * replaces every hit during redaction.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nconst PII_PATTERNS = {\n\tssn: { pattern: /\\b\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{4}\\b/g, marker: \"[SSN]\" },\n\tcreditCard: { pattern: /\\b(?:\\d{4}[-\\s]?){3}\\d{4}\\b/g, marker: \"[CREDIT_CARD]\" },\n\tcreditCardAlt: { pattern: /\\b\\d{15,16}\\b/g, marker: \"[CREDIT_CARD]\" },\n\t// TLD alternation mirrors `EmailSchema` so IDN/Punycode addresses\n\t// (e.g. `user@example.xn--p1ai`) are redacted, not leaked. The `xn--` branch\n\t// is tried first: unlike the anchored (`$`) validators, this pattern ends in\n\t// `\\b`, so `[A-Za-z]{2,}` would otherwise match just `xn` and stop at the\n\t// hyphen, leaving `--p1ai` unredacted. (Also drops the stray `|` from the\n\t// former `[A-Z|a-z]` class.)\n\temail: {\n\t\tpattern: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.(?:xn--[A-Za-z0-9-]+|[A-Za-z]{2,})\\b/g,\n\t\tmarker: \"[EMAIL]\",\n\t},\n\tphone: { pattern: /\\b(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}\\b/g, marker: \"[PHONE]\" },\n\tipv4: { pattern: /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, marker: \"[IP_ADDRESS]\" },\n\tipv6: { pattern: /\\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\b/g, marker: \"[IP_ADDRESS]\" },\n\tdate: {\n\t\tpattern: /\\b(?:\\d{1,2}[/-]\\d{1,2}[/-]\\d{2,4}|\\d{4}[/-]\\d{1,2}[/-]\\d{1,2})\\b/g,\n\t\tmarker: \"[DATE]\",\n\t},\n} as const;\n\n/**\n * Redacts PII from text using Effect Schema validated options.\n *\n * Total — never throws. Failure to decode `options` is returned as a resolved\n * {@link Exit.Exit} failure carrying the {@link S.SchemaError}. By default\n * dates are **not** redacted (`redactDates` defaults to `false`); every other\n * category defaults to `true`.\n *\n * @param text - The text to redact PII from.\n * @param options - Configuration options for redaction.\n * @returns An {@link Exit.Exit}: success carries the redacted text, failure\n * carries a {@link S.SchemaError} from invalid `options`.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n */\nexport const redactPIIEffect = (\n\ttext: string,\n\toptions: PIIRedactionOptions = {},\n): Exit.Exit<string, S.SchemaError> => {\n\tconst parsed = S.decodeUnknownExit(PIIRedactionOptionsSchema)(options);\n\tif (Exit.isFailure(parsed)) return Exit.failCause(parsed.cause);\n\n\tconst {\n\t\tredactEmails = true,\n\t\tredactPhones = true,\n\t\tredactSSN = true,\n\t\tredactCreditCards = true,\n\t\tredactIPs = true,\n\t\tredactDates = false,\n\t} = parsed.value;\n\n\tlet result = text;\n\n\tif (redactSSN) {\n\t\tresult = result.replaceAll(PII_PATTERNS.ssn.pattern, PII_PATTERNS.ssn.marker);\n\t}\n\n\tif (redactCreditCards) {\n\t\tresult = result.replaceAll(PII_PATTERNS.creditCard.pattern, PII_PATTERNS.creditCard.marker);\n\t\tresult = result.replaceAll(\n\t\t\tPII_PATTERNS.creditCardAlt.pattern,\n\t\t\tPII_PATTERNS.creditCardAlt.marker,\n\t\t);\n\t}\n\n\tif (redactEmails) {\n\t\tresult = result.replaceAll(PII_PATTERNS.email.pattern, PII_PATTERNS.email.marker);\n\t}\n\n\tif (redactPhones) {\n\t\tresult = result.replaceAll(PII_PATTERNS.phone.pattern, PII_PATTERNS.phone.marker);\n\t}\n\n\tif (redactIPs) {\n\t\tresult = result.replaceAll(PII_PATTERNS.ipv4.pattern, PII_PATTERNS.ipv4.marker);\n\t\tresult = result.replaceAll(PII_PATTERNS.ipv6.pattern, PII_PATTERNS.ipv6.marker);\n\t}\n\n\tif (redactDates) {\n\t\tresult = result.replaceAll(PII_PATTERNS.date.pattern, PII_PATTERNS.date.marker);\n\t}\n\n\treturn Exit.succeed(result);\n};\n\n/**\n * Redacts common PII patterns in a string for safe logging.\n * Detects and masks SSNs, credit cards, emails, phone numbers, etc.\n *\n * @param text - The text to redact PII from. Non-string input yields `\"\"`.\n * @param options - Configuration options for redaction, plus optional\n * `customPatterns` applied after the built-ins. Each `pattern` **must** be a\n * global (`/g`) RegExp.\n * @returns The text with PII patterns replaced with redaction markers, or `\"\"`\n * for non-string input. If the built-in options fail schema validation the\n * original `text` is returned unredacted rather than throwing.\n * @throws {TypeError} If any `customPatterns` entry uses a non-global RegExp —\n * `String.prototype.replaceAll` rejects non-global patterns.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n *\n * @example\n * ```typescript\n * redactPII('Contact john@example.com or call 555-123-4567');\n * // 'Contact [EMAIL] or call [PHONE]'\n *\n * redactPII('SSN: 123-45-6789');\n * // 'SSN: [SSN]'\n * ```\n */\nexport const redactPII = (\n\ttext: string,\n\toptions: PIIRedactionOptions & {\n\t\tcustomPatterns?: Array<{ pattern: RegExp; replacement: string }>;\n\t} = {},\n): string => {\n\tif (!text || typeof text !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst {\n\t\tredactEmails = true,\n\t\tredactPhones = true,\n\t\tredactSSN = true,\n\t\tredactCreditCards = true,\n\t\tredactIPs = true,\n\t\tredactDates = false,\n\t\tcustomPatterns = [],\n\t} = options;\n\n\tconst result = redactPIIEffect(text, {\n\t\tredactEmails,\n\t\tredactPhones,\n\t\tredactSSN,\n\t\tredactCreditCards,\n\t\tredactIPs,\n\t\tredactDates,\n\t});\n\n\tlet output = Exit.isSuccess(result) ? result.value : text;\n\n\tfor (const { pattern, replacement } of customPatterns) {\n\t\toutput = output.replaceAll(pattern, replacement);\n\t}\n\n\treturn output;\n};\n\n/**\n * Creates a safe string representation of an object for logging,\n * automatically redacting sensitive fields.\n *\n * Never throws: any serialization failure — a circular reference, a `BigInt`\n * value, a throwing `toJSON` — is caught and returned as the sentinel string\n * `\"[Unable to stringify object]\"`. Key matching is case-insensitive and\n * compares the full key name (not substrings), so `apiKey` matches only the\n * literal `\"apiKey\"`, not `\"apiKeyId\"`.\n *\n * @param obj - The object to stringify.\n * @param sensitiveKeys - Key names to redact, compared case-insensitively.\n * @param indent - JSON indentation (default: 2).\n * @returns A JSON string with sensitive values redacted, or the sentinel\n * `\"[Unable to stringify object]\"` when serialization fails.\n * @compliance NIST 800-53 AU-3 (Content of Audit Records)\n *\n * @example\n * ```typescript\n * safeStringify({ user: 'john', password: 'secret123' }, ['password']);\n * // '{\\n \"user\": \"john\",\\n \"password\": \"[REDACTED]\"\\n}'\n * ```\n */\nexport const safeStringify = (\n\tobj: unknown,\n\tsensitiveKeys: string[] = [\n\t\t\"password\",\n\t\t\"token\",\n\t\t\"apiKey\",\n\t\t\"secret\",\n\t\t\"authorization\",\n\t\t\"cookie\",\n\t\t\"ssn\",\n\t\t\"creditCard\",\n\t],\n\tindent = 2,\n): string => {\n\tconst sensitiveKeysLower = new Set(sensitiveKeys.map((k) => k.toLowerCase()));\n\n\tconst replacer = (_key: string, value: unknown): unknown => {\n\t\tif (_key && sensitiveKeysLower.has(_key.toLowerCase())) {\n\t\t\treturn \"[REDACTED]\";\n\t\t}\n\t\treturn value;\n\t};\n\n\ttry {\n\t\treturn JSON.stringify(obj, replacer, indent);\n\t} catch {\n\t\treturn \"[Unable to stringify object]\";\n\t}\n};\n\n//#endregion\n\n//#region Validation Helpers\n\n/**\n * Validates if a string is a valid email address using Effect Schema.\n *\n * Narrows the input to {@link Email} on success, so validated call sites\n * carry the brand into downstream code.\n *\n * @param email - The string to validate.\n * @returns true if valid email, false otherwise.\n */\nexport const isValidEmail = (email: string): email is Email => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(EmailSchema)(email));\n};\n\n/**\n * Validates if a string is a valid phone number using Effect Schema.\n *\n * Narrows the input to {@link PhoneNumber} on success.\n *\n * @param phone - The string to validate.\n * @returns true if valid phone number, false otherwise.\n */\nexport const isValidPhone = (phone: string): phone is PhoneNumber => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(PhoneNumberSchema)(phone));\n};\n\n/**\n * Validates if a string is a valid SSN using Effect Schema.\n *\n * Narrows the input to {@link SSN} on success.\n *\n * @param ssn - The string to validate.\n * @returns true if valid SSN, false otherwise.\n */\nexport const isValidSSN = (ssn: string): ssn is SSN => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(SSNSchema)(ssn));\n};\n\n/**\n * Validates if a string is a safe URL using Effect Schema.\n *\n * Narrows the input to {@link SafeUrl} on success.\n *\n * @param url - The string to validate.\n * @returns true if valid and safe URL, false otherwise.\n */\nexport const isValidUrl = (url: string): url is SafeUrl => {\n\treturn Exit.isSuccess(S.decodeUnknownExit(SafeUrlSchema)(url));\n};\n//#endregion\n"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//#region src/threats/capec.generated.d.ts
|
|
2
2
|
/**
|
|
3
3
|
* Copyright 2026 ResQ Systems, Inc.
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
5
|
*
|
|
5
6
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
7
|
* you may not use this file except in compliance with the License.
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
* @module @resq-systems/security/threats/capec
|
|
32
33
|
*/
|
|
33
34
|
/** One CAPEC attack pattern, reduced to what a consumer triaging a finding needs. */
|
|
34
|
-
interface AttackPattern {
|
|
35
|
+
export interface AttackPattern {
|
|
35
36
|
/** CAPEC identifier. */
|
|
36
37
|
readonly capec: number;
|
|
37
38
|
/** Pattern name, as published by MITRE. */
|
|
@@ -44,7 +45,7 @@ interface AttackPattern {
|
|
|
44
45
|
readonly cwes: readonly number[];
|
|
45
46
|
}
|
|
46
47
|
/** Every relevant pattern, ordered by CAPEC id. */
|
|
47
|
-
declare const ATTACK_PATTERNS: readonly AttackPattern[];
|
|
48
|
+
export declare const ATTACK_PATTERNS: readonly AttackPattern[];
|
|
48
49
|
/**
|
|
49
50
|
* Attack patterns related to a weakness.
|
|
50
51
|
*
|
|
@@ -53,7 +54,6 @@ declare const ATTACK_PATTERNS: readonly AttackPattern[];
|
|
|
53
54
|
* publishes no Standard or Detailed pattern for that weakness — not that the weakness
|
|
54
55
|
* is unimportant.
|
|
55
56
|
*/
|
|
56
|
-
declare function attackPatternsForCwe(cwe: number | undefined): readonly AttackPattern[];
|
|
57
|
+
export declare function attackPatternsForCwe(cwe: number | undefined): readonly AttackPattern[];
|
|
57
58
|
//#endregion
|
|
58
|
-
export { ATTACK_PATTERNS, AttackPattern, attackPatternsForCwe };
|
|
59
59
|
//# sourceMappingURL=capec.generated.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capec.generated.d.mts","names":[],"sources":["../../src/threats/capec.generated.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"capec.generated.d.mts","names":[],"sources":["../../src/threats/capec.generated.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCiB;;WAEP;;WAEA;;WAEA;;WAEA;;WAEA;;;qBAIG,0BAA0B;;;;;;;;;wBAknBvB,qBAAqB,mCAAmC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capec.generated.mjs","names":[],"sources":["../../src/threats/capec.generated.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview GENERATED FILE — do not edit by hand.\n *\n * MITRE CAPEC attack patterns whose Related Weaknesses intersect a CWE this catalog\n * uses. Derived from CAPEC view 1000 (Mechanisms of Attack), filtered to Standard and\n * Detailed abstractions because Meta patterns are too abstract to help anyone triaging\n * a finding.\n *\n * **Attribution, not conformance.** A CAPEC id tells a reader which family of attack a\n * finding belongs to and where to read more. It does not assert that the rule detects\n * every technique in that pattern, and nothing here changes a score or a verdict.\n *\n * Regenerate with `bun scripts/generate-capec.ts <capec-1000.csv>`.\n *\n * @module @resq-systems/security/threats/capec\n */\n\n/** One CAPEC attack pattern, reduced to what a consumer triaging a finding needs. */\nexport interface AttackPattern {\n\t/** CAPEC identifier. */\n\treadonly capec: number;\n\t/** Pattern name, as published by MITRE. */\n\treadonly name: string;\n\t/** MITRE abstraction level: \"Standard\" or \"Detailed\". */\n\treadonly abstraction: string;\n\t/** MITRE's typical severity for the pattern. */\n\treadonly severity: string;\n\t/** Catalog CWEs this pattern relates to. */\n\treadonly cwes: readonly number[];\n}\n\n/** Every relevant pattern, ordered by CAPEC id. */\nexport const ATTACK_PATTERNS: readonly AttackPattern[] = [\n\t{\n\t\tcapec: 1,\n\t\tname: \"Accessing Functionality Not Properly Constrained by ACLs\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [1321],\n\t},\n\t{\n\t\tcapec: 3,\n\t\tname: \"Using Leading 'Ghost' Character Sequences to Bypass Input Filters\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 6,\n\t\tname: \"Argument Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 78],\n\t},\n\t{\n\t\tcapec: 7,\n\t\tname: \"Blind SQL Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 89],\n\t},\n\t{\n\t\tcapec: 8,\n\t\tname: \"Buffer Overflow in an API Call\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 9,\n\t\tname: \"Buffer Overflow in Local Command-Line Utilities\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 10,\n\t\tname: \"Buffer Overflow via Environment Variables\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 13,\n\t\tname: \"Subverting Environment Variable Values\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 14,\n\t\tname: \"Client-side Injection-induced Buffer Overflow\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 15,\n\t\tname: \"Command Delimiters\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [78],\n\t},\n\t{\n\t\tcapec: 24,\n\t\tname: \"Filter Failure through Buffer Overflow\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 31,\n\t\tname: \"Accessing/Intercepting/Modifying HTTP Cookies\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [113],\n\t},\n\t{\n\t\tcapec: 34,\n\t\tname: \"HTTP Response Splitting\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 113],\n\t},\n\t{\n\t\tcapec: 35,\n\t\tname: \"Leverage Executable Code in Non-Executable Files\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [94, 95, 97],\n\t},\n\t{\n\t\tcapec: 41,\n\t\tname: \"Using Meta-characters in E-mail Headers to Inject Malicious Payloads\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [88],\n\t},\n\t{\n\t\tcapec: 42,\n\t\tname: \"MIME Conversion\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 43,\n\t\tname: \"Exploiting Multiple Input Interpretation Layers\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 78],\n\t},\n\t{\n\t\tcapec: 45,\n\t\tname: \"Buffer Overflow via Symbolic Links\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 46,\n\t\tname: \"Overflow Variables and Tags\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 47,\n\t\tname: \"Buffer Overflow via Parameter Expansion\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 50,\n\t\tname: \"Password Recovery Exploitation\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 51,\n\t\tname: \"Poison Web Service Registry\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 52,\n\t\tname: \"Embedding NULL Bytes\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 158],\n\t},\n\t{\n\t\tcapec: 53,\n\t\tname: \"Postfix, Null Terminate, and Backslash\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 158],\n\t},\n\t{\n\t\tcapec: 63,\n\t\tname: \"Cross-Site Scripting (XSS)\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [79],\n\t},\n\t{\n\t\tcapec: 64,\n\t\tname: \"Using Slashes and URL Encoding Combined to Bypass Validation Logic\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [22, 74, 177],\n\t},\n\t{\n\t\tcapec: 66,\n\t\tname: \"SQL Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [89],\n\t},\n\t{\n\t\tcapec: 67,\n\t\tname: \"String Format Overflow in syslog()\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 71,\n\t\tname: \"Using Unicode Encoding to Bypass Validation Logic\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 72,\n\t\tname: \"URL Encoding\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 177],\n\t},\n\t{\n\t\tcapec: 76,\n\t\tname: \"Manipulating Web Input to File System Calls\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [22, 74],\n\t},\n\t{\n\t\tcapec: 77,\n\t\tname: \"Manipulating User-Controlled Variables\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [94, 1321],\n\t},\n\t{\n\t\tcapec: 78,\n\t\tname: \"Using Escaped Slashes in Alternate Encoding\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [22, 74],\n\t},\n\t{\n\t\tcapec: 79,\n\t\tname: \"Using Slashes in Alternate Encoding\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [22, 74],\n\t},\n\t{\n\t\tcapec: 80,\n\t\tname: \"Using UTF-8 Encoding to Bypass Validation Logic\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 81,\n\t\tname: \"Web Server Logs Tampering\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [117],\n\t},\n\t{\n\t\tcapec: 83,\n\t\tname: \"XPath Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 91],\n\t},\n\t{\n\t\tcapec: 84,\n\t\tname: \"XQuery Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 85,\n\t\tname: \"AJAX Footprinting\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Low\",\n\t\tcwes: [79, 113],\n\t},\n\t{\n\t\tcapec: 88,\n\t\tname: \"OS Command Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [78, 88],\n\t},\n\t{\n\t\tcapec: 93,\n\t\tname: \"Log Injection-Tampering-Forging\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [117],\n\t},\n\t{\n\t\tcapec: 98,\n\t\tname: \"Phishing\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [451],\n\t},\n\t{\n\t\tcapec: 101,\n\t\tname: \"Server Side Include (SSI) Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 97],\n\t},\n\t{\n\t\tcapec: 102,\n\t\tname: \"Session Sidejacking\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 105,\n\t\tname: \"HTTP Request Splitting\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 113],\n\t},\n\t{\n\t\tcapec: 108,\n\t\tname: \"Command Line Execution through SQL Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [74, 78, 89],\n\t},\n\t{\n\t\tcapec: 109,\n\t\tname: \"Object Relational Mapping Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [89],\n\t},\n\t{\n\t\tcapec: 110,\n\t\tname: \"SQL Injection through SOAP Parameter Tampering\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [89],\n\t},\n\t{\n\t\tcapec: 120,\n\t\tname: \"Double Encoding\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [74, 177],\n\t},\n\t{\n\t\tcapec: 126,\n\t\tname: \"Path Traversal\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [22],\n\t},\n\t{\n\t\tcapec: 135,\n\t\tname: \"Format String Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 136,\n\t\tname: \"LDAP Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [90],\n\t},\n\t{\n\t\tcapec: 163,\n\t\tname: \"Spear Phishing\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [451],\n\t},\n\t{\n\t\tcapec: 164,\n\t\tname: \"Mobile Phishing\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [451],\n\t},\n\t{\n\t\tcapec: 174,\n\t\tname: \"Flash Parameter Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [88],\n\t},\n\t{\n\t\tcapec: 180,\n\t\tname: \"Exploiting Incorrectly Configured Access Control Security Levels\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [1321],\n\t},\n\t{\n\t\tcapec: 193,\n\t\tname: \"PHP Remote File Inclusion\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [98],\n\t},\n\t{\n\t\tcapec: 209,\n\t\tname: \"XSS Using MIME Type Mismatch\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [79],\n\t},\n\t{\n\t\tcapec: 215,\n\t\tname: \"Fuzzing for application mapping\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Low\",\n\t\tcwes: [532],\n\t},\n\t{\n\t\tcapec: 221,\n\t\tname: \"Data Serialization External Entities Blowup\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"\",\n\t\tcwes: [611],\n\t},\n\t{\n\t\tcapec: 250,\n\t\tname: \"XML Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"\",\n\t\tcwes: [74, 91],\n\t},\n\t{\n\t\tcapec: 267,\n\t\tname: \"Leverage Alternate Encoding\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 268,\n\t\tname: \"Audit Log Manipulation\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"\",\n\t\tcwes: [117],\n\t},\n\t{\n\t\tcapec: 273,\n\t\tname: \"HTTP Response Smuggling\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 460,\n\t\tname: \"HTTP Parameter Pollution (HPP)\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [88, 235],\n\t},\n\t{\n\t\tcapec: 463,\n\t\tname: \"Padding Oracle Crypto Attack\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [347],\n\t},\n\t{\n\t\tcapec: 468,\n\t\tname: \"Generic Cross-Browser Cross-Domain Theft\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [177],\n\t},\n\t{\n\t\tcapec: 470,\n\t\tname: \"Expanding Control over the Operating System from the Database\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [89],\n\t},\n\t{\n\t\tcapec: 474,\n\t\tname: \"Signature Spoofing by Key Theft\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 475,\n\t\tname: \"Signature Spoofing by Improper Validation\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [347],\n\t},\n\t{\n\t\tcapec: 509,\n\t\tname: \"Kerberoasting\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 551,\n\t\tname: \"Modify Existing Service\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 555,\n\t\tname: \"Remote Services with Stolen Credentials\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 561,\n\t\tname: \"Windows Admin Shares with Stolen Credentials\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 588,\n\t\tname: \"DOM-Based XSS\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [79],\n\t},\n\t{\n\t\tcapec: 591,\n\t\tname: \"Reflected XSS\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [79],\n\t},\n\t{\n\t\tcapec: 592,\n\t\tname: \"Stored XSS\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [79],\n\t},\n\t{\n\t\tcapec: 597,\n\t\tname: \"Absolute Path Traversal\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"\",\n\t\tcwes: [36],\n\t},\n\t{\n\t\tcapec: 600,\n\t\tname: \"Credential Stuffing\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 632,\n\t\tname: \"Homograph Attack via Homoglyphs\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [1007],\n\t},\n\t{\n\t\tcapec: 644,\n\t\tname: \"Use of Captured Hashes (Pass The Hash)\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 645,\n\t\tname: \"Use of Captured Tickets (Pass The Ticket)\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 652,\n\t\tname: \"Use of Known Kerberos Credentials\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 653,\n\t\tname: \"Use of Known Operating System Credentials\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 664,\n\t\tname: \"Server Side Request Forgery\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [918],\n\t},\n\t{\n\t\tcapec: 676,\n\t\tname: \"NoSQL Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [943],\n\t},\n];\n\n/** Index from CWE to the patterns referencing it, built once at module load. */\nconst BY_CWE: ReadonlyMap<number, readonly AttackPattern[]> = (() => {\n\tconst index = new Map<number, AttackPattern[]>();\n\tfor (const pattern of ATTACK_PATTERNS) {\n\t\tfor (const cwe of pattern.cwes) {\n\t\t\tconst bucket = index.get(cwe);\n\t\t\tif (bucket) bucket.push(pattern);\n\t\t\telse index.set(cwe, [pattern]);\n\t\t}\n\t}\n\treturn index;\n})();\n\n/**\n * Attack patterns related to a weakness.\n *\n * @param cwe - CWE identifier, typically a rule's `cwe` field.\n * @returns Related patterns, or an empty array when none is mapped. Empty means MITRE\n * publishes no Standard or Detailed pattern for that weakness — not that the weakness\n * is unimportant.\n */\nexport function attackPatternsForCwe(cwe: number | undefined): readonly AttackPattern[] {\n\tif (typeof cwe !== \"number\") return [];\n\treturn BY_CWE.get(cwe) ?? [];\n}\n"],"mappings":";;AAgDA,MAAa,kBAA4C;CACxD;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI;CACZ;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM;GAAC;GAAI;GAAI;EAAE;CAClB;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM;GAAC;GAAI;GAAI;EAAG;CACnB;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,IAAI;CAChB;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM;GAAC;GAAI;GAAI;EAAE;CAClB;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI;CACZ;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI;CACZ;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;AACD;;AAGA,MAAM,gBAA+D;CACpE,MAAM,wBAAQ,IAAI,IAA6B;CAC/C,KAAK,MAAM,WAAW,iBACrB,KAAK,MAAM,OAAO,QAAQ,MAAM;EAC/B,MAAM,SAAS,MAAM,IAAI,GAAG;EAC5B,IAAI,QAAQ,OAAO,KAAK,OAAO;OAC1B,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC;CAC9B;CAED,OAAO;AACR,EAAA,CAAG;;;;;;;;;AAUH,SAAgB,qBAAqB,KAAmD;CACvF,IAAI,OAAO,QAAQ,UAAU,OAAO,CAAC;CACrC,OAAO,OAAO,IAAI,GAAG,KAAK,CAAC;AAC5B"}
|
|
1
|
+
{"version":3,"file":"capec.generated.mjs","names":[],"sources":["../../src/threats/capec.generated.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n * SPDX-License-Identifier: Apache-2.0\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview GENERATED FILE — do not edit by hand.\n *\n * MITRE CAPEC attack patterns whose Related Weaknesses intersect a CWE this catalog\n * uses. Derived from CAPEC view 1000 (Mechanisms of Attack), filtered to Standard and\n * Detailed abstractions because Meta patterns are too abstract to help anyone triaging\n * a finding.\n *\n * **Attribution, not conformance.** A CAPEC id tells a reader which family of attack a\n * finding belongs to and where to read more. It does not assert that the rule detects\n * every technique in that pattern, and nothing here changes a score or a verdict.\n *\n * Regenerate with `bun scripts/generate-capec.ts <capec-1000.csv>`.\n *\n * @module @resq-systems/security/threats/capec\n */\n\n/** One CAPEC attack pattern, reduced to what a consumer triaging a finding needs. */\nexport interface AttackPattern {\n\t/** CAPEC identifier. */\n\treadonly capec: number;\n\t/** Pattern name, as published by MITRE. */\n\treadonly name: string;\n\t/** MITRE abstraction level: \"Standard\" or \"Detailed\". */\n\treadonly abstraction: string;\n\t/** MITRE's typical severity for the pattern. */\n\treadonly severity: string;\n\t/** Catalog CWEs this pattern relates to. */\n\treadonly cwes: readonly number[];\n}\n\n/** Every relevant pattern, ordered by CAPEC id. */\nexport const ATTACK_PATTERNS: readonly AttackPattern[] = [\n\t{\n\t\tcapec: 1,\n\t\tname: \"Accessing Functionality Not Properly Constrained by ACLs\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [1321],\n\t},\n\t{\n\t\tcapec: 3,\n\t\tname: \"Using Leading 'Ghost' Character Sequences to Bypass Input Filters\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 6,\n\t\tname: \"Argument Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 78],\n\t},\n\t{\n\t\tcapec: 7,\n\t\tname: \"Blind SQL Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 89],\n\t},\n\t{\n\t\tcapec: 8,\n\t\tname: \"Buffer Overflow in an API Call\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 9,\n\t\tname: \"Buffer Overflow in Local Command-Line Utilities\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 10,\n\t\tname: \"Buffer Overflow via Environment Variables\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 13,\n\t\tname: \"Subverting Environment Variable Values\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 14,\n\t\tname: \"Client-side Injection-induced Buffer Overflow\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 15,\n\t\tname: \"Command Delimiters\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [78],\n\t},\n\t{\n\t\tcapec: 24,\n\t\tname: \"Filter Failure through Buffer Overflow\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 31,\n\t\tname: \"Accessing/Intercepting/Modifying HTTP Cookies\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [113],\n\t},\n\t{\n\t\tcapec: 34,\n\t\tname: \"HTTP Response Splitting\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 113],\n\t},\n\t{\n\t\tcapec: 35,\n\t\tname: \"Leverage Executable Code in Non-Executable Files\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [94, 95, 97],\n\t},\n\t{\n\t\tcapec: 41,\n\t\tname: \"Using Meta-characters in E-mail Headers to Inject Malicious Payloads\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [88],\n\t},\n\t{\n\t\tcapec: 42,\n\t\tname: \"MIME Conversion\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 43,\n\t\tname: \"Exploiting Multiple Input Interpretation Layers\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 78],\n\t},\n\t{\n\t\tcapec: 45,\n\t\tname: \"Buffer Overflow via Symbolic Links\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 46,\n\t\tname: \"Overflow Variables and Tags\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 47,\n\t\tname: \"Buffer Overflow via Parameter Expansion\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 50,\n\t\tname: \"Password Recovery Exploitation\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 51,\n\t\tname: \"Poison Web Service Registry\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 52,\n\t\tname: \"Embedding NULL Bytes\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 158],\n\t},\n\t{\n\t\tcapec: 53,\n\t\tname: \"Postfix, Null Terminate, and Backslash\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 158],\n\t},\n\t{\n\t\tcapec: 63,\n\t\tname: \"Cross-Site Scripting (XSS)\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [79],\n\t},\n\t{\n\t\tcapec: 64,\n\t\tname: \"Using Slashes and URL Encoding Combined to Bypass Validation Logic\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [22, 74, 177],\n\t},\n\t{\n\t\tcapec: 66,\n\t\tname: \"SQL Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [89],\n\t},\n\t{\n\t\tcapec: 67,\n\t\tname: \"String Format Overflow in syslog()\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 71,\n\t\tname: \"Using Unicode Encoding to Bypass Validation Logic\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 72,\n\t\tname: \"URL Encoding\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 177],\n\t},\n\t{\n\t\tcapec: 76,\n\t\tname: \"Manipulating Web Input to File System Calls\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [22, 74],\n\t},\n\t{\n\t\tcapec: 77,\n\t\tname: \"Manipulating User-Controlled Variables\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [94, 1321],\n\t},\n\t{\n\t\tcapec: 78,\n\t\tname: \"Using Escaped Slashes in Alternate Encoding\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [22, 74],\n\t},\n\t{\n\t\tcapec: 79,\n\t\tname: \"Using Slashes in Alternate Encoding\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [22, 74],\n\t},\n\t{\n\t\tcapec: 80,\n\t\tname: \"Using UTF-8 Encoding to Bypass Validation Logic\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 81,\n\t\tname: \"Web Server Logs Tampering\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [117],\n\t},\n\t{\n\t\tcapec: 83,\n\t\tname: \"XPath Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 91],\n\t},\n\t{\n\t\tcapec: 84,\n\t\tname: \"XQuery Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 85,\n\t\tname: \"AJAX Footprinting\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Low\",\n\t\tcwes: [79, 113],\n\t},\n\t{\n\t\tcapec: 88,\n\t\tname: \"OS Command Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [78, 88],\n\t},\n\t{\n\t\tcapec: 93,\n\t\tname: \"Log Injection-Tampering-Forging\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [117],\n\t},\n\t{\n\t\tcapec: 98,\n\t\tname: \"Phishing\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [451],\n\t},\n\t{\n\t\tcapec: 101,\n\t\tname: \"Server Side Include (SSI) Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 97],\n\t},\n\t{\n\t\tcapec: 102,\n\t\tname: \"Session Sidejacking\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 105,\n\t\tname: \"HTTP Request Splitting\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74, 113],\n\t},\n\t{\n\t\tcapec: 108,\n\t\tname: \"Command Line Execution through SQL Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [74, 78, 89],\n\t},\n\t{\n\t\tcapec: 109,\n\t\tname: \"Object Relational Mapping Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [89],\n\t},\n\t{\n\t\tcapec: 110,\n\t\tname: \"SQL Injection through SOAP Parameter Tampering\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [89],\n\t},\n\t{\n\t\tcapec: 120,\n\t\tname: \"Double Encoding\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [74, 177],\n\t},\n\t{\n\t\tcapec: 126,\n\t\tname: \"Path Traversal\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [22],\n\t},\n\t{\n\t\tcapec: 135,\n\t\tname: \"Format String Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 136,\n\t\tname: \"LDAP Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [90],\n\t},\n\t{\n\t\tcapec: 163,\n\t\tname: \"Spear Phishing\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [451],\n\t},\n\t{\n\t\tcapec: 164,\n\t\tname: \"Mobile Phishing\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [451],\n\t},\n\t{\n\t\tcapec: 174,\n\t\tname: \"Flash Parameter Injection\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [88],\n\t},\n\t{\n\t\tcapec: 180,\n\t\tname: \"Exploiting Incorrectly Configured Access Control Security Levels\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [1321],\n\t},\n\t{\n\t\tcapec: 193,\n\t\tname: \"PHP Remote File Inclusion\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [98],\n\t},\n\t{\n\t\tcapec: 209,\n\t\tname: \"XSS Using MIME Type Mismatch\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [79],\n\t},\n\t{\n\t\tcapec: 215,\n\t\tname: \"Fuzzing for application mapping\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Low\",\n\t\tcwes: [532],\n\t},\n\t{\n\t\tcapec: 221,\n\t\tname: \"Data Serialization External Entities Blowup\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"\",\n\t\tcwes: [611],\n\t},\n\t{\n\t\tcapec: 250,\n\t\tname: \"XML Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"\",\n\t\tcwes: [74, 91],\n\t},\n\t{\n\t\tcapec: 267,\n\t\tname: \"Leverage Alternate Encoding\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 268,\n\t\tname: \"Audit Log Manipulation\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"\",\n\t\tcwes: [117],\n\t},\n\t{\n\t\tcapec: 273,\n\t\tname: \"HTTP Response Smuggling\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [74],\n\t},\n\t{\n\t\tcapec: 460,\n\t\tname: \"HTTP Parameter Pollution (HPP)\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [88, 235],\n\t},\n\t{\n\t\tcapec: 463,\n\t\tname: \"Padding Oracle Crypto Attack\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [347],\n\t},\n\t{\n\t\tcapec: 468,\n\t\tname: \"Generic Cross-Browser Cross-Domain Theft\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [177],\n\t},\n\t{\n\t\tcapec: 470,\n\t\tname: \"Expanding Control over the Operating System from the Database\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [89],\n\t},\n\t{\n\t\tcapec: 474,\n\t\tname: \"Signature Spoofing by Key Theft\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 475,\n\t\tname: \"Signature Spoofing by Improper Validation\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [347],\n\t},\n\t{\n\t\tcapec: 509,\n\t\tname: \"Kerberoasting\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 551,\n\t\tname: \"Modify Existing Service\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 555,\n\t\tname: \"Remote Services with Stolen Credentials\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 561,\n\t\tname: \"Windows Admin Shares with Stolen Credentials\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 588,\n\t\tname: \"DOM-Based XSS\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [79],\n\t},\n\t{\n\t\tcapec: 591,\n\t\tname: \"Reflected XSS\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [79],\n\t},\n\t{\n\t\tcapec: 592,\n\t\tname: \"Stored XSS\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Very High\",\n\t\tcwes: [79],\n\t},\n\t{\n\t\tcapec: 597,\n\t\tname: \"Absolute Path Traversal\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"\",\n\t\tcwes: [36],\n\t},\n\t{\n\t\tcapec: 600,\n\t\tname: \"Credential Stuffing\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 632,\n\t\tname: \"Homograph Attack via Homoglyphs\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"Medium\",\n\t\tcwes: [1007],\n\t},\n\t{\n\t\tcapec: 644,\n\t\tname: \"Use of Captured Hashes (Pass The Hash)\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 645,\n\t\tname: \"Use of Captured Tickets (Pass The Ticket)\",\n\t\tabstraction: \"Detailed\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 652,\n\t\tname: \"Use of Known Kerberos Credentials\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 653,\n\t\tname: \"Use of Known Operating System Credentials\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [522],\n\t},\n\t{\n\t\tcapec: 664,\n\t\tname: \"Server Side Request Forgery\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [918],\n\t},\n\t{\n\t\tcapec: 676,\n\t\tname: \"NoSQL Injection\",\n\t\tabstraction: \"Standard\",\n\t\tseverity: \"High\",\n\t\tcwes: [943],\n\t},\n];\n\n/** Index from CWE to the patterns referencing it, built once at module load. */\nconst BY_CWE: ReadonlyMap<number, readonly AttackPattern[]> = (() => {\n\tconst index = new Map<number, AttackPattern[]>();\n\tfor (const pattern of ATTACK_PATTERNS) {\n\t\tfor (const cwe of pattern.cwes) {\n\t\t\tconst bucket = index.get(cwe);\n\t\t\tif (bucket) bucket.push(pattern);\n\t\t\telse index.set(cwe, [pattern]);\n\t\t}\n\t}\n\treturn index;\n})();\n\n/**\n * Attack patterns related to a weakness.\n *\n * @param cwe - CWE identifier, typically a rule's `cwe` field.\n * @returns Related patterns, or an empty array when none is mapped. Empty means MITRE\n * publishes no Standard or Detailed pattern for that weakness — not that the weakness\n * is unimportant.\n */\nexport function attackPatternsForCwe(cwe: number | undefined): readonly AttackPattern[] {\n\tif (typeof cwe !== \"number\") return [];\n\treturn BY_CWE.get(cwe) ?? [];\n}\n"],"mappings":";;AAiDA,MAAa,kBAA4C;CACxD;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI;CACZ;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM;GAAC;GAAI;GAAI;EAAE;CAClB;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM;GAAC;GAAI;GAAI;EAAG;CACnB;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,IAAI;CAChB;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM;GAAC;GAAI;GAAI;EAAE;CAClB;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI;CACZ;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,EAAE;CACd;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI,GAAG;CACf;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,EAAE;CACV;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,IAAI;CACZ;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;CACA;EACC,OAAO;EACP,MAAM;EACN,aAAa;EACb,UAAU;EACV,MAAM,CAAC,GAAG;CACX;AACD;;AAGA,MAAM,gBAA+D;CACpE,MAAM,wBAAQ,IAAI,IAA6B;CAC/C,KAAK,MAAM,WAAW,iBACrB,KAAK,MAAM,OAAO,QAAQ,MAAM;EAC/B,MAAM,SAAS,MAAM,IAAI,GAAG;EAC5B,IAAI,QAAQ,OAAO,KAAK,OAAO;OAC1B,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC;CAC9B;CAED,OAAO;AACR,EAAA,CAAG;;;;;;;;;AAUH,SAAgB,qBAAqB,KAAmD;CACvF,IAAI,OAAO,QAAQ,UAAU,OAAO,CAAC;CACrC,OAAO,OAAO,IAAI,GAAG,KAAK,CAAC;AAC5B"}
|
package/lib/threats/engine.d.mts
CHANGED
|
@@ -7,9 +7,9 @@ import { EventContext, ThreatContext, ThreatFinding, ThreatPolicy, ThreatSeverit
|
|
|
7
7
|
* unbounded nested quantifiers — so this is a second line of defense rather than the
|
|
8
8
|
* only one, but a 50 MB paste should not become a 50 MB × 89-rule scan regardless.
|
|
9
9
|
*/
|
|
10
|
-
declare const MAX_SCAN_LENGTH = 100000;
|
|
10
|
+
export declare const MAX_SCAN_LENGTH = 100000;
|
|
11
11
|
/** Controls what {@link scanForThreats} evaluates and how it grades the outcome. */
|
|
12
|
-
interface ThreatScanOptions {
|
|
12
|
+
export interface ThreatScanOptions {
|
|
13
13
|
/**
|
|
14
14
|
* Correlation metadata, echoed onto the result untouched.
|
|
15
15
|
*
|
|
@@ -44,7 +44,7 @@ interface ThreatScanOptions {
|
|
|
44
44
|
readonly policy?: ThreatPolicy;
|
|
45
45
|
}
|
|
46
46
|
/** Outcome of a scan. */
|
|
47
|
-
interface ThreatScanResult {
|
|
47
|
+
export interface ThreatScanResult {
|
|
48
48
|
/**
|
|
49
49
|
* The {@link EventContext} the caller supplied, echoed verbatim.
|
|
50
50
|
*
|
|
@@ -88,7 +88,6 @@ interface ThreatScanResult {
|
|
|
88
88
|
* scanForThreats(bio, { contexts: ["filesystem"] }).verdict; // "review"
|
|
89
89
|
* ```
|
|
90
90
|
*/
|
|
91
|
-
declare function scanForThreats(input: string, options?: ThreatScanOptions): ThreatScanResult;
|
|
91
|
+
export declare function scanForThreats(input: string, options?: ThreatScanOptions): ThreatScanResult;
|
|
92
92
|
//#endregion
|
|
93
|
-
export { MAX_SCAN_LENGTH, ThreatScanOptions, ThreatScanResult, scanForThreats };
|
|
94
93
|
//# sourceMappingURL=engine.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.mts","names":[],"sources":["../../src/threats/engine.ts"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"engine.d.mts","names":[],"sources":["../../src/threats/engine.ts"],"mappings":";;;;;;;;;qBAyDa;;iBAgBI;;;;;;WAMP,QAAQ;;;;;;;;;WASR,oBAAoB;;WAEpB;;;;;;WAMA;;WAEA,cAAc;;;;;WAKd;;WAEA,SAAS;;;iBAIF;;;;;;WAMP,QAAQ;;WAER;;WAEA;;WAEA,SAAS;;WAET,mBAAmB;;WAEnB,gBAAgB;;WAEhB;;;;;;;;;;;;;;;;;;;;;;;;;;wBAkFM,eAAe,eAAe,UAAS,oBAAyB"}
|
package/lib/threats/engine.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { buildInputVariants } from "./variants.mjs";
|
|
|
5
5
|
//#region src/threats/engine.ts
|
|
6
6
|
/**
|
|
7
7
|
* Copyright 2026 ResQ Systems, Inc.
|
|
8
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
8
9
|
*
|
|
9
10
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
11
|
* you may not use this file except in compliance with the License.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.mjs","names":[],"sources":["../../src/threats/engine.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview The scan engine: evaluate the rules applicable to a declared set of\n * sinks against a bounded set of input representations, then score the result.\n *\n * The engine's defining constraint is that **the caller must say where the value is\n * going**. Running every detector against every field is what makes signature-based\n * validation unusable in practice — a biography containing `C:\\Windows`, a support\n * ticket containing `1=1`, and a programming question containing `eval(` are all\n * ordinary text, and only become evidence when the value actually reaches a\n * filesystem, SQL, or HTML sink. Declaring the context is the difference between a\n * detector that catches attacks and one that rejects customers.\n *\n * @module @resq-systems/security/threats/engine\n */\n\nimport { getRulesForContexts } from \"./rules/index.js\";\nimport { calculateThreatScore, verdictForScore } from \"./scoring.js\";\nimport {\n\tDEFAULT_THREAT_POLICY,\n\ttype EventContext,\n\ttype InputVariant,\n\tSEVERITY_ORDER,\n\ttype ThreatContext,\n\ttype ThreatFinding,\n\ttype ThreatPolicy,\n\ttype ThreatSeverity,\n\ttype ThreatType,\n\ttype ThreatVerdict,\n} from \"./types.js\";\nimport { buildInputVariants } from \"./variants.js\";\n\n//#region Constants\n\n/**\n * Characters scanned before truncation.\n *\n * Bounds worst-case regex cost per call. Individual rules are bounded too — no\n * unbounded nested quantifiers — so this is a second line of defense rather than the\n * only one, but a 50 MB paste should not become a 50 MB × 89-rule scan regardless.\n */\nexport const MAX_SCAN_LENGTH = 100_000;\n\n/** Matched substrings are truncated to this many characters before entering a finding. */\nconst MAX_MATCH_EXCERPT = 50;\n\n/**\n * Run length at which a repeated single character is reported as resource abuse.\n * Set well above anything that occurs in prose or formatted text.\n */\nconst REPETITION_THRESHOLD = 200;\n\n//#endregion\n\n//#region Options and result\n\n/** Controls what {@link scanForThreats} evaluates and how it grades the outcome. */\nexport interface ThreatScanOptions {\n\t/**\n\t * Correlation metadata, echoed onto the result untouched.\n\t *\n\t * Affects nothing about detection. See {@link EventContext} for why it exists.\n\t */\n\treadonly event?: EventContext;\n\t/**\n\t * Sinks the value is destined for.\n\t *\n\t * Defaults to `[\"general_text\"]`, which enables only the universal rules — bidi\n\t * overrides, invisible characters, control characters. Any caller that wants\n\t * injection detection must name the sink it is protecting; that requirement is\n\t * the package's primary false-positive control, not an inconvenience.\n\t */\n\treadonly contexts?: readonly ThreatContext[];\n\t/** Truncation bound. Defaults to {@link MAX_SCAN_LENGTH}. */\n\treadonly maxLength?: number;\n\t/**\n\t * Scan encoded representations in addition to the raw string. Defaults to `true`.\n\t * Pass `false` when the caller has already decoded the value and re-decoding\n\t * would misrepresent it.\n\t */\n\treadonly scanVariants?: boolean;\n\t/** Drop findings below this severity before scoring. Defaults to `\"low\"`. */\n\treadonly minSeverity?: ThreatSeverity;\n\t/**\n\t * Rule IDs to skip. The tuning knob to reach for first: silencing one noisy rule\n\t * on one route beats disabling a whole category.\n\t */\n\treadonly excludeRuleIds?: readonly string[];\n\t/** Score thresholds. Defaults to {@link DEFAULT_THREAT_POLICY}. */\n\treadonly policy?: ThreatPolicy;\n}\n\n/** Outcome of a scan. */\nexport interface ThreatScanResult {\n\t/**\n\t * The {@link EventContext} the caller supplied, echoed verbatim.\n\t *\n\t * Present only when one was passed. The scanner neither resolves nor validates it.\n\t */\n\treadonly event?: EventContext;\n\t/** `true` when nothing fired at all — strictly stronger than `verdict === \"allow\"`. */\n\treadonly isSafe: boolean;\n\t/** Anomaly score. See {@link calculateThreatScore}. */\n\treadonly score: number;\n\t/** Policy band derived from {@link ThreatScanResult.score}. */\n\treadonly verdict: ThreatVerdict;\n\t/** Every finding, in catalog order then variant order. */\n\treadonly findings: readonly ThreatFinding[];\n\t/** Distinct weakness categories present, in first-seen order. */\n\treadonly types: readonly ThreatType[];\n\t/** `true` when the input exceeded `maxLength` and the tail went unscanned. */\n\treadonly truncated: boolean;\n}\n\n/** Result returned for empty or non-string input. */\nconst SAFE_RESULT: ThreatScanResult = {\n\tisSafe: true,\n\tscore: 0,\n\tverdict: \"allow\",\n\tfindings: [],\n\ttypes: [],\n\ttruncated: false,\n};\n\n//#endregion\n\n//#region Scanning\n\n/**\n * Structural check that is cheaper and more informative computed than matched.\n *\n * A backreference like `/(.)\\1{200,}/` expresses \"long repeated run\" but costs far\n * more than the single linear pass below, and the pass can report the actual run\n * length in the finding.\n *\n * @param value - Bounded input.\n * @returns At most one finding, for the first over-threshold run.\n */\nfunction detectResourceAbuse(value: string): ThreatFinding[] {\n\tlet runStart = 0;\n\n\tfor (let i = 1; i <= value.length; i++) {\n\t\tif (i < value.length && value[i] === value[runStart]) continue;\n\n\t\tconst runLength = i - runStart;\n\t\tif (runLength >= REPETITION_THRESHOLD) {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\truleId: \"RESOURCE-REPETITION-001\",\n\t\t\t\t\ttype: \"resource_abuse\",\n\t\t\t\t\tseverity: \"medium\",\n\t\t\t\t\tconfidence: \"medium\",\n\t\t\t\t\tdescription: `Single character repeated ${runLength} times`,\n\t\t\t\t\tcwe: 1333,\n\t\t\t\t\tprimaryControl:\n\t\t\t\t\t\t\"Bound input length at the boundary and use linear-time matching (RE2) for untrusted patterns\",\n\t\t\t\t\tvariant: \"raw\",\n\t\t\t\t\tmatchedPattern: value.slice(runStart, runStart + MAX_MATCH_EXCERPT),\n\t\t\t\t\tstart: runStart,\n\t\t\t\t\tend: i,\n\t\t\t\t},\n\t\t\t];\n\t\t}\n\t\trunStart = i;\n\t}\n\n\treturn [];\n}\n\n/**\n * Scan a value against the rules for its declared sinks.\n *\n * Every applicable rule is evaluated against every applicable representation, so the\n * finding list may contain one rule more than once with different `variant` values.\n * Scoring collapses those — see {@link calculateThreatScore} — so an encoded payload\n * is not penalized twice merely for being encoded.\n *\n * Non-string input (`null`, `undefined`, a number) is reported safe rather than\n * throwing: this is a detector, and rejecting the wrong *type* belongs to the\n * caller's schema layer.\n *\n * @param input - The candidate value.\n * @param options - Contexts and tuning. See {@link ThreatScanOptions}.\n * @returns A {@link ThreatScanResult}. Never throws.\n *\n * @example Scoping to the actual sink\n * ```ts\n * const bio = \"I maintain build scripts under C:\\\\Windows\\\\System32\";\n *\n * scanForThreats(bio, { contexts: [\"general_text\"] }).verdict; // \"allow\"\n * scanForThreats(bio, { contexts: [\"filesystem\"] }).verdict; // \"review\"\n * ```\n */\nexport function scanForThreats(input: string, options: ThreatScanOptions = {}): ThreatScanResult {\n\tif (typeof input !== \"string\" || input.length === 0) {\n\t\treturn SAFE_RESULT;\n\t}\n\n\tconst {\n\t\tcontexts = [\"general_text\"],\n\t\tmaxLength = MAX_SCAN_LENGTH,\n\t\tscanVariants = true,\n\t\tminSeverity = \"low\",\n\t\texcludeRuleIds,\n\t\tpolicy = DEFAULT_THREAT_POLICY,\n\t} = options;\n\n\tconst truncated = input.length > maxLength;\n\tconst bounded = truncated ? input.slice(0, maxLength) : input;\n\n\tconst excluded = excludeRuleIds && excludeRuleIds.length > 0 ? new Set(excludeRuleIds) : null;\n\tconst severityFloor = SEVERITY_ORDER[minSeverity];\n\n\tconst rules = getRulesForContexts(contexts);\n\tconst rawOnly: readonly InputVariant[] = [{ kind: \"raw\", value: bounded }];\n\tconst variants = scanVariants ? buildInputVariants(bounded) : rawOnly;\n\n\tconst findings: ThreatFinding[] = [];\n\n\tfor (const rule of rules) {\n\t\tif (excluded?.has(rule.id)) continue;\n\t\tif (SEVERITY_ORDER[rule.severity] < severityFloor) continue;\n\n\t\tfor (const variant of variants) {\n\t\t\tif (rule.variants && !rule.variants.includes(variant.kind)) continue;\n\n\t\t\t// Patterns are validated non-global at catalog load, so `exec` carries no\n\t\t\t// lastIndex state between calls and always matches from position 0.\n\t\t\tconst match = rule.pattern.exec(variant.value);\n\t\t\tif (match === null) continue;\n\n\t\t\tfindings.push({\n\t\t\t\truleId: rule.id,\n\t\t\t\ttype: rule.type,\n\t\t\t\tseverity: rule.severity,\n\t\t\t\tconfidence: rule.confidence,\n\t\t\t\tdescription: rule.description,\n\t\t\t\t...(rule.cwe === undefined ? {} : { cwe: rule.cwe }),\n\t\t\t\tprimaryControl: rule.primaryControl,\n\t\t\t\tvariant: variant.kind,\n\t\t\t\tmatchedPattern: match[0].slice(0, MAX_MATCH_EXCERPT),\n\t\t\t\tstart: match.index,\n\t\t\t\tend: match.index + match[0].length,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (SEVERITY_ORDER.medium >= severityFloor && !excluded?.has(\"RESOURCE-REPETITION-001\")) {\n\t\tfindings.push(...detectResourceAbuse(bounded));\n\t}\n\n\tconst score = calculateThreatScore(findings);\n\n\tconst types: ThreatType[] = [];\n\tfor (const finding of findings) {\n\t\tif (!types.includes(finding.type)) types.push(finding.type);\n\t}\n\n\treturn {\n\t\tisSafe: findings.length === 0,\n\t\tscore,\n\t\tverdict: verdictForScore(score, policy),\n\t\tfindings,\n\t\ttypes,\n\t\ttruncated,\n\t\t...(options.event === undefined ? {} : { event: options.event }),\n\t};\n}\n\n//#endregion\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,kBAAkB;;AAG/B,MAAM,oBAAoB;;;;;AAM1B,MAAM,uBAAuB;;AAiE7B,MAAM,cAAgC;CACrC,QAAQ;CACR,OAAO;CACP,SAAS;CACT,UAAU,CAAC;CACX,OAAO,CAAC;CACR,WAAW;AACZ;;;;;;;;;;;AAgBA,SAAS,oBAAoB,OAAgC;CAC5D,IAAI,WAAW;CAEf,KAAK,IAAI,IAAI,GAAG,KAAK,MAAM,QAAQ,KAAK;EACvC,IAAI,IAAI,MAAM,UAAU,MAAM,OAAO,MAAM,WAAW;EAEtD,MAAM,YAAY,IAAI;EACtB,IAAI,aAAa,sBAChB,OAAO,CACN;GACC,QAAQ;GACR,MAAM;GACN,UAAU;GACV,YAAY;GACZ,aAAa,6BAA6B,UAAU;GACpD,KAAK;GACL,gBACC;GACD,SAAS;GACT,gBAAgB,MAAM,MAAM,UAAU,WAAW,iBAAiB;GAClE,OAAO;GACP,KAAK;EACN,CACD;EAED,WAAW;CACZ;CAEA,OAAO,CAAC;AACT;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,eAAe,OAAe,UAA6B,CAAC,GAAqB;CAChG,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAGR,MAAM,EACL,WAAW,CAAC,cAAc,GAC1B,YAAY,iBACZ,eAAe,MACf,cAAc,OACd,gBACA,SAAS,0BACN;CAEJ,MAAM,YAAY,MAAM,SAAS;CACjC,MAAM,UAAU,YAAY,MAAM,MAAM,GAAG,SAAS,IAAI;CAExD,MAAM,WAAW,kBAAkB,eAAe,SAAS,IAAI,IAAI,IAAI,cAAc,IAAI;CACzF,MAAM,gBAAgB,eAAe;CAErC,MAAM,QAAQ,oBAAoB,QAAQ;CAE1C,MAAM,WAAW,eAAe,mBAAmB,OAAO,IAAI,CADpB;EAAE,MAAM;EAAO,OAAO;CAAQ,CACJ;CAEpE,MAAM,WAA4B,CAAC;CAEnC,KAAK,MAAM,QAAQ,OAAO;EACzB,IAAI,UAAU,IAAI,KAAK,EAAE,GAAG;EAC5B,IAAI,eAAe,KAAK,YAAY,eAAe;EAEnD,KAAK,MAAM,WAAW,UAAU;GAC/B,IAAI,KAAK,YAAY,CAAC,KAAK,SAAS,SAAS,QAAQ,IAAI,GAAG;GAI5D,MAAM,QAAQ,KAAK,QAAQ,KAAK,QAAQ,KAAK;GAC7C,IAAI,UAAU,MAAM;GAEpB,SAAS,KAAK;IACb,QAAQ,KAAK;IACb,MAAM,KAAK;IACX,UAAU,KAAK;IACf,YAAY,KAAK;IACjB,aAAa,KAAK;IAClB,GAAI,KAAK,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI;IAClD,gBAAgB,KAAK;IACrB,SAAS,QAAQ;IACjB,gBAAgB,MAAM,EAAE,CAAC,MAAM,GAAG,iBAAiB;IACnD,OAAO,MAAM;IACb,KAAK,MAAM,QAAQ,MAAM,EAAE,CAAC;GAC7B,CAAC;EACF;CACD;CAEA,IAAI,eAAe,UAAU,iBAAiB,CAAC,UAAU,IAAI,yBAAyB,GACrF,SAAS,KAAK,GAAG,oBAAoB,OAAO,CAAC;CAG9C,MAAM,QAAQ,qBAAqB,QAAQ;CAE3C,MAAM,QAAsB,CAAC;CAC7B,KAAK,MAAM,WAAW,UACrB,IAAI,CAAC,MAAM,SAAS,QAAQ,IAAI,GAAG,MAAM,KAAK,QAAQ,IAAI;CAG3D,OAAO;EACN,QAAQ,SAAS,WAAW;EAC5B;EACA,SAAS,gBAAgB,OAAO,MAAM;EACtC;EACA;EACA;EACA,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;CAC/D;AACD"}
|
|
1
|
+
{"version":3,"file":"engine.mjs","names":[],"sources":["../../src/threats/engine.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n * SPDX-License-Identifier: Apache-2.0\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview The scan engine: evaluate the rules applicable to a declared set of\n * sinks against a bounded set of input representations, then score the result.\n *\n * The engine's defining constraint is that **the caller must say where the value is\n * going**. Running every detector against every field is what makes signature-based\n * validation unusable in practice — a biography containing `C:\\Windows`, a support\n * ticket containing `1=1`, and a programming question containing `eval(` are all\n * ordinary text, and only become evidence when the value actually reaches a\n * filesystem, SQL, or HTML sink. Declaring the context is the difference between a\n * detector that catches attacks and one that rejects customers.\n *\n * @module @resq-systems/security/threats/engine\n */\n\nimport { getRulesForContexts } from \"./rules/index.js\";\nimport { calculateThreatScore, verdictForScore } from \"./scoring.js\";\nimport {\n\tDEFAULT_THREAT_POLICY,\n\ttype EventContext,\n\ttype InputVariant,\n\tSEVERITY_ORDER,\n\ttype ThreatContext,\n\ttype ThreatFinding,\n\ttype ThreatPolicy,\n\ttype ThreatSeverity,\n\ttype ThreatType,\n\ttype ThreatVerdict,\n} from \"./types.js\";\nimport { buildInputVariants } from \"./variants.js\";\n\n//#region Constants\n\n/**\n * Characters scanned before truncation.\n *\n * Bounds worst-case regex cost per call. Individual rules are bounded too — no\n * unbounded nested quantifiers — so this is a second line of defense rather than the\n * only one, but a 50 MB paste should not become a 50 MB × 89-rule scan regardless.\n */\nexport const MAX_SCAN_LENGTH = 100_000;\n\n/** Matched substrings are truncated to this many characters before entering a finding. */\nconst MAX_MATCH_EXCERPT = 50;\n\n/**\n * Run length at which a repeated single character is reported as resource abuse.\n * Set well above anything that occurs in prose or formatted text.\n */\nconst REPETITION_THRESHOLD = 200;\n\n//#endregion\n\n//#region Options and result\n\n/** Controls what {@link scanForThreats} evaluates and how it grades the outcome. */\nexport interface ThreatScanOptions {\n\t/**\n\t * Correlation metadata, echoed onto the result untouched.\n\t *\n\t * Affects nothing about detection. See {@link EventContext} for why it exists.\n\t */\n\treadonly event?: EventContext;\n\t/**\n\t * Sinks the value is destined for.\n\t *\n\t * Defaults to `[\"general_text\"]`, which enables only the universal rules — bidi\n\t * overrides, invisible characters, control characters. Any caller that wants\n\t * injection detection must name the sink it is protecting; that requirement is\n\t * the package's primary false-positive control, not an inconvenience.\n\t */\n\treadonly contexts?: readonly ThreatContext[];\n\t/** Truncation bound. Defaults to {@link MAX_SCAN_LENGTH}. */\n\treadonly maxLength?: number;\n\t/**\n\t * Scan encoded representations in addition to the raw string. Defaults to `true`.\n\t * Pass `false` when the caller has already decoded the value and re-decoding\n\t * would misrepresent it.\n\t */\n\treadonly scanVariants?: boolean;\n\t/** Drop findings below this severity before scoring. Defaults to `\"low\"`. */\n\treadonly minSeverity?: ThreatSeverity;\n\t/**\n\t * Rule IDs to skip. The tuning knob to reach for first: silencing one noisy rule\n\t * on one route beats disabling a whole category.\n\t */\n\treadonly excludeRuleIds?: readonly string[];\n\t/** Score thresholds. Defaults to {@link DEFAULT_THREAT_POLICY}. */\n\treadonly policy?: ThreatPolicy;\n}\n\n/** Outcome of a scan. */\nexport interface ThreatScanResult {\n\t/**\n\t * The {@link EventContext} the caller supplied, echoed verbatim.\n\t *\n\t * Present only when one was passed. The scanner neither resolves nor validates it.\n\t */\n\treadonly event?: EventContext;\n\t/** `true` when nothing fired at all — strictly stronger than `verdict === \"allow\"`. */\n\treadonly isSafe: boolean;\n\t/** Anomaly score. See {@link calculateThreatScore}. */\n\treadonly score: number;\n\t/** Policy band derived from {@link ThreatScanResult.score}. */\n\treadonly verdict: ThreatVerdict;\n\t/** Every finding, in catalog order then variant order. */\n\treadonly findings: readonly ThreatFinding[];\n\t/** Distinct weakness categories present, in first-seen order. */\n\treadonly types: readonly ThreatType[];\n\t/** `true` when the input exceeded `maxLength` and the tail went unscanned. */\n\treadonly truncated: boolean;\n}\n\n/** Result returned for empty or non-string input. */\nconst SAFE_RESULT: ThreatScanResult = {\n\tisSafe: true,\n\tscore: 0,\n\tverdict: \"allow\",\n\tfindings: [],\n\ttypes: [],\n\ttruncated: false,\n};\n\n//#endregion\n\n//#region Scanning\n\n/**\n * Structural check that is cheaper and more informative computed than matched.\n *\n * A backreference like `/(.)\\1{200,}/` expresses \"long repeated run\" but costs far\n * more than the single linear pass below, and the pass can report the actual run\n * length in the finding.\n *\n * @param value - Bounded input.\n * @returns At most one finding, for the first over-threshold run.\n */\nfunction detectResourceAbuse(value: string): ThreatFinding[] {\n\tlet runStart = 0;\n\n\tfor (let i = 1; i <= value.length; i++) {\n\t\tif (i < value.length && value[i] === value[runStart]) continue;\n\n\t\tconst runLength = i - runStart;\n\t\tif (runLength >= REPETITION_THRESHOLD) {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\truleId: \"RESOURCE-REPETITION-001\",\n\t\t\t\t\ttype: \"resource_abuse\",\n\t\t\t\t\tseverity: \"medium\",\n\t\t\t\t\tconfidence: \"medium\",\n\t\t\t\t\tdescription: `Single character repeated ${runLength} times`,\n\t\t\t\t\tcwe: 1333,\n\t\t\t\t\tprimaryControl:\n\t\t\t\t\t\t\"Bound input length at the boundary and use linear-time matching (RE2) for untrusted patterns\",\n\t\t\t\t\tvariant: \"raw\",\n\t\t\t\t\tmatchedPattern: value.slice(runStart, runStart + MAX_MATCH_EXCERPT),\n\t\t\t\t\tstart: runStart,\n\t\t\t\t\tend: i,\n\t\t\t\t},\n\t\t\t];\n\t\t}\n\t\trunStart = i;\n\t}\n\n\treturn [];\n}\n\n/**\n * Scan a value against the rules for its declared sinks.\n *\n * Every applicable rule is evaluated against every applicable representation, so the\n * finding list may contain one rule more than once with different `variant` values.\n * Scoring collapses those — see {@link calculateThreatScore} — so an encoded payload\n * is not penalized twice merely for being encoded.\n *\n * Non-string input (`null`, `undefined`, a number) is reported safe rather than\n * throwing: this is a detector, and rejecting the wrong *type* belongs to the\n * caller's schema layer.\n *\n * @param input - The candidate value.\n * @param options - Contexts and tuning. See {@link ThreatScanOptions}.\n * @returns A {@link ThreatScanResult}. Never throws.\n *\n * @example Scoping to the actual sink\n * ```ts\n * const bio = \"I maintain build scripts under C:\\\\Windows\\\\System32\";\n *\n * scanForThreats(bio, { contexts: [\"general_text\"] }).verdict; // \"allow\"\n * scanForThreats(bio, { contexts: [\"filesystem\"] }).verdict; // \"review\"\n * ```\n */\nexport function scanForThreats(input: string, options: ThreatScanOptions = {}): ThreatScanResult {\n\tif (typeof input !== \"string\" || input.length === 0) {\n\t\treturn SAFE_RESULT;\n\t}\n\n\tconst {\n\t\tcontexts = [\"general_text\"],\n\t\tmaxLength = MAX_SCAN_LENGTH,\n\t\tscanVariants = true,\n\t\tminSeverity = \"low\",\n\t\texcludeRuleIds,\n\t\tpolicy = DEFAULT_THREAT_POLICY,\n\t} = options;\n\n\tconst truncated = input.length > maxLength;\n\tconst bounded = truncated ? input.slice(0, maxLength) : input;\n\n\tconst excluded = excludeRuleIds && excludeRuleIds.length > 0 ? new Set(excludeRuleIds) : null;\n\tconst severityFloor = SEVERITY_ORDER[minSeverity];\n\n\tconst rules = getRulesForContexts(contexts);\n\tconst rawOnly: readonly InputVariant[] = [{ kind: \"raw\", value: bounded }];\n\tconst variants = scanVariants ? buildInputVariants(bounded) : rawOnly;\n\n\tconst findings: ThreatFinding[] = [];\n\n\tfor (const rule of rules) {\n\t\tif (excluded?.has(rule.id)) continue;\n\t\tif (SEVERITY_ORDER[rule.severity] < severityFloor) continue;\n\n\t\tfor (const variant of variants) {\n\t\t\tif (rule.variants && !rule.variants.includes(variant.kind)) continue;\n\n\t\t\t// Patterns are validated non-global at catalog load, so `exec` carries no\n\t\t\t// lastIndex state between calls and always matches from position 0.\n\t\t\tconst match = rule.pattern.exec(variant.value);\n\t\t\tif (match === null) continue;\n\n\t\t\tfindings.push({\n\t\t\t\truleId: rule.id,\n\t\t\t\ttype: rule.type,\n\t\t\t\tseverity: rule.severity,\n\t\t\t\tconfidence: rule.confidence,\n\t\t\t\tdescription: rule.description,\n\t\t\t\t...(rule.cwe === undefined ? {} : { cwe: rule.cwe }),\n\t\t\t\tprimaryControl: rule.primaryControl,\n\t\t\t\tvariant: variant.kind,\n\t\t\t\tmatchedPattern: match[0].slice(0, MAX_MATCH_EXCERPT),\n\t\t\t\tstart: match.index,\n\t\t\t\tend: match.index + match[0].length,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (SEVERITY_ORDER.medium >= severityFloor && !excluded?.has(\"RESOURCE-REPETITION-001\")) {\n\t\tfindings.push(...detectResourceAbuse(bounded));\n\t}\n\n\tconst score = calculateThreatScore(findings);\n\n\tconst types: ThreatType[] = [];\n\tfor (const finding of findings) {\n\t\tif (!types.includes(finding.type)) types.push(finding.type);\n\t}\n\n\treturn {\n\t\tisSafe: findings.length === 0,\n\t\tscore,\n\t\tverdict: verdictForScore(score, policy),\n\t\tfindings,\n\t\ttypes,\n\t\ttruncated,\n\t\t...(options.event === undefined ? {} : { event: options.event }),\n\t};\n}\n\n//#endregion\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,MAAa,kBAAkB;;AAG/B,MAAM,oBAAoB;;;;;AAM1B,MAAM,uBAAuB;;AAiE7B,MAAM,cAAgC;CACrC,QAAQ;CACR,OAAO;CACP,SAAS;CACT,UAAU,CAAC;CACX,OAAO,CAAC;CACR,WAAW;AACZ;;;;;;;;;;;AAgBA,SAAS,oBAAoB,OAAgC;CAC5D,IAAI,WAAW;CAEf,KAAK,IAAI,IAAI,GAAG,KAAK,MAAM,QAAQ,KAAK;EACvC,IAAI,IAAI,MAAM,UAAU,MAAM,OAAO,MAAM,WAAW;EAEtD,MAAM,YAAY,IAAI;EACtB,IAAI,aAAa,sBAChB,OAAO,CACN;GACC,QAAQ;GACR,MAAM;GACN,UAAU;GACV,YAAY;GACZ,aAAa,6BAA6B,UAAU;GACpD,KAAK;GACL,gBACC;GACD,SAAS;GACT,gBAAgB,MAAM,MAAM,UAAU,WAAW,iBAAiB;GAClE,OAAO;GACP,KAAK;EACN,CACD;EAED,WAAW;CACZ;CAEA,OAAO,CAAC;AACT;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,eAAe,OAAe,UAA6B,CAAC,GAAqB;CAChG,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAGR,MAAM,EACL,WAAW,CAAC,cAAc,GAC1B,YAAY,iBACZ,eAAe,MACf,cAAc,OACd,gBACA,SAAS,0BACN;CAEJ,MAAM,YAAY,MAAM,SAAS;CACjC,MAAM,UAAU,YAAY,MAAM,MAAM,GAAG,SAAS,IAAI;CAExD,MAAM,WAAW,kBAAkB,eAAe,SAAS,IAAI,IAAI,IAAI,cAAc,IAAI;CACzF,MAAM,gBAAgB,eAAe;CAErC,MAAM,QAAQ,oBAAoB,QAAQ;CAE1C,MAAM,WAAW,eAAe,mBAAmB,OAAO,IAAI,CADpB;EAAE,MAAM;EAAO,OAAO;CAAQ,CACJ;CAEpE,MAAM,WAA4B,CAAC;CAEnC,KAAK,MAAM,QAAQ,OAAO;EACzB,IAAI,UAAU,IAAI,KAAK,EAAE,GAAG;EAC5B,IAAI,eAAe,KAAK,YAAY,eAAe;EAEnD,KAAK,MAAM,WAAW,UAAU;GAC/B,IAAI,KAAK,YAAY,CAAC,KAAK,SAAS,SAAS,QAAQ,IAAI,GAAG;GAI5D,MAAM,QAAQ,KAAK,QAAQ,KAAK,QAAQ,KAAK;GAC7C,IAAI,UAAU,MAAM;GAEpB,SAAS,KAAK;IACb,QAAQ,KAAK;IACb,MAAM,KAAK;IACX,UAAU,KAAK;IACf,YAAY,KAAK;IACjB,aAAa,KAAK;IAClB,GAAI,KAAK,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI;IAClD,gBAAgB,KAAK;IACrB,SAAS,QAAQ;IACjB,gBAAgB,MAAM,EAAE,CAAC,MAAM,GAAG,iBAAiB;IACnD,OAAO,MAAM;IACb,KAAK,MAAM,QAAQ,MAAM,EAAE,CAAC;GAC7B,CAAC;EACF;CACD;CAEA,IAAI,eAAe,UAAU,iBAAiB,CAAC,UAAU,IAAI,yBAAyB,GACrF,SAAS,KAAK,GAAG,oBAAoB,OAAO,CAAC;CAG9C,MAAM,QAAQ,qBAAqB,QAAQ;CAE3C,MAAM,QAAsB,CAAC;CAC7B,KAAK,MAAM,WAAW,UACrB,IAAI,CAAC,MAAM,SAAS,QAAQ,IAAI,GAAG,MAAM,KAAK,QAAQ,IAAI;CAG3D,OAAO;EACN,QAAQ,SAAS,WAAW;EAC5B;EACA,SAAS,gBAAgB,OAAO,MAAM;EACtC;EACA;EACA;EACA,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;CAC/D;AACD"}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { ThreatRule } from "../types.mjs";
|
|
2
2
|
//#region src/threats/rules/datastore.d.ts
|
|
3
3
|
/** SQL-injection signatures. Scoped to the `sql` context. */
|
|
4
|
-
declare const SQL_INJECTION_RULES: readonly ThreatRule[];
|
|
4
|
+
export declare const SQL_INJECTION_RULES: readonly ThreatRule[];
|
|
5
5
|
/** Document-store operator injection (MongoDB and compatible drivers). */
|
|
6
|
-
declare const NOSQL_INJECTION_RULES: readonly ThreatRule[];
|
|
6
|
+
export declare const NOSQL_INJECTION_RULES: readonly ThreatRule[];
|
|
7
7
|
/** LDAP filter and DN injection. */
|
|
8
|
-
declare const LDAP_INJECTION_RULES: readonly ThreatRule[];
|
|
8
|
+
export declare const LDAP_INJECTION_RULES: readonly ThreatRule[];
|
|
9
9
|
/** XPath/XQuery injection. */
|
|
10
|
-
declare const XPATH_INJECTION_RULES: readonly ThreatRule[];
|
|
10
|
+
export declare const XPATH_INJECTION_RULES: readonly ThreatRule[];
|
|
11
11
|
//#endregion
|
|
12
|
-
export { LDAP_INJECTION_RULES, NOSQL_INJECTION_RULES, SQL_INJECTION_RULES, XPATH_INJECTION_RULES };
|
|
13
12
|
//# sourceMappingURL=datastore.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"datastore.d.mts","names":[],"sources":["../../../src/threats/rules/datastore.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"datastore.d.mts","names":[],"sources":["../../../src/threats/rules/datastore.ts"],"mappings":";;;qBA6Ca,8BAA8B;;qBA+O9B,gCAAgC;;qBAyDhC,+BAA+B;;qBA6E/B,gCAAgC"}
|