@resq-systems/security 2.0.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.
Files changed (90) hide show
  1. package/README.md +1 -1
  2. package/lib/controls/address.d.mts +8 -8
  3. package/lib/controls/address.d.mts.map +1 -1
  4. package/lib/controls/address.mjs.map +1 -1
  5. package/lib/controls/csrf.d.mts +7 -7
  6. package/lib/controls/csrf.d.mts.map +1 -1
  7. package/lib/controls/csrf.mjs +5 -2
  8. package/lib/controls/csrf.mjs.map +1 -1
  9. package/lib/controls/origin.d.mts +6 -6
  10. package/lib/controls/origin.d.mts.map +1 -1
  11. package/lib/controls/origin.mjs +1 -0
  12. package/lib/controls/origin.mjs.map +1 -1
  13. package/lib/controls/payload.d.mts +4 -4
  14. package/lib/controls/payload.d.mts.map +1 -1
  15. package/lib/controls/payload.mjs.map +1 -1
  16. package/lib/controls/query.d.mts +22 -10
  17. package/lib/controls/query.d.mts.map +1 -1
  18. package/lib/controls/query.mjs +42 -23
  19. package/lib/controls/query.mjs.map +1 -1
  20. package/lib/controls/redirect.d.mts +5 -5
  21. package/lib/controls/redirect.d.mts.map +1 -1
  22. package/lib/controls/redirect.mjs.map +1 -1
  23. package/lib/controls/upload.d.mts +7 -7
  24. package/lib/controls/upload.d.mts.map +1 -1
  25. package/lib/controls/upload.mjs.map +1 -1
  26. package/lib/crypto.d.mts +20 -21
  27. package/lib/crypto.d.mts.map +1 -1
  28. package/lib/crypto.mjs +3 -1
  29. package/lib/crypto.mjs.map +1 -1
  30. package/lib/hash.d.mts +5 -5
  31. package/lib/hash.d.mts.map +1 -1
  32. package/lib/hash.mjs +1 -0
  33. package/lib/hash.mjs.map +1 -1
  34. package/lib/paths.d.mts +5 -5
  35. package/lib/paths.d.mts.map +1 -1
  36. package/lib/paths.mjs +1 -0
  37. package/lib/paths.mjs.map +1 -1
  38. package/lib/sanitize.d.mts +36 -37
  39. package/lib/sanitize.d.mts.map +1 -1
  40. package/lib/sanitize.mjs.map +1 -1
  41. package/lib/threats/capec.generated.d.mts +4 -4
  42. package/lib/threats/capec.generated.d.mts.map +1 -1
  43. package/lib/threats/capec.generated.mjs.map +1 -1
  44. package/lib/threats/engine.d.mts +4 -5
  45. package/lib/threats/engine.d.mts.map +1 -1
  46. package/lib/threats/engine.mjs +1 -0
  47. package/lib/threats/engine.mjs.map +1 -1
  48. package/lib/threats/rules/datastore.d.mts +4 -5
  49. package/lib/threats/rules/datastore.d.mts.map +1 -1
  50. package/lib/threats/rules/datastore.mjs.map +1 -1
  51. package/lib/threats/rules/index.d.mts +5 -5
  52. package/lib/threats/rules/index.d.mts.map +1 -1
  53. package/lib/threats/rules/index.mjs.map +1 -1
  54. package/lib/threats/rules/markup.d.mts +4 -5
  55. package/lib/threats/rules/markup.d.mts.map +1 -1
  56. package/lib/threats/rules/markup.mjs +1 -0
  57. package/lib/threats/rules/markup.mjs.map +1 -1
  58. package/lib/threats/rules/protocol.d.mts +4 -5
  59. package/lib/threats/rules/protocol.d.mts.map +1 -1
  60. package/lib/threats/rules/protocol.mjs.map +1 -1
  61. package/lib/threats/rules/system.d.mts +4 -5
  62. package/lib/threats/rules/system.d.mts.map +1 -1
  63. package/lib/threats/rules/system.mjs.map +1 -1
  64. package/lib/threats/rules/web.d.mts +5 -6
  65. package/lib/threats/rules/web.d.mts.map +1 -1
  66. package/lib/threats/rules/web.mjs +1 -1
  67. package/lib/threats/rules/web.mjs.map +1 -1
  68. package/lib/threats/scoring.d.mts +5 -6
  69. package/lib/threats/scoring.d.mts.map +1 -1
  70. package/lib/threats/scoring.mjs +1 -0
  71. package/lib/threats/scoring.mjs.map +1 -1
  72. package/lib/threats/types.d.mts +18 -18
  73. package/lib/threats/types.d.mts.map +1 -1
  74. package/lib/threats/types.mjs.map +1 -1
  75. package/lib/threats/variants.d.mts +3 -4
  76. package/lib/threats/variants.d.mts.map +1 -1
  77. package/lib/threats/variants.mjs.map +1 -1
  78. package/lib/unicode/confusables.d.mts +5 -5
  79. package/lib/unicode/confusables.d.mts.map +1 -1
  80. package/lib/unicode/confusables.mjs +1 -0
  81. package/lib/unicode/confusables.mjs.map +1 -1
  82. package/lib/unicode/index.d.mts +11 -11
  83. package/lib/unicode/index.d.mts.map +1 -1
  84. package/lib/unicode/index.mjs +1 -0
  85. package/lib/unicode/index.mjs.map +1 -1
  86. package/lib/validators.d.mts +89 -39
  87. package/lib/validators.d.mts.map +1 -1
  88. package/lib/validators.mjs +285 -21
  89. package/lib/validators.mjs.map +1 -1
  90. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"system.mjs","names":[],"sources":["../../../src/threats/rules/system.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 Host-facing signatures: OS command injection, path traversal, file\n * inclusion, and server-side request forgery.\n *\n * Two of these categories are only weakly served by signatures, and the rules say so\n * in their `primaryControl`:\n *\n * - **Path traversal** is prevented by canonicalizing the candidate path and\n * verifying it stays beneath the allowed base directory — see `resolveContainedPath`\n * in `@resq-systems/security/paths`. Detection catches `../`; it does not catch a\n * symlink, nor an absolute path that happens to resolve inside the base.\n * - **SSRF** is prevented by parsing the URL, resolving the host, rejecting private\n * and link-local ranges, re-validating after every redirect, and constraining\n * network egress. A regex over the URL string catches a literal `127.0.0.1` but\n * never a hostname whose DNS record points there.\n *\n * @module @resq-systems/security/threats/rules/system\n */\n\nimport type { ThreatRule } from \"../types.js\";\n\n//#region Command injection\n\n/** Repeated across every command-injection rule. */\nconst CMD_CONTROL =\n\t\"Spawn with an argv array and shell:false; never build a shell string from input\";\n\n/** OS command injection. Scoped to the `shell` context — never runs elsewhere. */\nexport const COMMAND_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"CMD-SUBSTITUTION-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Shell command substitution $(...)\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\t// `(` excluded alongside `)` so a run of `$(` cannot make the engine consume\n\t\t// 200 characters and backtrack at every position. A nested `$(a $(b))` still\n\t\t// matches on the inner substitution.\n\t\tpattern: /\\$\\([^()]{1,200}\\)/,\n\t},\n\t{\n\t\tid: \"CMD-BACKTICK-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Backtick command substitution\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /`[^`]{1,200}`/,\n\t},\n\t{\n\t\tid: \"CMD-CHAIN-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Command separator followed by a state-changing or network command\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern:\n\t\t\t/[;&|]{1,2}\\s{0,8}(?:rm|del|cat|type|wget|curl|nc|ncat|bash|sh|zsh|powershell|pwsh|python[23]?|perl|ruby|chmod|chown|kill|shutdown|reboot|mkfifo|base64|openssl)\\b/i,\n\t},\n\t{\n\t\tid: \"CMD-PIPE-SHELL-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Output piped into a shell interpreter\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /\\|\\s{0,8}(?:sh|bash|zsh|dash|cmd|powershell|pwsh)\\b/i,\n\t},\n\t{\n\t\tid: \"CMD-REDIRECT-SYSTEM-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Redirection into a system directory\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: />\\s{0,8}\\/(?:etc|dev|proc|sys|boot)\\//,\n\t},\n\t{\n\t\t// Commix documents both as filter bypasses against defences that block spaces:\n\t\t// each expands to a word separator, so a command survives a filter that only\n\t\t// looks for whitespace between tokens.\n\t\tid: \"CMD-WORD-SPLIT-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Shell word-splitting construct standing in for a space\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /\\$\\{IFS\\}|\\{\\s{0,4},\\s{0,4}\\}/,\n\t},\n\t{\n\t\t// The shell removes an empty quote pair before execution, so a command name split\n\t\t// by one still runs while defeating a literal match on that name.\n\t\t//\n\t\t// Deliberately narrower than Commix's full set: the equivalent backslash form is\n\t\t// NOT matched, because it is indistinguishable from an ordinary Windows-style\n\t\t// relative path, and a rule firing on those would be disabled by the first person\n\t\t// it inconvenienced.\n\t\tid: \"CMD-QUOTE-SPLIT-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Empty quote pair inside a word, removed by the shell before execution\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /[a-z](?:\"\"|'')[a-z]/i,\n\t},\n\t{\n\t\tid: \"CMD-ENV-EXPANSION-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Shell parameter expansion\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\t// `{` excluded alongside `}` for the same backtracking reason as above.\n\t\tpattern: /\\$\\{[^{}]{1,200}\\}/,\n\t},\n\t{\n\t\tid: \"CMD-ARG-NEWLINE-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line break in a value destined for a command line\",\n\t\tcwe: 88,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /[\\r\\n]/,\n\t},\n];\n\n//#endregion\n\n//#region Path traversal\n\n/** Repeated across every path-traversal rule. */\nconst PATH_CONTROL =\n\t\"Resolve the candidate against the base directory and verify containment (resolveContainedPath); resolve symlinks before use\";\n\n/** Directory-traversal and sensitive-path signatures. Scoped to `filesystem`. */\nexport const PATH_TRAVERSAL_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PATH-TRAVERSAL-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Parent-directory segment\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// Anchored to segment boundaries so `my..file/` and `v1..v2` do not fire.\n\t\tpattern: /(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Percent-encoded parent-directory traversal\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// The previous catalog wrote `/%2e%2e[%2f%5c]/`, whose character class matches\n\t\t// a single character from {%, 2, f, 5, c} — so it fired on `%2e%2e%` and on\n\t\t// `%2e%2e2`, and expressed nothing about the intended separator alternation.\n\t\tpattern: /%2e%2e(?:%2f|%5c|\\/|\\\\)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-MIXED-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Literal dots with a percent-encoded separator\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /\\.\\.(?:%2f|%5c)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-DOUBLE-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Double percent-encoded parent-directory traversal\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /%252e%252e(?:%252f|%255c)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-OVERLONG-UTF8-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Overlong UTF-8 encoding of a path separator\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /%c0%(?:af|2f)|%e0%80%af|%c1%9c/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-NULL-BYTE-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"NUL byte, used to truncate a path before its extension check\",\n\t\tcwe: 158,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: matching the NUL byte is this rule's entire purpose\n\t\tpattern: /%00|\\u0000/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-FILTER-STRIP-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Dot run that survives a naive ../ stripping filter\",\n\t\tcwe: 22,\n\t\tprimaryControl:\n\t\t\t\"Resolve the candidate against the base directory and verify containment (resolveContainedPath). Never sanitize by stripping '../' — stripping is precisely what this payload targets: '....//' becomes '../' after one pass.\",\n\t\t// PATH-TRAVERSAL-001 requires exactly two dots between boundaries, so `....//`\n\t\t// and `.../` walked past it. Bounded at 200 because a longer run is already\n\t\t// reported by the engine's repetition check.\n\t\tpattern: /(?:^|[/\\\\])\\.{3,200}[/\\\\]/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-PARAM-BYPASS-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Path parameter segment used to bypass a gateway path ACL\",\n\t\tcwe: 22,\n\t\tprimaryControl:\n\t\t\t\"Ensure the proxy and the origin agree on path normalization before authorization is applied; resolve and verify containment with resolveContainedPath\",\n\t\t// `..;/` — Tomcat and Spring treat `;` as starting a path parameter, so the\n\t\t// segment normalizes to `..` at the origin while the gateway sees a literal name.\n\t\tpattern: /(?:^|[/\\\\])\\.\\.;[^/\\\\]{0,64}[/\\\\]/,\n\t},\n\t{\n\t\tid: \"PATH-SENSITIVE-UNIX-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Reference to a sensitive Unix path\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /\\/(?:etc\\/(?:passwd|shadow|sudoers|hosts)|proc\\/self|root\\/\\.ssh)\\b/i,\n\t},\n\t{\n\t\tid: \"PATH-SENSITIVE-WINDOWS-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Reference to a sensitive Windows path\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// Requires a system subdirectory. The old rule was a bare `/C:\\\\Windows/gi`,\n\t\t// which fired on any documentation that merely mentioned the folder.\n\t\tpattern: /[a-z]:[\\\\/]{1,2}(?:windows|winnt)[\\\\/]{1,2}(?:system32|win\\.ini|repair)/i,\n\t},\n\t{\n\t\tid: \"PATH-ABSOLUTE-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Absolute path where a relative one is expected\",\n\t\tcwe: 36,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /^(?:\\/|[a-z]:[\\\\/]|\\\\\\\\)/i,\n\t},\n];\n\n//#endregion\n\n//#region File inclusion\n\n/** Repeated across every file-inclusion rule. */\nconst LFI_CONTROL =\n\t\"Map the request to a fixed allowlist of loadable resources; never derive a module or template path from input\";\n\n/** Adds the remote-fetch half of the control, for the RFI rules. */\nconst RFI_CONTROL =\n\t\"Map the request to a fixed allowlist of loadable resources; never derive a module, template, or file path from input. Disable remote stream wrappers (PHP allow_url_fopen/allow_url_include) and reject any candidate not relative to the configured base directory\";\n\n/** Local/remote file inclusion via stream wrappers and alternate schemes. */\nexport const FILE_INCLUSION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"LFI-PHP-WRAPPER-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"PHP stream wrapper used to read or execute source\",\n\t\tcwe: 98,\n\t\tprimaryControl: LFI_CONTROL,\n\t\tpattern: /\\bphp:\\/\\/(?:filter|input|memory|temp|stdin)/i,\n\t},\n\t{\n\t\tid: \"LFI-WRAPPER-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Alternate stream wrapper capable of loading remote or archived content\",\n\t\tcwe: 98,\n\t\tprimaryControl: LFI_CONTROL,\n\t\tpattern: /\\b(?:expect|zip|phar|glob|dict|gopher|netdoc|jar|ogg):\\/\\//i,\n\t},\n\t{\n\t\tid: \"RFI-REMOTE-SCHEME-001\",\n\t\ttype: \"file_inclusion\",\n\t\t// `filesystem` only. The SSRF rules already own `url`; this is about a value\n\t\t// that was supposed to be a *path* and instead names a remote resource.\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Remote scheme where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\tpattern:\n\t\t\t/(?:^\\s{0,8}|[/\\\\=])(?:https?|ftps?|smb|cifs|nfs|webdav|ssh2|rar|zlib|compress\\.(?:zlib|bzip2)):\\/\\//i,\n\t},\n\t{\n\t\tid: \"RFI-DATA-URI-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"data: URI where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\t// The class is ordered `[a-z0-9+.-]` deliberately: writing `[a-z0-9.+-]` embeds\n\t\t// the literal substring `.+`, which the ReDoS structural test rejects.\n\t\tpattern:\n\t\t\t/(?:^\\s{0,8}|[/\\\\=])data:(?:\\/\\/)?(?:[a-z][a-z0-9+.-]{0,32}\\/[a-z0-9+.-]{1,32}[a-z0-9;=+.-]{0,32}|;base64|),/i,\n\t},\n\t{\n\t\tid: \"RFI-REMOTE-HOST-PATH-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Protocol-relative or UNC host path where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\t// Re-grades rather than newly detects: PATH-ABSOLUTE-001 already matches these\n\t\t// at low confidence, contributing 1.0 — not enough to leave the allow band.\n\t\tpattern: /^\\s{0,8}(?:\\/\\/|\\\\\\\\)[a-z0-9][a-z0-9-]{0,62}(?:\\.[a-z0-9-]{1,63}){1,4}[/\\\\]/i,\n\t},\n];\n\n//#endregion\n\n//#region SSRF\n\n/** Repeated across every SSRF rule. */\nconst SSRF_CONTROL =\n\t\"Gate the request with assertOutboundUrl, re-validate after every redirect, and constrain egress at the network layer\";\n\n/**\n * Server-side request forgery.\n *\n * These signatures cover literal addresses only. A hostname whose DNS record\n * resolves to `169.254.169.254` passes every rule here — which is exactly why\n * {@link SSRF_CONTROL} names resolution and egress control as the real defense.\n */\nexport const SSRF_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"SSRF-LOOPBACK-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL host is a loopback address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0|\\[?::1\\]?)(?::\\d{1,5})?(?:[/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-PRIVATE-RANGE-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL host is in an RFC 1918 private range\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/\\/\\/(?:10(?:\\.\\d{1,3}){3}|192\\.168(?:\\.\\d{1,3}){2}|172\\.(?:1[6-9]|2\\d|3[01])(?:\\.\\d{1,3}){2})/,\n\t},\n\t{\n\t\tid: \"SSRF-METADATA-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL targets a cloud instance metadata endpoint\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:169\\.254(?:\\.\\d{1,3}){2}|metadata\\.google\\.internal|metadata\\.goog)\\b/i,\n\t},\n\t{\n\t\tid: \"SSRF-ALT-IP-ENCODING-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Host written as a decimal, octal, or hex integer to evade address filters\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:0x[0-9a-f]{6,8}|0\\d{6,11}|\\d{8,10})(?:[:/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-USERINFO-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Userinfo segment, used to disguise the true host\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/[^/@\\s]{1,128}@/,\n\t},\n\t{\n\t\tid: \"SSRF-NON-HTTP-SCHEME-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Non-HTTP scheme in a fetchable URL\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /^\\s{0,8}(?:file|gopher|dict|tftp|ldaps?|jar|netdoc|sftp):/i,\n\t},\n\t{\n\t\tid: \"SSRF-USERINFO-INTERNAL-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Internal address in the host position behind a userinfo segment\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// The textbook bypass. SSRF-LOOPBACK-001 and SSRF-METADATA-001 both anchor on\n\t\t// `//` immediately before the address, so `http://good.com@169.254.169.254/`\n\t\t// matched only the generic userinfo rule and scored 2.0 — an allow verdict on a\n\t\t// cloud-credential read.\n\t\tpattern:\n\t\t\t/@(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0|169\\.254(?:\\.\\d{1,3}){2}|10(?:\\.\\d{1,3}){3}|192\\.168(?:\\.\\d{1,3}){2}|172\\.(?:1[6-9]|2\\d|3[01])(?:\\.\\d{1,3}){2})(?=[:/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-SUFFIXED-LOOPBACK-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Loopback literal used as a hostname prefix\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0)\\.[a-z]/i,\n\t},\n\t{\n\t\tid: \"SSRF-METADATA-ALT-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Alibaba Cloud or Oracle Cloud instance metadata endpoint\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:100\\.100\\.100\\.200|192\\.0\\.0\\.192)(?=[:/?#]|$)/,\n\t},\n\t{\n\t\tid: \"SSRF-IPV6-INTERNAL-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"IPv6 loopback, unique-local, or link-local literal\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/(?:\\/\\/|@)\\[(?:::|(?:0{1,4}:){2,7}0{0,3}[01]|f[cd][0-9a-f]{0,2}:[0-9a-f:]{0,32}|fe[89ab][0-9a-f]?:[0-9a-f:]{0,32})\\]/i,\n\t},\n\t{\n\t\tid: \"SSRF-IPV4-MAPPED-INTERNAL-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"IPv4-mapped IPv6 literal carrying an internal address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/(?:\\/\\/|@)\\[::ffff:(?:127\\.|10\\.|169\\.254\\.|192\\.168\\.|0\\.0\\.0\\.0|172\\.(?:1[6-9]|2\\d|3[01])\\.|7f[0-9a-f]{2}:|a9fe:|c0a8:)/i,\n\t},\n\t{\n\t\tid: \"SSRF-SHORT-IP-FORM-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Abbreviated or octal IPv4 form resolving to a loopback address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// `127.1`, `0`, and `0177.0.0.1` all resolve to loopback through inet_aton.\n\t\tpattern: /\\/\\/(?:127\\.\\d{1,3}(?:\\.\\d{1,3})?|0|0\\d{2,3}(?:\\.0*\\d{1,3}){3})(?=[:/?#]|$)/,\n\t},\n\t{\n\t\tid: \"SSRF-WILDCARD-DNS-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Wildcard-DNS service that resolves an arbitrary address from the hostname\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// Coverage is incomplete by construction — a new wildcard-DNS service needs a\n\t\t// catalog edit. Host resolution plus egress control is what actually holds.\n\t\tpattern:\n\t\t\t/\\/\\/(?:[^/\\s]{0,64}\\.)?(?:nip\\.io|sslip\\.io|localtest\\.me|lvh\\.me|vcap\\.me|xip\\.io|localho\\.st|traefik\\.me)(?=[:/?#]|$)/i,\n\t},\n];\n\n//#endregion\n"],"mappings":"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"}
1
+ {"version":3,"file":"system.mjs","names":[],"sources":["../../../src/threats/rules/system.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 Host-facing signatures: OS command injection, path traversal, file\n * inclusion, and server-side request forgery.\n *\n * Two of these categories are only weakly served by signatures, and the rules say so\n * in their `primaryControl`:\n *\n * - **Path traversal** is prevented by canonicalizing the candidate path and\n * verifying it stays beneath the allowed base directory — see `resolveContainedPath`\n * in `@resq-systems/security/paths`. Detection catches `../`; it does not catch a\n * symlink, nor an absolute path that happens to resolve inside the base.\n * - **SSRF** is prevented by parsing the URL, resolving the host, rejecting private\n * and link-local ranges, re-validating after every redirect, and constraining\n * network egress. A regex over the URL string catches a literal `127.0.0.1` but\n * never a hostname whose DNS record points there.\n *\n * @module @resq-systems/security/threats/rules/system\n */\n\nimport type { ThreatRule } from \"../types.js\";\n\n//#region Command injection\n\n/** Repeated across every command-injection rule. */\nconst CMD_CONTROL =\n\t\"Spawn with an argv array and shell:false; never build a shell string from input\";\n\n/** OS command injection. Scoped to the `shell` context — never runs elsewhere. */\nexport const COMMAND_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"CMD-SUBSTITUTION-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Shell command substitution $(...)\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\t// `(` excluded alongside `)` so a run of `$(` cannot make the engine consume\n\t\t// 200 characters and backtrack at every position. A nested `$(a $(b))` still\n\t\t// matches on the inner substitution.\n\t\tpattern: /\\$\\([^()]{1,200}\\)/,\n\t},\n\t{\n\t\tid: \"CMD-BACKTICK-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Backtick command substitution\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /`[^`]{1,200}`/,\n\t},\n\t{\n\t\tid: \"CMD-CHAIN-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Command separator followed by a state-changing or network command\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern:\n\t\t\t/[;&|]{1,2}\\s{0,8}(?:rm|del|cat|type|wget|curl|nc|ncat|bash|sh|zsh|powershell|pwsh|python[23]?|perl|ruby|chmod|chown|kill|shutdown|reboot|mkfifo|base64|openssl)\\b/i,\n\t},\n\t{\n\t\tid: \"CMD-PIPE-SHELL-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Output piped into a shell interpreter\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /\\|\\s{0,8}(?:sh|bash|zsh|dash|cmd|powershell|pwsh)\\b/i,\n\t},\n\t{\n\t\tid: \"CMD-REDIRECT-SYSTEM-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Redirection into a system directory\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: />\\s{0,8}\\/(?:etc|dev|proc|sys|boot)\\//,\n\t},\n\t{\n\t\t// Commix documents both as filter bypasses against defences that block spaces:\n\t\t// each expands to a word separator, so a command survives a filter that only\n\t\t// looks for whitespace between tokens.\n\t\tid: \"CMD-WORD-SPLIT-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Shell word-splitting construct standing in for a space\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /\\$\\{IFS\\}|\\{\\s{0,4},\\s{0,4}\\}/,\n\t},\n\t{\n\t\t// The shell removes an empty quote pair before execution, so a command name split\n\t\t// by one still runs while defeating a literal match on that name.\n\t\t//\n\t\t// Deliberately narrower than Commix's full set: the equivalent backslash form is\n\t\t// NOT matched, because it is indistinguishable from an ordinary Windows-style\n\t\t// relative path, and a rule firing on those would be disabled by the first person\n\t\t// it inconvenienced.\n\t\tid: \"CMD-QUOTE-SPLIT-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Empty quote pair inside a word, removed by the shell before execution\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /[a-z](?:\"\"|'')[a-z]/i,\n\t},\n\t{\n\t\tid: \"CMD-ENV-EXPANSION-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Shell parameter expansion\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\t// `{` excluded alongside `}` for the same backtracking reason as above.\n\t\tpattern: /\\$\\{[^{}]{1,200}\\}/,\n\t},\n\t{\n\t\tid: \"CMD-ARG-NEWLINE-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line break in a value destined for a command line\",\n\t\tcwe: 88,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /[\\r\\n]/,\n\t},\n];\n\n//#endregion\n\n//#region Path traversal\n\n/** Repeated across every path-traversal rule. */\nconst PATH_CONTROL =\n\t\"Resolve the candidate against the base directory and verify containment (resolveContainedPath); resolve symlinks before use\";\n\n/** Directory-traversal and sensitive-path signatures. Scoped to `filesystem`. */\nexport const PATH_TRAVERSAL_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PATH-TRAVERSAL-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Parent-directory segment\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// Anchored to segment boundaries so `my..file/` and `v1..v2` do not fire.\n\t\tpattern: /(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Percent-encoded parent-directory traversal\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// The previous catalog wrote `/%2e%2e[%2f%5c]/`, whose character class matches\n\t\t// a single character from {%, 2, f, 5, c} — so it fired on `%2e%2e%` and on\n\t\t// `%2e%2e2`, and expressed nothing about the intended separator alternation.\n\t\tpattern: /%2e%2e(?:%2f|%5c|\\/|\\\\)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-MIXED-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Literal dots with a percent-encoded separator\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /\\.\\.(?:%2f|%5c)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-DOUBLE-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Double percent-encoded parent-directory traversal\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /%252e%252e(?:%252f|%255c)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-OVERLONG-UTF8-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Overlong UTF-8 encoding of a path separator\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /%c0%(?:af|2f)|%e0%80%af|%c1%9c/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-NULL-BYTE-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"NUL byte, used to truncate a path before its extension check\",\n\t\tcwe: 158,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: matching the NUL byte is this rule's entire purpose\n\t\tpattern: /%00|\\u0000/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-FILTER-STRIP-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Dot run that survives a naive ../ stripping filter\",\n\t\tcwe: 22,\n\t\tprimaryControl:\n\t\t\t\"Resolve the candidate against the base directory and verify containment (resolveContainedPath). Never sanitize by stripping '../' — stripping is precisely what this payload targets: '....//' becomes '../' after one pass.\",\n\t\t// PATH-TRAVERSAL-001 requires exactly two dots between boundaries, so `....//`\n\t\t// and `.../` walked past it. Bounded at 200 because a longer run is already\n\t\t// reported by the engine's repetition check.\n\t\tpattern: /(?:^|[/\\\\])\\.{3,200}[/\\\\]/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-PARAM-BYPASS-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Path parameter segment used to bypass a gateway path ACL\",\n\t\tcwe: 22,\n\t\tprimaryControl:\n\t\t\t\"Ensure the proxy and the origin agree on path normalization before authorization is applied; resolve and verify containment with resolveContainedPath\",\n\t\t// `..;/` — Tomcat and Spring treat `;` as starting a path parameter, so the\n\t\t// segment normalizes to `..` at the origin while the gateway sees a literal name.\n\t\tpattern: /(?:^|[/\\\\])\\.\\.;[^/\\\\]{0,64}[/\\\\]/,\n\t},\n\t{\n\t\tid: \"PATH-SENSITIVE-UNIX-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Reference to a sensitive Unix path\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /\\/(?:etc\\/(?:passwd|shadow|sudoers|hosts)|proc\\/self|root\\/\\.ssh)\\b/i,\n\t},\n\t{\n\t\tid: \"PATH-SENSITIVE-WINDOWS-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Reference to a sensitive Windows path\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// Requires a system subdirectory. The old rule was a bare `/C:\\\\Windows/gi`,\n\t\t// which fired on any documentation that merely mentioned the folder.\n\t\tpattern: /[a-z]:[\\\\/]{1,2}(?:windows|winnt)[\\\\/]{1,2}(?:system32|win\\.ini|repair)/i,\n\t},\n\t{\n\t\tid: \"PATH-ABSOLUTE-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Absolute path where a relative one is expected\",\n\t\tcwe: 36,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /^(?:\\/|[a-z]:[\\\\/]|\\\\\\\\)/i,\n\t},\n];\n\n//#endregion\n\n//#region File inclusion\n\n/** Repeated across every file-inclusion rule. */\nconst LFI_CONTROL =\n\t\"Map the request to a fixed allowlist of loadable resources; never derive a module or template path from input\";\n\n/** Adds the remote-fetch half of the control, for the RFI rules. */\nconst RFI_CONTROL =\n\t\"Map the request to a fixed allowlist of loadable resources; never derive a module, template, or file path from input. Disable remote stream wrappers (PHP allow_url_fopen/allow_url_include) and reject any candidate not relative to the configured base directory\";\n\n/** Local/remote file inclusion via stream wrappers and alternate schemes. */\nexport const FILE_INCLUSION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"LFI-PHP-WRAPPER-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"PHP stream wrapper used to read or execute source\",\n\t\tcwe: 98,\n\t\tprimaryControl: LFI_CONTROL,\n\t\tpattern: /\\bphp:\\/\\/(?:filter|input|memory|temp|stdin)/i,\n\t},\n\t{\n\t\tid: \"LFI-WRAPPER-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Alternate stream wrapper capable of loading remote or archived content\",\n\t\tcwe: 98,\n\t\tprimaryControl: LFI_CONTROL,\n\t\tpattern: /\\b(?:expect|zip|phar|glob|dict|gopher|netdoc|jar|ogg):\\/\\//i,\n\t},\n\t{\n\t\tid: \"RFI-REMOTE-SCHEME-001\",\n\t\ttype: \"file_inclusion\",\n\t\t// `filesystem` only. The SSRF rules already own `url`; this is about a value\n\t\t// that was supposed to be a *path* and instead names a remote resource.\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Remote scheme where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\tpattern:\n\t\t\t/(?:^\\s{0,8}|[/\\\\=])(?:https?|ftps?|smb|cifs|nfs|webdav|ssh2|rar|zlib|compress\\.(?:zlib|bzip2)):\\/\\//i,\n\t},\n\t{\n\t\tid: \"RFI-DATA-URI-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"data: URI where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\t// The class is ordered `[a-z0-9+.-]` deliberately: writing `[a-z0-9.+-]` embeds\n\t\t// the literal substring `.+`, which the ReDoS structural test rejects.\n\t\tpattern:\n\t\t\t/(?:^\\s{0,8}|[/\\\\=])data:(?:\\/\\/)?(?:[a-z][a-z0-9+.-]{0,32}\\/[a-z0-9+.-]{1,32}[a-z0-9;=+.-]{0,32}|;base64|),/i,\n\t},\n\t{\n\t\tid: \"RFI-REMOTE-HOST-PATH-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Protocol-relative or UNC host path where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\t// Re-grades rather than newly detects: PATH-ABSOLUTE-001 already matches these\n\t\t// at low confidence, contributing 1.0 — not enough to leave the allow band.\n\t\tpattern: /^\\s{0,8}(?:\\/\\/|\\\\\\\\)[a-z0-9][a-z0-9-]{0,62}(?:\\.[a-z0-9-]{1,63}){1,4}[/\\\\]/i,\n\t},\n];\n\n//#endregion\n\n//#region SSRF\n\n/** Repeated across every SSRF rule. */\nconst SSRF_CONTROL =\n\t\"Gate the request with assertOutboundUrl, re-validate after every redirect, and constrain egress at the network layer\";\n\n/**\n * Server-side request forgery.\n *\n * These signatures cover literal addresses only. A hostname whose DNS record\n * resolves to `169.254.169.254` passes every rule here — which is exactly why\n * {@link SSRF_CONTROL} names resolution and egress control as the real defense.\n */\nexport const SSRF_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"SSRF-LOOPBACK-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL host is a loopback address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0|\\[?::1\\]?)(?::\\d{1,5})?(?:[/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-PRIVATE-RANGE-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL host is in an RFC 1918 private range\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/\\/\\/(?:10(?:\\.\\d{1,3}){3}|192\\.168(?:\\.\\d{1,3}){2}|172\\.(?:1[6-9]|2\\d|3[01])(?:\\.\\d{1,3}){2})/,\n\t},\n\t{\n\t\tid: \"SSRF-METADATA-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL targets a cloud instance metadata endpoint\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:169\\.254(?:\\.\\d{1,3}){2}|metadata\\.google\\.internal|metadata\\.goog)\\b/i,\n\t},\n\t{\n\t\tid: \"SSRF-ALT-IP-ENCODING-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Host written as a decimal, octal, or hex integer to evade address filters\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:0x[0-9a-f]{6,8}|0\\d{6,11}|\\d{8,10})(?:[:/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-USERINFO-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Userinfo segment, used to disguise the true host\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/[^/@\\s]{1,128}@/,\n\t},\n\t{\n\t\tid: \"SSRF-NON-HTTP-SCHEME-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Non-HTTP scheme in a fetchable URL\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /^\\s{0,8}(?:file|gopher|dict|tftp|ldaps?|jar|netdoc|sftp):/i,\n\t},\n\t{\n\t\tid: \"SSRF-USERINFO-INTERNAL-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Internal address in the host position behind a userinfo segment\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// The textbook bypass. SSRF-LOOPBACK-001 and SSRF-METADATA-001 both anchor on\n\t\t// `//` immediately before the address, so `http://good.com@169.254.169.254/`\n\t\t// matched only the generic userinfo rule and scored 2.0 — an allow verdict on a\n\t\t// cloud-credential read.\n\t\tpattern:\n\t\t\t/@(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0|169\\.254(?:\\.\\d{1,3}){2}|10(?:\\.\\d{1,3}){3}|192\\.168(?:\\.\\d{1,3}){2}|172\\.(?:1[6-9]|2\\d|3[01])(?:\\.\\d{1,3}){2})(?=[:/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-SUFFIXED-LOOPBACK-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Loopback literal used as a hostname prefix\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0)\\.[a-z]/i,\n\t},\n\t{\n\t\tid: \"SSRF-METADATA-ALT-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Alibaba Cloud or Oracle Cloud instance metadata endpoint\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:100\\.100\\.100\\.200|192\\.0\\.0\\.192)(?=[:/?#]|$)/,\n\t},\n\t{\n\t\tid: \"SSRF-IPV6-INTERNAL-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"IPv6 loopback, unique-local, or link-local literal\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/(?:\\/\\/|@)\\[(?:::|(?:0{1,4}:){2,7}0{0,3}[01]|f[cd][0-9a-f]{0,2}:[0-9a-f:]{0,32}|fe[89ab][0-9a-f]?:[0-9a-f:]{0,32})\\]/i,\n\t},\n\t{\n\t\tid: \"SSRF-IPV4-MAPPED-INTERNAL-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"IPv4-mapped IPv6 literal carrying an internal address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/(?:\\/\\/|@)\\[::ffff:(?:127\\.|10\\.|169\\.254\\.|192\\.168\\.|0\\.0\\.0\\.0|172\\.(?:1[6-9]|2\\d|3[01])\\.|7f[0-9a-f]{2}:|a9fe:|c0a8:)/i,\n\t},\n\t{\n\t\tid: \"SSRF-SHORT-IP-FORM-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Abbreviated or octal IPv4 form resolving to a loopback address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// `127.1`, `0`, and `0177.0.0.1` all resolve to loopback through inet_aton.\n\t\tpattern: /\\/\\/(?:127\\.\\d{1,3}(?:\\.\\d{1,3})?|0|0\\d{2,3}(?:\\.0*\\d{1,3}){3})(?=[:/?#]|$)/,\n\t},\n\t{\n\t\tid: \"SSRF-WILDCARD-DNS-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Wildcard-DNS service that resolves an arbitrary address from the hostname\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// Coverage is incomplete by construction — a new wildcard-DNS service needs a\n\t\t// catalog edit. Host resolution plus egress control is what actually holds.\n\t\tpattern:\n\t\t\t/\\/\\/(?:[^/\\s]{0,64}\\.)?(?:nip\\.io|sslip\\.io|localtest\\.me|lvh\\.me|vcap\\.me|xip\\.io|localho\\.st|traefik\\.me)(?=[:/?#]|$)/i,\n\t},\n];\n\n//#endregion\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { ThreatRule } from "../types.mjs";
2
2
  //#region src/threats/rules/web.d.ts
3
3
  /** XSS signatures. Meaningful only where the value is interpolated into markup. */
4
- declare const XSS_RULES: readonly ThreatRule[];
4
+ export declare const XSS_RULES: readonly ThreatRule[];
5
5
  /**
6
6
  * Prototype-pollution signatures.
7
7
  *
@@ -14,13 +14,12 @@ declare const XSS_RULES: readonly ThreatRule[];
14
14
  * notation or as a quoted JSON key is caught while prose that merely mentions the
15
15
  * word is not.
16
16
  */
17
- declare const PROTOTYPE_POLLUTION_RULES: readonly ThreatRule[];
17
+ export declare const PROTOTYPE_POLLUTION_RULES: readonly ThreatRule[];
18
18
  /** CRLF injection into HTTP or SMTP header values (response splitting). */
19
- declare const HEADER_INJECTION_RULES: readonly ThreatRule[];
19
+ export declare const HEADER_INJECTION_RULES: readonly ThreatRule[];
20
20
  /** CSV/XLSX formula injection — the payload executes in the *recipient's* spreadsheet. */
21
- declare const FORMULA_INJECTION_RULES: readonly ThreatRule[];
21
+ export declare const FORMULA_INJECTION_RULES: readonly ThreatRule[];
22
22
  /** Log injection — forged entries, and terminal escape sequences in logged content. */
23
- declare const LOG_INJECTION_RULES: readonly ThreatRule[];
23
+ export declare const LOG_INJECTION_RULES: readonly ThreatRule[];
24
24
  //#endregion
25
- export { FORMULA_INJECTION_RULES, HEADER_INJECTION_RULES, LOG_INJECTION_RULES, PROTOTYPE_POLLUTION_RULES, XSS_RULES };
26
25
  //# sourceMappingURL=web.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"web.d.mts","names":[],"sources":["../../../src/threats/rules/web.ts"],"mappings":";;;cAqCa,oBAAoB;;;;;;;;;;;;;cAuQpB,oCAAoC;;cAgDpC,iCAAiC;;cA4EjC,kCAAkC;;cAmClC,8BAA8B"}
1
+ {"version":3,"file":"web.d.mts","names":[],"sources":["../../../src/threats/rules/web.ts"],"mappings":";;;qBAsCa,oBAAoB;;;;;;;;;;;;;qBAuQpB,oCAAoC;;qBAgDpC,iCAAiC;;qBA4EjC,kCAAkC;;qBAyClC,8BAA8B"}
@@ -356,7 +356,7 @@ const FORMULA_INJECTION_RULES = [{
356
356
  description: "Cell value begins with a formula-trigger character",
357
357
  cwe: 1236,
358
358
  primaryControl: FORMULA_CONTROL,
359
- pattern: /^[\s'"]{0,8}[=+\-@\t\r]/
359
+ pattern: /^[\s\x1c-\x1f\x85'"]*[=+\-@\t\r\n\uff1d\uff0b\uff0d\uff20]/
360
360
  }, {
361
361
  id: "CSV-DDE-001",
362
362
  type: "formula_injection",
@@ -1 +1 @@
1
- {"version":3,"file":"web.mjs","names":[],"sources":["../../../src/threats/rules/web.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 Browser- and protocol-facing signatures: XSS, prototype pollution,\n * HTTP/SMTP header injection (CRLF), spreadsheet formula injection, and log forging.\n *\n * None of these rules is the control that stops the attack. XSS is stopped by\n * context-correct output encoding plus a parser-based sanitizer; prototype pollution\n * by schema validation that rejects unknown keys and by null-prototype dictionaries;\n * header injection by a header API that refuses CR/LF; formula injection by prefixing\n * exported cells; log forging by structured logging that encodes field values.\n *\n * @module @resq-systems/security/threats/rules/web\n */\n\nimport type { ThreatRule } from \"../types.js\";\n\n//#region Cross-site scripting\n\n/** Repeated across every XSS rule — the control that actually prevents the weakness. */\nconst XSS_CONTROL = \"Escape at the HTML sink; sanitize rich HTML with DOMPurify\";\n\n/** XSS signatures. Meaningful only where the value is interpolated into markup. */\nexport const XSS_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"XSS-SCRIPT-TAG-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Opening <script> tag\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\t// Opening tag only — a greedy `<script>[\\s\\S]*</script>` invites backtracking\n\t\t// blowups on crafted input and adds no detection value.\n\t\tpattern: /<\\s*script\\b/i,\n\t},\n\t{\n\t\tid: \"XSS-EVENT-HANDLER-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Inline event handler attribute inside a tag\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern: /<[a-z][^>]{0,300}\\bon[a-z]{3,20}\\s{0,8}=/i,\n\t},\n\t{\n\t\tid: \"XSS-EVENT-HANDLER-002\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Event-handler-shaped assignment to a quoted value\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern:\n\t\t\t/\\bon(?:error|load|click|focus|blur|submit|change|toggle|mouse[a-z]{2,12}|key[a-z]{2,6}|animation[a-z]{3,10}|pointer[a-z]{2,12})\\s{0,8}=\\s{0,8}[\"'`]/i,\n\t},\n\t{\n\t\tid: \"XSS-URI-SCHEME-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"url\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Script-bearing URI scheme\",\n\t\tcwe: 79,\n\t\tprimaryControl: \"Allowlist the URL scheme before rendering or navigating\",\n\t\tpattern: /\\b(?:javascript|vbscript|livescript|mocha)\\s{0,8}:/i,\n\t},\n\t{\n\t\tid: \"XSS-DATA-URI-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"data: URI carrying an active content type\",\n\t\tcwe: 79,\n\t\tprimaryControl: \"Allowlist the URL scheme before rendering or navigating\",\n\t\tpattern:\n\t\t\t/\\bdata\\s{0,8}:\\s{0,8}(?:text\\/html|image\\/svg\\+xml|application\\/(?:javascript|xhtml\\+xml|ecmascript))/i,\n\t},\n\t{\n\t\tid: \"XSS-DANGEROUS-TAG-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"xml\"],\n\t\t// High rather than medium: these tags load or reframe active content, which\n\t\t// buys clickjacking, phishing overlays, and drive-by loads even without script\n\t\t// execution. Confidence stays medium because a rich-text field may legitimately\n\t\t// permit some of them — that is what `excludeRuleIds` is for.\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Tag that can load or reframe active content\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern: /<\\s{0,8}(?:iframe|object|embed|applet|base|meta|link|svg|math|portal|frameset)\\b/i,\n\t},\n\t{\n\t\tid: \"XSS-STYLE-EXPRESSION-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"CSS expression() — legacy IE script execution vector\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern: /\\bexpression\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"XSS-DOM-SINK-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Reference to a credential- or navigation-bearing DOM property\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:document|window)\\s{0,8}\\.\\s{0,8}(?:cookie|domain|write|writeln|location|open)\\b/i,\n\t},\n\t{\n\t\tid: \"XSS-DOM-SINK-002\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Assignment to a raw-markup DOM sink\",\n\t\tcwe: 79,\n\t\tprimaryControl: \"Assign via textContent, or sanitize with DOMPurify first\",\n\t\tpattern: /\\b(?:innerHTML|outerHTML|srcdoc)\\s{0,8}=|\\binsertAdjacentHTML\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"XSS-DYNAMIC-EVAL-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Dynamic code evaluation (eval / Function constructor)\",\n\t\tcwe: 95,\n\t\tprimaryControl: \"Never evaluate strings; a CSP without 'unsafe-eval' blocks this\",\n\t\tpattern: /\\beval\\s{0,8}\\(|\\bnew\\s+Function\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"XSS-URI-SCHEME-002\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Script-bearing URI scheme split by tab, CR, or LF\",\n\t\tcwe: 79,\n\t\tprimaryControl:\n\t\t\t\"Allowlist the URL scheme after canonicalization: decode HTML character references, strip ASCII tab/CR/LF, then compare against an allowlist (http, https, mailto)\",\n\t\t// XSS-URI-SCHEME-001 allows whitespace only *between* the token and the colon,\n\t\t// so `jav&#x09;ascript:alert(1)` scanned clean and still executes in current\n\t\t// browsers — the WHATWG URL parser strips exactly tab, CR, and LF from a scheme.\n\t\t// The leading lookahead requires an intra-token separator, so a plain\n\t\t// `javascript:` is not double-scored by this rule and -001 together.\n\t\tpattern:\n\t\t\t/(?=[a-z\\t\\n\\r]{0,16}[\\t\\n\\r])j[\\t\\n\\r]{0,8}a[\\t\\n\\r]{0,8}v[\\t\\n\\r]{0,8}a[\\t\\n\\r]{0,8}s[\\t\\n\\r]{0,8}c[\\t\\n\\r]{0,8}r[\\t\\n\\r]{0,8}i[\\t\\n\\r]{0,8}p[\\t\\n\\r]{0,8}t[\\t\\n\\r ]{0,8}:/i,\n\t},\n\t{\n\t\tid: \"XSS-DYNAMIC-EVAL-002\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\t// Graded to match XSS-DYNAMIC-EVAL-001: a string-argument timer is\n\t\t// eval-equivalent, so the same severity applies. Confidence stays medium\n\t\t// because the token appears in ordinary code discussion.\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"String-argument timer or Function call — an implicit eval sink\",\n\t\tcwe: 95,\n\t\tprimaryControl: \"Never evaluate strings; a CSP without 'unsafe-eval' blocks this\",\n\t\tpattern:\n\t\t\t/\\b(?:setTimeout|setInterval|setImmediate|Function)\\s{0,8}\\(\\s{0,8}[\"'`]|\\bexecScript\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"XSS-DYNAMIC-EVAL-003\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"constructor.constructor chain used to reach the Function constructor\",\n\t\tcwe: 95,\n\t\tprimaryControl: \"Never evaluate strings; a CSP without 'unsafe-eval' blocks this\",\n\t\tpattern: /\\bconstructor\\s{0,8}(?:\\.\\s{0,8}constructor|\\[\\s{0,8}[\"'`]constructor)/i,\n\t},\n\t{\n\t\tid: \"XSS-EVENT-HANDLER-003\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Event handler introduced by breaking out of a quoted attribute\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\t// XSS-EVENT-HANDLER-001 needs a preceding `<tag`; -002 needs a quote *after*\n\t\t// the `=`. An attribute-breakout payload (`\" onmouseover=alert(1) x=\"`) has\n\t\t// neither, so it scanned clean while its quoted twin scored 2 — the pair was\n\t\t// inconsistent precisely on the harder case.\n\t\t// Handler names are an explicit allowlist rather than `on[a-z]{3,20}`, so\n\t\t// `\"onboarding=\"` and `\"onlineStatus=\"` cannot trip it.\n\t\tpattern:\n\t\t\t/[\"'`]\\s{0,8}on(?:error|load|click|focus|blur|submit|change|toggle|wheel|scroll|input|select|paste|copy|cut|contextmenu|auxclick|mouse[a-z]{2,12}|key[a-z]{2,6}|animation[a-z]{3,10}|pointer[a-z]{2,12}|drag[a-z]{0,10}|touch[a-z]{2,10})\\s{0,8}=/i,\n\t},\n\t{\n\t\tid: \"XSS-EVENT-HANDLER-004\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\t// High, matching every other event-handler rule: an inline handler assigned a\n\t\t// call expression *is* script execution. Confidence stays medium because the\n\t\t// same text appears in escaped documentation.\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Unquoted event handler assigned a callable expression\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\t// The `(?:^|[^.\\w-])` prefix is a consuming class rather than a lookbehind, to\n\t\t// match catalog style; it excludes ordinary property assignment (`el.onclick=`).\n\t\t// Whitespace before `=` is forbidden, which kills prose false positives such as\n\t\t// \"onload = init()\" at the cost of missing spaced payloads into unquoted sinks.\n\t\tpattern:\n\t\t\t/(?:^|[^.\\w-])on(?:error|load|click|focus|blur|submit|change|toggle|wheel|scroll|input|select|paste|copy|cut|contextmenu|auxclick|mouse[a-z]{2,12}|key[a-z]{2,6}|animation[a-z]{3,10}|pointer[a-z]{2,12}|drag[a-z]{0,10}|touch[a-z]{2,10})=\\s{0,8}[\\w$.[\\]]{1,40}\\s{0,8}[(`]/i,\n\t},\n\t{\n\t\tid: \"XSS-FORM-TAG-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Injected form or input element — credential-harvesting overlay\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\t// Must exclude `html_decoded`: without that it fires on any CMS body that\n\t\t// documents HTML in escaped form (`&lt;form action=\"/login\"&gt;`). An\n\t\t// entity-encoded form tag is already neutralized — the encoding *is* the\n\t\t// control. Percent-encoded evasion stays covered.\n\t\tvariants: [\"raw\", \"nfc\", \"nfkc\", \"percent_decoded\"],\n\t\tpattern: /<\\s{0,8}(?:form|input|textarea|button|select|keygen|isindex)\\b/i,\n\t},\n\t{\n\t\tid: \"XSS-STYLE-TAG-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Injected style element\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tvariants: [\"raw\", \"nfc\", \"nfkc\", \"percent_decoded\"],\n\t\tpattern: /<\\s{0,8}style\\b/i,\n\t},\n\t{\n\t\tid: \"XSS-DANGLING-MARKUP-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Unterminated resource attribute — exfiltrates the following markup\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:src|href|action|formaction|poster|background)\\s{0,8}=\\s{0,8}[\"'][^\"'<>]{0,300}$/i,\n\t},\n];\n\n//#endregion\n\n//#region Prototype pollution\n\n/** Repeated across the prototype-pollution rules. */\nconst PROTO_CONTROL =\n\t\"Reject unknown keys via schema validation; use Object.create(null) maps and a merge that skips __proto__/constructor/prototype\";\n\n/**\n * Prototype-pollution signatures.\n *\n * Deliberately its own {@link ThreatRule.type} rather than a subcategory of XSS:\n * this is an object-graph manipulation weakness, not a markup one, and it is fixed\n * by different controls (`Object.create(null)` dictionaries, schema validation that\n * rejects unknown keys, merge helpers that skip inherited and forbidden keys).\n *\n * The patterns match structural property paths, so `__proto__` reached via bracket\n * notation or as a quoted JSON key is caught while prose that merely mentions the\n * word is not.\n */\nexport const PROTOTYPE_POLLUTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PROTO-POLLUTION-PROTO-001\",\n\t\ttype: \"prototype_pollution\",\n\t\tcontexts: [\"object_merge\", \"nosql\", \"url\", \"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"__proto__ used as a structural property path segment\",\n\t\tcwe: 1321,\n\t\tprimaryControl: PROTO_CONTROL,\n\t\tpattern: /(?:^|[.[\\]\"'])__proto__(?:$|[.[\\]\"'])/,\n\t},\n\t{\n\t\tid: \"PROTO-POLLUTION-PROTOTYPE-001\",\n\t\ttype: \"prototype_pollution\",\n\t\tcontexts: [\"object_merge\", \"nosql\", \"url\", \"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"'prototype' used as a structural property path segment\",\n\t\tcwe: 1321,\n\t\tprimaryControl: PROTO_CONTROL,\n\t\tpattern: /(?:^|[.[\\]\"'])prototype(?:$|[.[\\]\"'])/,\n\t},\n\t{\n\t\tid: \"PROTO-POLLUTION-CONSTRUCTOR-001\",\n\t\ttype: \"prototype_pollution\",\n\t\tcontexts: [\"object_merge\", \"nosql\", \"url\", \"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"constructor.prototype access chain\",\n\t\tcwe: 1321,\n\t\tprimaryControl: PROTO_CONTROL,\n\t\t// Two shapes: property/bracket access (`constructor.prototype`,\n\t\t// `constructor[\"prototype\"]`) and the nested-object form that arrives through\n\t\t// a JSON body or query-string expansion (`\"constructor\":{\"prototype\":…}`).\n\t\tpattern:\n\t\t\t/\\bconstructor\\s{0,8}(?:\\.|\\[)\\s{0,8}[\"']?prototype\\b|[\"']constructor[\"']\\s{0,8}:\\s{0,8}\\{\\s{0,8}[\"']prototype[\"']/i,\n\t},\n];\n\n//#endregion\n\n//#region Header injection\n\n/** Repeated across the header-injection rules. */\nconst HEADER_CONTROL = \"Use a header API that rejects CR/LF; never concatenate raw header strings\";\n\n/** CRLF injection into HTTP or SMTP header values (response splitting). */\nexport const HEADER_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"HEADER-CRLF-001\",\n\t\ttype: \"header_injection\",\n\t\tcontexts: [\"http_header\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Raw CR or LF in a header value\",\n\t\tcwe: 113,\n\t\tprimaryControl: HEADER_CONTROL,\n\t\tpattern: /[\\r\\n]/,\n\t},\n\t{\n\t\tid: \"HEADER-CRLF-ENCODED-001\",\n\t\ttype: \"header_injection\",\n\t\tcontexts: [\"http_header\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Percent-encoded CR/LF or NUL, decodable into a header break\",\n\t\tcwe: 113,\n\t\tprimaryControl: HEADER_CONTROL,\n\t\tpattern: /%0d|%0a|%00/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"HEADER-SMUGGLED-NAME-001\",\n\t\ttype: \"header_injection\",\n\t\tcontexts: [\"http_header\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Line break followed by a security-relevant header name\",\n\t\tcwe: 113,\n\t\tprimaryControl: HEADER_CONTROL,\n\t\tpattern:\n\t\t\t/[\\r\\n]\\s{0,8}(?:set-cookie|location|content-length|transfer-encoding|host|authorization|bcc|cc|to)\\s{0,8}:/i,\n\t},\n\t{\n\t\tid: \"HEADER-UNICODE-LINEBREAK-001\",\n\t\ttype: \"header_injection\",\n\t\tcontexts: [\"http_header\", \"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Unicode line break — treated as a newline by some parsers and loggers\",\n\t\tcwe: 113,\n\t\tprimaryControl: HEADER_CONTROL,\n\t\t// U+2028 LINE SEPARATOR, U+2029 PARAGRAPH SEPARATOR, U+0085 NEL. None is matched\n\t\t// by `[\\r\\n]`, yet each terminates a line in JS string literals, in several log\n\t\t// processors, and in some header parsers. All three scanned clean before this.\n\t\tpattern: /[\\u2028\\u2029\\u0085]/,\n\t},\n\t{\n\t\tid: \"HEADER-CRLF-OVERLONG-001\",\n\t\ttype: \"header_injection\",\n\t\tcontexts: [\"http_header\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Overlong or Unicode-squeezed encoding of CR/LF\",\n\t\tcwe: 113,\n\t\tprimaryControl: HEADER_CONTROL,\n\t\t// `%e5%98%8a` is U+560A, whose low byte is 0x0A: any component that narrows\n\t\t// UTF-16 to a byte emits a bare LF. `%c0%8a` is the overlong UTF-8 form of the\n\t\t// same byte. Both slip past a filter that only looks for `%0a`.\n\t\tvariants: [\"raw\"],\n\t\tpattern: /%e5%98%8[ad]|%c0%8[ad]|%c1%8[ad]/i,\n\t},\n];\n\n//#endregion\n\n//#region Spreadsheet formula injection\n\n/** Repeated across the formula-injection rules. */\nconst FORMULA_CONTROL =\n\t\"Escape exported cells with escapeCsvField / toCsvRow before writing the file\";\n\n/** CSV/XLSX formula injection — the payload executes in the *recipient's* spreadsheet. */\nexport const FORMULA_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"CSV-FORMULA-LEAD-001\",\n\t\ttype: \"formula_injection\",\n\t\tcontexts: [\"spreadsheet\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Cell value begins with a formula-trigger character\",\n\t\tcwe: 1236,\n\t\tprimaryControl: FORMULA_CONTROL,\n\t\tpattern: /^[\\s'\"]{0,8}[=+\\-@\\t\\r]/,\n\t},\n\t{\n\t\tid: \"CSV-DDE-001\",\n\t\ttype: \"formula_injection\",\n\t\tcontexts: [\"spreadsheet\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Spreadsheet function capable of remote fetch or command execution\",\n\t\tcwe: 1236,\n\t\tprimaryControl: FORMULA_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:DDE|DDEAUTO|WEBSERVICE|HYPERLINK|IMPORTXML|IMPORTDATA|IMPORTFEED|IMPORTHTML|IMPORTRANGE|RTD)\\s{0,8}\\(/i,\n\t},\n];\n\n//#endregion\n\n//#region Log forging\n\n/** Repeated across the log-injection rules. */\nconst LOG_CONTROL =\n\t\"Log structured records; encode field values with encodeLogValue before writing them\";\n\n/** Log injection — forged entries, and terminal escape sequences in logged content. */\nexport const LOG_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"LOG-CRLF-001\",\n\t\ttype: \"log_injection\",\n\t\tcontexts: [\"log\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line break in a value written to a line-based log\",\n\t\tcwe: 117,\n\t\tprimaryControl: LOG_CONTROL,\n\t\tpattern: /[\\r\\n]/,\n\t},\n\t{\n\t\tid: \"LOG-FORGED-ENTRY-001\",\n\t\ttype: \"log_injection\",\n\t\tcontexts: [\"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line break followed by a forged log-level marker\",\n\t\tcwe: 117,\n\t\tprimaryControl: LOG_CONTROL,\n\t\tpattern: /[\\r\\n]\\s{0,8}[[{(]?\\s{0,8}(?:INFO|WARN|WARNING|ERROR|DEBUG|FATAL|TRACE|CRITICAL)\\b/i,\n\t},\n\t{\n\t\tid: \"LOG-ANSI-ESCAPE-001\",\n\t\ttype: \"log_injection\",\n\t\tcontexts: [\"log\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"ANSI/terminal escape sequence in logged content\",\n\t\tcwe: 117,\n\t\tprimaryControl:\n\t\t\t\"Encode with encodeLogValue, or strip with stripAnsi when the rendering matters more than the record\",\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI sequences are defined by the ESC control character\n\t\tpattern: /\\u001b\\[[0-9;?]{0,32}[\\x40-\\x7e]/,\n\t},\n];\n\n//#endregion\n"],"mappings":"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"}
1
+ {"version":3,"file":"web.mjs","names":[],"sources":["../../../src/threats/rules/web.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 Browser- and protocol-facing signatures: XSS, prototype pollution,\n * HTTP/SMTP header injection (CRLF), spreadsheet formula injection, and log forging.\n *\n * None of these rules is the control that stops the attack. XSS is stopped by\n * context-correct output encoding plus a parser-based sanitizer; prototype pollution\n * by schema validation that rejects unknown keys and by null-prototype dictionaries;\n * header injection by a header API that refuses CR/LF; formula injection by prefixing\n * exported cells; log forging by structured logging that encodes field values.\n *\n * @module @resq-systems/security/threats/rules/web\n */\n\nimport type { ThreatRule } from \"../types.js\";\n\n//#region Cross-site scripting\n\n/** Repeated across every XSS rule — the control that actually prevents the weakness. */\nconst XSS_CONTROL = \"Escape at the HTML sink; sanitize rich HTML with DOMPurify\";\n\n/** XSS signatures. Meaningful only where the value is interpolated into markup. */\nexport const XSS_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"XSS-SCRIPT-TAG-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Opening <script> tag\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\t// Opening tag only — a greedy `<script>[\\s\\S]*</script>` invites backtracking\n\t\t// blowups on crafted input and adds no detection value.\n\t\tpattern: /<\\s*script\\b/i,\n\t},\n\t{\n\t\tid: \"XSS-EVENT-HANDLER-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Inline event handler attribute inside a tag\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern: /<[a-z][^>]{0,300}\\bon[a-z]{3,20}\\s{0,8}=/i,\n\t},\n\t{\n\t\tid: \"XSS-EVENT-HANDLER-002\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Event-handler-shaped assignment to a quoted value\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern:\n\t\t\t/\\bon(?:error|load|click|focus|blur|submit|change|toggle|mouse[a-z]{2,12}|key[a-z]{2,6}|animation[a-z]{3,10}|pointer[a-z]{2,12})\\s{0,8}=\\s{0,8}[\"'`]/i,\n\t},\n\t{\n\t\tid: \"XSS-URI-SCHEME-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"url\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Script-bearing URI scheme\",\n\t\tcwe: 79,\n\t\tprimaryControl: \"Allowlist the URL scheme before rendering or navigating\",\n\t\tpattern: /\\b(?:javascript|vbscript|livescript|mocha)\\s{0,8}:/i,\n\t},\n\t{\n\t\tid: \"XSS-DATA-URI-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"data: URI carrying an active content type\",\n\t\tcwe: 79,\n\t\tprimaryControl: \"Allowlist the URL scheme before rendering or navigating\",\n\t\tpattern:\n\t\t\t/\\bdata\\s{0,8}:\\s{0,8}(?:text\\/html|image\\/svg\\+xml|application\\/(?:javascript|xhtml\\+xml|ecmascript))/i,\n\t},\n\t{\n\t\tid: \"XSS-DANGEROUS-TAG-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"xml\"],\n\t\t// High rather than medium: these tags load or reframe active content, which\n\t\t// buys clickjacking, phishing overlays, and drive-by loads even without script\n\t\t// execution. Confidence stays medium because a rich-text field may legitimately\n\t\t// permit some of them — that is what `excludeRuleIds` is for.\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Tag that can load or reframe active content\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern: /<\\s{0,8}(?:iframe|object|embed|applet|base|meta|link|svg|math|portal|frameset)\\b/i,\n\t},\n\t{\n\t\tid: \"XSS-STYLE-EXPRESSION-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"CSS expression() — legacy IE script execution vector\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern: /\\bexpression\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"XSS-DOM-SINK-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Reference to a credential- or navigation-bearing DOM property\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:document|window)\\s{0,8}\\.\\s{0,8}(?:cookie|domain|write|writeln|location|open)\\b/i,\n\t},\n\t{\n\t\tid: \"XSS-DOM-SINK-002\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Assignment to a raw-markup DOM sink\",\n\t\tcwe: 79,\n\t\tprimaryControl: \"Assign via textContent, or sanitize with DOMPurify first\",\n\t\tpattern: /\\b(?:innerHTML|outerHTML|srcdoc)\\s{0,8}=|\\binsertAdjacentHTML\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"XSS-DYNAMIC-EVAL-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Dynamic code evaluation (eval / Function constructor)\",\n\t\tcwe: 95,\n\t\tprimaryControl: \"Never evaluate strings; a CSP without 'unsafe-eval' blocks this\",\n\t\tpattern: /\\beval\\s{0,8}\\(|\\bnew\\s+Function\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"XSS-URI-SCHEME-002\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Script-bearing URI scheme split by tab, CR, or LF\",\n\t\tcwe: 79,\n\t\tprimaryControl:\n\t\t\t\"Allowlist the URL scheme after canonicalization: decode HTML character references, strip ASCII tab/CR/LF, then compare against an allowlist (http, https, mailto)\",\n\t\t// XSS-URI-SCHEME-001 allows whitespace only *between* the token and the colon,\n\t\t// so `jav&#x09;ascript:alert(1)` scanned clean and still executes in current\n\t\t// browsers — the WHATWG URL parser strips exactly tab, CR, and LF from a scheme.\n\t\t// The leading lookahead requires an intra-token separator, so a plain\n\t\t// `javascript:` is not double-scored by this rule and -001 together.\n\t\tpattern:\n\t\t\t/(?=[a-z\\t\\n\\r]{0,16}[\\t\\n\\r])j[\\t\\n\\r]{0,8}a[\\t\\n\\r]{0,8}v[\\t\\n\\r]{0,8}a[\\t\\n\\r]{0,8}s[\\t\\n\\r]{0,8}c[\\t\\n\\r]{0,8}r[\\t\\n\\r]{0,8}i[\\t\\n\\r]{0,8}p[\\t\\n\\r]{0,8}t[\\t\\n\\r ]{0,8}:/i,\n\t},\n\t{\n\t\tid: \"XSS-DYNAMIC-EVAL-002\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\t// Graded to match XSS-DYNAMIC-EVAL-001: a string-argument timer is\n\t\t// eval-equivalent, so the same severity applies. Confidence stays medium\n\t\t// because the token appears in ordinary code discussion.\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"String-argument timer or Function call — an implicit eval sink\",\n\t\tcwe: 95,\n\t\tprimaryControl: \"Never evaluate strings; a CSP without 'unsafe-eval' blocks this\",\n\t\tpattern:\n\t\t\t/\\b(?:setTimeout|setInterval|setImmediate|Function)\\s{0,8}\\(\\s{0,8}[\"'`]|\\bexecScript\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"XSS-DYNAMIC-EVAL-003\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"constructor.constructor chain used to reach the Function constructor\",\n\t\tcwe: 95,\n\t\tprimaryControl: \"Never evaluate strings; a CSP without 'unsafe-eval' blocks this\",\n\t\tpattern: /\\bconstructor\\s{0,8}(?:\\.\\s{0,8}constructor|\\[\\s{0,8}[\"'`]constructor)/i,\n\t},\n\t{\n\t\tid: \"XSS-EVENT-HANDLER-003\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Event handler introduced by breaking out of a quoted attribute\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\t// XSS-EVENT-HANDLER-001 needs a preceding `<tag`; -002 needs a quote *after*\n\t\t// the `=`. An attribute-breakout payload (`\" onmouseover=alert(1) x=\"`) has\n\t\t// neither, so it scanned clean while its quoted twin scored 2 — the pair was\n\t\t// inconsistent precisely on the harder case.\n\t\t// Handler names are an explicit allowlist rather than `on[a-z]{3,20}`, so\n\t\t// `\"onboarding=\"` and `\"onlineStatus=\"` cannot trip it.\n\t\tpattern:\n\t\t\t/[\"'`]\\s{0,8}on(?:error|load|click|focus|blur|submit|change|toggle|wheel|scroll|input|select|paste|copy|cut|contextmenu|auxclick|mouse[a-z]{2,12}|key[a-z]{2,6}|animation[a-z]{3,10}|pointer[a-z]{2,12}|drag[a-z]{0,10}|touch[a-z]{2,10})\\s{0,8}=/i,\n\t},\n\t{\n\t\tid: \"XSS-EVENT-HANDLER-004\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\t// High, matching every other event-handler rule: an inline handler assigned a\n\t\t// call expression *is* script execution. Confidence stays medium because the\n\t\t// same text appears in escaped documentation.\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Unquoted event handler assigned a callable expression\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\t// The `(?:^|[^.\\w-])` prefix is a consuming class rather than a lookbehind, to\n\t\t// match catalog style; it excludes ordinary property assignment (`el.onclick=`).\n\t\t// Whitespace before `=` is forbidden, which kills prose false positives such as\n\t\t// \"onload = init()\" at the cost of missing spaced payloads into unquoted sinks.\n\t\tpattern:\n\t\t\t/(?:^|[^.\\w-])on(?:error|load|click|focus|blur|submit|change|toggle|wheel|scroll|input|select|paste|copy|cut|contextmenu|auxclick|mouse[a-z]{2,12}|key[a-z]{2,6}|animation[a-z]{3,10}|pointer[a-z]{2,12}|drag[a-z]{0,10}|touch[a-z]{2,10})=\\s{0,8}[\\w$.[\\]]{1,40}\\s{0,8}[(`]/i,\n\t},\n\t{\n\t\tid: \"XSS-FORM-TAG-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Injected form or input element — credential-harvesting overlay\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\t// Must exclude `html_decoded`: without that it fires on any CMS body that\n\t\t// documents HTML in escaped form (`&lt;form action=\"/login\"&gt;`). An\n\t\t// entity-encoded form tag is already neutralized — the encoding *is* the\n\t\t// control. Percent-encoded evasion stays covered.\n\t\tvariants: [\"raw\", \"nfc\", \"nfkc\", \"percent_decoded\"],\n\t\tpattern: /<\\s{0,8}(?:form|input|textarea|button|select|keygen|isindex)\\b/i,\n\t},\n\t{\n\t\tid: \"XSS-STYLE-TAG-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Injected style element\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tvariants: [\"raw\", \"nfc\", \"nfkc\", \"percent_decoded\"],\n\t\tpattern: /<\\s{0,8}style\\b/i,\n\t},\n\t{\n\t\tid: \"XSS-DANGLING-MARKUP-001\",\n\t\ttype: \"xss\",\n\t\tcontexts: [\"html\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Unterminated resource attribute — exfiltrates the following markup\",\n\t\tcwe: 79,\n\t\tprimaryControl: XSS_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:src|href|action|formaction|poster|background)\\s{0,8}=\\s{0,8}[\"'][^\"'<>]{0,300}$/i,\n\t},\n];\n\n//#endregion\n\n//#region Prototype pollution\n\n/** Repeated across the prototype-pollution rules. */\nconst PROTO_CONTROL =\n\t\"Reject unknown keys via schema validation; use Object.create(null) maps and a merge that skips __proto__/constructor/prototype\";\n\n/**\n * Prototype-pollution signatures.\n *\n * Deliberately its own {@link ThreatRule.type} rather than a subcategory of XSS:\n * this is an object-graph manipulation weakness, not a markup one, and it is fixed\n * by different controls (`Object.create(null)` dictionaries, schema validation that\n * rejects unknown keys, merge helpers that skip inherited and forbidden keys).\n *\n * The patterns match structural property paths, so `__proto__` reached via bracket\n * notation or as a quoted JSON key is caught while prose that merely mentions the\n * word is not.\n */\nexport const PROTOTYPE_POLLUTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PROTO-POLLUTION-PROTO-001\",\n\t\ttype: \"prototype_pollution\",\n\t\tcontexts: [\"object_merge\", \"nosql\", \"url\", \"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"__proto__ used as a structural property path segment\",\n\t\tcwe: 1321,\n\t\tprimaryControl: PROTO_CONTROL,\n\t\tpattern: /(?:^|[.[\\]\"'])__proto__(?:$|[.[\\]\"'])/,\n\t},\n\t{\n\t\tid: \"PROTO-POLLUTION-PROTOTYPE-001\",\n\t\ttype: \"prototype_pollution\",\n\t\tcontexts: [\"object_merge\", \"nosql\", \"url\", \"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"'prototype' used as a structural property path segment\",\n\t\tcwe: 1321,\n\t\tprimaryControl: PROTO_CONTROL,\n\t\tpattern: /(?:^|[.[\\]\"'])prototype(?:$|[.[\\]\"'])/,\n\t},\n\t{\n\t\tid: \"PROTO-POLLUTION-CONSTRUCTOR-001\",\n\t\ttype: \"prototype_pollution\",\n\t\tcontexts: [\"object_merge\", \"nosql\", \"url\", \"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"constructor.prototype access chain\",\n\t\tcwe: 1321,\n\t\tprimaryControl: PROTO_CONTROL,\n\t\t// Two shapes: property/bracket access (`constructor.prototype`,\n\t\t// `constructor[\"prototype\"]`) and the nested-object form that arrives through\n\t\t// a JSON body or query-string expansion (`\"constructor\":{\"prototype\":…}`).\n\t\tpattern:\n\t\t\t/\\bconstructor\\s{0,8}(?:\\.|\\[)\\s{0,8}[\"']?prototype\\b|[\"']constructor[\"']\\s{0,8}:\\s{0,8}\\{\\s{0,8}[\"']prototype[\"']/i,\n\t},\n];\n\n//#endregion\n\n//#region Header injection\n\n/** Repeated across the header-injection rules. */\nconst HEADER_CONTROL = \"Use a header API that rejects CR/LF; never concatenate raw header strings\";\n\n/** CRLF injection into HTTP or SMTP header values (response splitting). */\nexport const HEADER_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"HEADER-CRLF-001\",\n\t\ttype: \"header_injection\",\n\t\tcontexts: [\"http_header\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Raw CR or LF in a header value\",\n\t\tcwe: 113,\n\t\tprimaryControl: HEADER_CONTROL,\n\t\tpattern: /[\\r\\n]/,\n\t},\n\t{\n\t\tid: \"HEADER-CRLF-ENCODED-001\",\n\t\ttype: \"header_injection\",\n\t\tcontexts: [\"http_header\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Percent-encoded CR/LF or NUL, decodable into a header break\",\n\t\tcwe: 113,\n\t\tprimaryControl: HEADER_CONTROL,\n\t\tpattern: /%0d|%0a|%00/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"HEADER-SMUGGLED-NAME-001\",\n\t\ttype: \"header_injection\",\n\t\tcontexts: [\"http_header\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Line break followed by a security-relevant header name\",\n\t\tcwe: 113,\n\t\tprimaryControl: HEADER_CONTROL,\n\t\tpattern:\n\t\t\t/[\\r\\n]\\s{0,8}(?:set-cookie|location|content-length|transfer-encoding|host|authorization|bcc|cc|to)\\s{0,8}:/i,\n\t},\n\t{\n\t\tid: \"HEADER-UNICODE-LINEBREAK-001\",\n\t\ttype: \"header_injection\",\n\t\tcontexts: [\"http_header\", \"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Unicode line break — treated as a newline by some parsers and loggers\",\n\t\tcwe: 113,\n\t\tprimaryControl: HEADER_CONTROL,\n\t\t// U+2028 LINE SEPARATOR, U+2029 PARAGRAPH SEPARATOR, U+0085 NEL. None is matched\n\t\t// by `[\\r\\n]`, yet each terminates a line in JS string literals, in several log\n\t\t// processors, and in some header parsers. All three scanned clean before this.\n\t\tpattern: /[\\u2028\\u2029\\u0085]/,\n\t},\n\t{\n\t\tid: \"HEADER-CRLF-OVERLONG-001\",\n\t\ttype: \"header_injection\",\n\t\tcontexts: [\"http_header\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Overlong or Unicode-squeezed encoding of CR/LF\",\n\t\tcwe: 113,\n\t\tprimaryControl: HEADER_CONTROL,\n\t\t// `%e5%98%8a` is U+560A, whose low byte is 0x0A: any component that narrows\n\t\t// UTF-16 to a byte emits a bare LF. `%c0%8a` is the overlong UTF-8 form of the\n\t\t// same byte. Both slip past a filter that only looks for `%0a`.\n\t\tvariants: [\"raw\"],\n\t\tpattern: /%e5%98%8[ad]|%c0%8[ad]|%c1%8[ad]/i,\n\t},\n];\n\n//#endregion\n\n//#region Spreadsheet formula injection\n\n/** Repeated across the formula-injection rules. */\nconst FORMULA_CONTROL =\n\t\"Escape exported cells with escapeCsvField / toCsvRow before writing the file\";\n\n/** CSV/XLSX formula injection — the payload executes in the *recipient's* spreadsheet. */\nexport const FORMULA_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"CSV-FORMULA-LEAD-001\",\n\t\ttype: \"formula_injection\",\n\t\tcontexts: [\"spreadsheet\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Cell value begins with a formula-trigger character\",\n\t\tcwe: 1236,\n\t\tprimaryControl: FORMULA_CONTROL,\n\t\t// Unbounded, like `escapeCsvField`: a reader that strips the leading run strips all\n\t\t// of it. One anchored character class under `*` backtracks linearly. The run adds\n\t\t// U+001C to U+001F and U+0085 to `\\s`, since Python's `strip()`, .NET's `Trim()` and\n\t\t// Java's `trim()` remove some or all of them. The triggers are the OWASP list, LF and\n\t\t// the full-width `=` `+` `-` `@` included.\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: U+001C to U+001F are whitespace to the readers that trim them\n\t\tpattern: /^[\\s\\x1c-\\x1f\\x85'\"]*[=+\\-@\\t\\r\\n\\uff1d\\uff0b\\uff0d\\uff20]/,\n\t},\n\t{\n\t\tid: \"CSV-DDE-001\",\n\t\ttype: \"formula_injection\",\n\t\tcontexts: [\"spreadsheet\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Spreadsheet function capable of remote fetch or command execution\",\n\t\tcwe: 1236,\n\t\tprimaryControl: FORMULA_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:DDE|DDEAUTO|WEBSERVICE|HYPERLINK|IMPORTXML|IMPORTDATA|IMPORTFEED|IMPORTHTML|IMPORTRANGE|RTD)\\s{0,8}\\(/i,\n\t},\n];\n\n//#endregion\n\n//#region Log forging\n\n/** Repeated across the log-injection rules. */\nconst LOG_CONTROL =\n\t\"Log structured records; encode field values with encodeLogValue before writing them\";\n\n/** Log injection — forged entries, and terminal escape sequences in logged content. */\nexport const LOG_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"LOG-CRLF-001\",\n\t\ttype: \"log_injection\",\n\t\tcontexts: [\"log\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line break in a value written to a line-based log\",\n\t\tcwe: 117,\n\t\tprimaryControl: LOG_CONTROL,\n\t\tpattern: /[\\r\\n]/,\n\t},\n\t{\n\t\tid: \"LOG-FORGED-ENTRY-001\",\n\t\ttype: \"log_injection\",\n\t\tcontexts: [\"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line break followed by a forged log-level marker\",\n\t\tcwe: 117,\n\t\tprimaryControl: LOG_CONTROL,\n\t\tpattern: /[\\r\\n]\\s{0,8}[[{(]?\\s{0,8}(?:INFO|WARN|WARNING|ERROR|DEBUG|FATAL|TRACE|CRITICAL)\\b/i,\n\t},\n\t{\n\t\tid: \"LOG-ANSI-ESCAPE-001\",\n\t\ttype: \"log_injection\",\n\t\tcontexts: [\"log\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"ANSI/terminal escape sequence in logged content\",\n\t\tcwe: 117,\n\t\tprimaryControl:\n\t\t\t\"Encode with encodeLogValue, or strip with stripAnsi when the rendering matters more than the record\",\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI sequences are defined by the ESC control character\n\t\tpattern: /\\u001b\\[[0-9;?]{0,32}[\\x40-\\x7e]/,\n\t},\n];\n\n//#endregion\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ import { ThreatFinding, ThreatPolicy, ThreatType, ThreatVerdict } from "./types.
6
6
  * @param finding - The finding to weigh.
7
7
  * @returns A non-negative score contribution.
8
8
  */
9
- declare function scoreForFinding(finding: ThreatFinding): number;
9
+ export declare function scoreForFinding(finding: ThreatFinding): number;
10
10
  /**
11
11
  * Total anomaly score for a set of findings.
12
12
  *
@@ -24,7 +24,7 @@ declare function scoreForFinding(finding: ThreatFinding): number;
24
24
  * calculateThreatScore(findings); // 7
25
25
  * ```
26
26
  */
27
- declare function calculateThreatScore(findings: readonly ThreatFinding[]): number;
27
+ export declare function calculateThreatScore(findings: readonly ThreatFinding[]): number;
28
28
  /**
29
29
  * Map a score onto a policy band.
30
30
  *
@@ -33,9 +33,9 @@ declare function calculateThreatScore(findings: readonly ThreatFinding[]): numbe
33
33
  * at 4, `block` at 8.
34
34
  * @returns `"allow"`, `"review"`, or `"block"`.
35
35
  */
36
- declare function verdictForScore(score: number, policy?: ThreatPolicy): ThreatVerdict;
36
+ export declare function verdictForScore(score: number, policy?: ThreatPolicy): ThreatVerdict;
37
37
  /** One category's contribution to a scan result. */
38
- interface ThreatTypeSummary {
38
+ export interface ThreatTypeSummary {
39
39
  /** The weakness category. */
40
40
  readonly type: ThreatType;
41
41
  /** How many findings carried this category. */
@@ -53,7 +53,6 @@ interface ThreatTypeSummary {
53
53
  * @returns Category totals sorted by descending score, then by category name so ties
54
54
  * are ordered deterministically.
55
55
  */
56
- declare function summarizeByType(findings: readonly ThreatFinding[]): readonly ThreatTypeSummary[];
56
+ export declare function summarizeByType(findings: readonly ThreatFinding[]): readonly ThreatTypeSummary[];
57
57
  //#endregion
58
- export { ThreatTypeSummary, calculateThreatScore, scoreForFinding, summarizeByType, verdictForScore };
59
58
  //# sourceMappingURL=scoring.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scoring.d.mts","names":[],"sources":["../../src/threats/scoring.ts"],"mappings":";;;;;;;;iBAgDgB,gBAAgB,SAAS;;;;;;;;;;;;;;;;;;iBAqBzB,qBAAqB,mBAAmB;;;;;;;;;iBA8BxC,gBACf,eACA,SAAQ,eACN;;UAOc;;WAEP,MAAM;;WAEN;;WAEA;;;;;;;;;;;;iBAaM,gBAAgB,mBAAmB,2BAA2B"}
1
+ {"version":3,"file":"scoring.d.mts","names":[],"sources":["../../src/threats/scoring.ts"],"mappings":";;;;;;;;wBAiDgB,gBAAgB,SAAS;;;;;;;;;;;;;;;;;;wBAqBzB,qBAAqB,mBAAmB;;;;;;;;;wBA8BxC,gBACf,eACA,SAAQ,eACN;;iBAOc;;WAEP,MAAM;;WAEN;;WAEA;;;;;;;;;;;;wBAaM,gBAAgB,mBAAmB,2BAA2B"}
@@ -2,6 +2,7 @@ import { CONFIDENCE_MULTIPLIERS, DEFAULT_THREAT_POLICY, SEVERITY_WEIGHTS } from
2
2
  //#region src/threats/scoring.ts
3
3
  /**
4
4
  * Copyright 2026 ResQ Systems, Inc.
5
+ * SPDX-License-Identifier: Apache-2.0
5
6
  *
6
7
  * Licensed under the Apache License, Version 2.0 (the "License");
7
8
  * you may not use this file except in compliance with the License.
@@ -1 +1 @@
1
- {"version":3,"file":"scoring.mjs","names":[],"sources":["../../src/threats/scoring.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 Anomaly scoring — turns a list of findings into a number and a\n * policy verdict.\n *\n * The point of scoring is that no single signature is trustworthy enough to reject a\n * request on its own. A lone low-confidence hit should raise a signal; a cluster of\n * high-confidence hits should stop the request. This is the model OWASP CRS uses —\n * anomaly score plus tunable thresholds — rather than one-rule-one-block, and it is\n * what makes per-route tuning possible: an operator raises `blockAt` for a field that\n * legitimately carries code samples instead of disabling detection wholesale.\n *\n * @module @resq-systems/security/threats/scoring\n */\n\nimport {\n\tCONFIDENCE_MULTIPLIERS,\n\tDEFAULT_THREAT_POLICY,\n\tSEVERITY_WEIGHTS,\n\ttype ThreatFinding,\n\ttype ThreatPolicy,\n\ttype ThreatType,\n\ttype ThreatVerdict,\n} from \"./types.js\";\n\n//#region Scoring\n\n/**\n * Points a single finding contributes: severity weight scaled by confidence.\n *\n * @param finding - The finding to weigh.\n * @returns A non-negative score contribution.\n */\nexport function scoreForFinding(finding: ThreatFinding): number {\n\treturn SEVERITY_WEIGHTS[finding.severity] * CONFIDENCE_MULTIPLIERS[finding.confidence];\n}\n\n/**\n * Total anomaly score for a set of findings.\n *\n * Each **rule** counts once no matter how many variants it matched in. Without that\n * collapse a payload written as `%3Cscript%3E` would score twice — once for `raw` and\n * once for `percent_decoded` — penalizing an input for being encoded rather than for\n * what it contains.\n *\n * @param findings - Findings from one scan.\n * @returns The summed score; `0` for an empty list.\n *\n * @example\n * ```ts\n * // one high/high finding (4 × 1.5) plus one medium/low finding (2 × 0.5)\n * calculateThreatScore(findings); // 7\n * ```\n */\nexport function calculateThreatScore(findings: readonly ThreatFinding[]): number {\n\tconst bestPerRule = new Map<string, number>();\n\n\tfor (const finding of findings) {\n\t\tconst score = scoreForFinding(finding);\n\t\tconst existing = bestPerRule.get(finding.ruleId);\n\t\tif (existing === undefined || score > existing) {\n\t\t\tbestPerRule.set(finding.ruleId, score);\n\t\t}\n\t}\n\n\tlet total = 0;\n\tfor (const score of bestPerRule.values()) {\n\t\ttotal += score;\n\t}\n\treturn total;\n}\n\n//#endregion\n\n//#region Policy\n\n/**\n * Map a score onto a policy band.\n *\n * @param score - Output of {@link calculateThreatScore}.\n * @param policy - Thresholds. Defaults to {@link DEFAULT_THREAT_POLICY} — `review`\n * at 4, `block` at 8.\n * @returns `\"allow\"`, `\"review\"`, or `\"block\"`.\n */\nexport function verdictForScore(\n\tscore: number,\n\tpolicy: ThreatPolicy = DEFAULT_THREAT_POLICY,\n): ThreatVerdict {\n\tif (score >= policy.blockAt) return \"block\";\n\tif (score >= policy.reviewAt) return \"review\";\n\treturn \"allow\";\n}\n\n/** One category's contribution to a scan result. */\nexport interface ThreatTypeSummary {\n\t/** The weakness category. */\n\treadonly type: ThreatType;\n\t/** How many findings carried this category. */\n\treadonly count: number;\n\t/** Summed score contribution, before per-rule deduplication. */\n\treadonly score: number;\n}\n\n/**\n * Summarize findings by weakness category, highest score first.\n *\n * Useful for dashboards and alert routing, where \"three SQL findings and one XSS\n * finding\" is more actionable than a flat list.\n *\n * @param findings - Findings from one scan.\n * @returns Category totals sorted by descending score, then by category name so ties\n * are ordered deterministically.\n */\nexport function summarizeByType(findings: readonly ThreatFinding[]): readonly ThreatTypeSummary[] {\n\tconst totals = new Map<ThreatType, { count: number; score: number }>();\n\n\tfor (const finding of findings) {\n\t\tconst entry = totals.get(finding.type) ?? { count: 0, score: 0 };\n\t\tentry.count += 1;\n\t\tentry.score += scoreForFinding(finding);\n\t\ttotals.set(finding.type, entry);\n\t}\n\n\treturn [...totals.entries()]\n\t\t.map(([type, entry]) => ({ type, count: entry.count, score: entry.score }))\n\t\t.sort((a, b) => b.score - a.score || a.type.localeCompare(b.type));\n}\n\n//#endregion\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,gBAAgB,SAAgC;CAC/D,OAAO,iBAAiB,QAAQ,YAAY,uBAAuB,QAAQ;AAC5E;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,qBAAqB,UAA4C;CAChF,MAAM,8BAAc,IAAI,IAAoB;CAE5C,KAAK,MAAM,WAAW,UAAU;EAC/B,MAAM,QAAQ,gBAAgB,OAAO;EACrC,MAAM,WAAW,YAAY,IAAI,QAAQ,MAAM;EAC/C,IAAI,aAAa,KAAA,KAAa,QAAQ,UACrC,YAAY,IAAI,QAAQ,QAAQ,KAAK;CAEvC;CAEA,IAAI,QAAQ;CACZ,KAAK,MAAM,SAAS,YAAY,OAAO,GACtC,SAAS;CAEV,OAAO;AACR;;;;;;;;;AAcA,SAAgB,gBACf,OACA,SAAuB,uBACP;CAChB,IAAI,SAAS,OAAO,SAAS,OAAO;CACpC,IAAI,SAAS,OAAO,UAAU,OAAO;CACrC,OAAO;AACR;;;;;;;;;;;AAsBA,SAAgB,gBAAgB,UAAkE;CACjG,MAAM,yBAAS,IAAI,IAAkD;CAErE,KAAK,MAAM,WAAW,UAAU;EAC/B,MAAM,QAAQ,OAAO,IAAI,QAAQ,IAAI,KAAK;GAAE,OAAO;GAAG,OAAO;EAAE;EAC/D,MAAM,SAAS;EACf,MAAM,SAAS,gBAAgB,OAAO;EACtC,OAAO,IAAI,QAAQ,MAAM,KAAK;CAC/B;CAEA,OAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,CAAC,CAC1B,KAAK,CAAC,MAAM,YAAY;EAAE;EAAM,OAAO,MAAM;EAAO,OAAO,MAAM;CAAM,EAAE,CAAC,CAC1E,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AACnE"}
1
+ {"version":3,"file":"scoring.mjs","names":[],"sources":["../../src/threats/scoring.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 Anomaly scoring — turns a list of findings into a number and a\n * policy verdict.\n *\n * The point of scoring is that no single signature is trustworthy enough to reject a\n * request on its own. A lone low-confidence hit should raise a signal; a cluster of\n * high-confidence hits should stop the request. This is the model OWASP CRS uses —\n * anomaly score plus tunable thresholds — rather than one-rule-one-block, and it is\n * what makes per-route tuning possible: an operator raises `blockAt` for a field that\n * legitimately carries code samples instead of disabling detection wholesale.\n *\n * @module @resq-systems/security/threats/scoring\n */\n\nimport {\n\tCONFIDENCE_MULTIPLIERS,\n\tDEFAULT_THREAT_POLICY,\n\tSEVERITY_WEIGHTS,\n\ttype ThreatFinding,\n\ttype ThreatPolicy,\n\ttype ThreatType,\n\ttype ThreatVerdict,\n} from \"./types.js\";\n\n//#region Scoring\n\n/**\n * Points a single finding contributes: severity weight scaled by confidence.\n *\n * @param finding - The finding to weigh.\n * @returns A non-negative score contribution.\n */\nexport function scoreForFinding(finding: ThreatFinding): number {\n\treturn SEVERITY_WEIGHTS[finding.severity] * CONFIDENCE_MULTIPLIERS[finding.confidence];\n}\n\n/**\n * Total anomaly score for a set of findings.\n *\n * Each **rule** counts once no matter how many variants it matched in. Without that\n * collapse a payload written as `%3Cscript%3E` would score twice — once for `raw` and\n * once for `percent_decoded` — penalizing an input for being encoded rather than for\n * what it contains.\n *\n * @param findings - Findings from one scan.\n * @returns The summed score; `0` for an empty list.\n *\n * @example\n * ```ts\n * // one high/high finding (4 × 1.5) plus one medium/low finding (2 × 0.5)\n * calculateThreatScore(findings); // 7\n * ```\n */\nexport function calculateThreatScore(findings: readonly ThreatFinding[]): number {\n\tconst bestPerRule = new Map<string, number>();\n\n\tfor (const finding of findings) {\n\t\tconst score = scoreForFinding(finding);\n\t\tconst existing = bestPerRule.get(finding.ruleId);\n\t\tif (existing === undefined || score > existing) {\n\t\t\tbestPerRule.set(finding.ruleId, score);\n\t\t}\n\t}\n\n\tlet total = 0;\n\tfor (const score of bestPerRule.values()) {\n\t\ttotal += score;\n\t}\n\treturn total;\n}\n\n//#endregion\n\n//#region Policy\n\n/**\n * Map a score onto a policy band.\n *\n * @param score - Output of {@link calculateThreatScore}.\n * @param policy - Thresholds. Defaults to {@link DEFAULT_THREAT_POLICY} — `review`\n * at 4, `block` at 8.\n * @returns `\"allow\"`, `\"review\"`, or `\"block\"`.\n */\nexport function verdictForScore(\n\tscore: number,\n\tpolicy: ThreatPolicy = DEFAULT_THREAT_POLICY,\n): ThreatVerdict {\n\tif (score >= policy.blockAt) return \"block\";\n\tif (score >= policy.reviewAt) return \"review\";\n\treturn \"allow\";\n}\n\n/** One category's contribution to a scan result. */\nexport interface ThreatTypeSummary {\n\t/** The weakness category. */\n\treadonly type: ThreatType;\n\t/** How many findings carried this category. */\n\treadonly count: number;\n\t/** Summed score contribution, before per-rule deduplication. */\n\treadonly score: number;\n}\n\n/**\n * Summarize findings by weakness category, highest score first.\n *\n * Useful for dashboards and alert routing, where \"three SQL findings and one XSS\n * finding\" is more actionable than a flat list.\n *\n * @param findings - Findings from one scan.\n * @returns Category totals sorted by descending score, then by category name so ties\n * are ordered deterministically.\n */\nexport function summarizeByType(findings: readonly ThreatFinding[]): readonly ThreatTypeSummary[] {\n\tconst totals = new Map<ThreatType, { count: number; score: number }>();\n\n\tfor (const finding of findings) {\n\t\tconst entry = totals.get(finding.type) ?? { count: 0, score: 0 };\n\t\tentry.count += 1;\n\t\tentry.score += scoreForFinding(finding);\n\t\ttotals.set(finding.type, entry);\n\t}\n\n\treturn [...totals.entries()]\n\t\t.map(([type, entry]) => ({ type, count: entry.count, score: entry.score }))\n\t\t.sort((a, b) => b.score - a.score || a.type.localeCompare(b.type));\n}\n\n//#endregion\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,gBAAgB,SAAgC;CAC/D,OAAO,iBAAiB,QAAQ,YAAY,uBAAuB,QAAQ;AAC5E;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,qBAAqB,UAA4C;CAChF,MAAM,8BAAc,IAAI,IAAoB;CAE5C,KAAK,MAAM,WAAW,UAAU;EAC/B,MAAM,QAAQ,gBAAgB,OAAO;EACrC,MAAM,WAAW,YAAY,IAAI,QAAQ,MAAM;EAC/C,IAAI,aAAa,KAAA,KAAa,QAAQ,UACrC,YAAY,IAAI,QAAQ,QAAQ,KAAK;CAEvC;CAEA,IAAI,QAAQ;CACZ,KAAK,MAAM,SAAS,YAAY,OAAO,GACtC,SAAS;CAEV,OAAO;AACR;;;;;;;;;AAcA,SAAgB,gBACf,OACA,SAAuB,uBACP;CAChB,IAAI,SAAS,OAAO,SAAS,OAAO;CACpC,IAAI,SAAS,OAAO,UAAU,OAAO;CACrC,OAAO;AACR;;;;;;;;;;;AAsBA,SAAgB,gBAAgB,UAAkE;CACjG,MAAM,yBAAS,IAAI,IAAkD;CAErE,KAAK,MAAM,WAAW,UAAU;EAC/B,MAAM,QAAQ,OAAO,IAAI,QAAQ,IAAI,KAAK;GAAE,OAAO;GAAG,OAAO;EAAE;EAC/D,MAAM,SAAS;EACf,MAAM,SAAS,gBAAgB,OAAO;EACtC,OAAO,IAAI,QAAQ,MAAM,KAAK;CAC/B;CAEA,OAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,CAAC,CAC1B,KAAK,CAAC,MAAM,YAAY;EAAE;EAAM,OAAO,MAAM;EAAO,OAAO,MAAM;CAAM,EAAE,CAAC,CAC1E,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AACnE"}
@@ -1,6 +1,7 @@
1
1
  //#region src/threats/types.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.
@@ -39,7 +40,7 @@
39
40
  * Categories follow the OWASP Web Security Testing Guide's input-validation chapter
40
41
  * so findings map onto an established taxonomy rather than package-local names.
41
42
  */
42
- type ThreatType = "xss" | "sql_injection" | "nosql_injection" | "command_injection" | "path_traversal" | "prototype_pollution" | "homoglyph" | "header_injection" | "ldap_injection" | "xpath_injection" | "xml_injection" | "template_injection" | "file_inclusion" | "ssrf" | "formula_injection" | "log_injection" | "prompt_injection" | "parameter_pollution" | "credential_exposure" | "jwt_tampering" | "double_encoding" | "resource_abuse";
43
+ export type ThreatType = "xss" | "sql_injection" | "nosql_injection" | "command_injection" | "path_traversal" | "prototype_pollution" | "homoglyph" | "header_injection" | "ldap_injection" | "xpath_injection" | "xml_injection" | "template_injection" | "file_inclusion" | "ssrf" | "formula_injection" | "log_injection" | "prompt_injection" | "parameter_pollution" | "credential_exposure" | "jwt_tampering" | "double_encoding" | "resource_abuse";
43
44
  /**
44
45
  * The sink a value is destined for. Rules declare which contexts they apply to, and
45
46
  * the scanner only evaluates rules matching the caller's declared contexts.
@@ -78,14 +79,14 @@ type ThreatType = "xss" | "sql_injection" | "nosql_injection" | "command_injecti
78
79
  * - `log` — written to a line-based or structured log sink.
79
80
  * - `llm_prompt` — concatenated into an LLM prompt or reachable by a tool-using agent.
80
81
  */
81
- type ThreatContext = "general_text" | "html" | "sql" | "nosql" | "shell" | "filesystem" | "url" | "url_parameter" | "http_header" | "jwt" | "identifier" | "object_merge" | "template" | "xml" | "ldap" | "xpath" | "spreadsheet" | "log" | "llm_prompt";
82
+ export type ThreatContext = "general_text" | "html" | "sql" | "nosql" | "shell" | "filesystem" | "url" | "url_parameter" | "http_header" | "jwt" | "identifier" | "object_merge" | "template" | "xml" | "ldap" | "xpath" | "spreadsheet" | "log" | "llm_prompt";
82
83
  /** Every {@link ThreatContext} value, for iteration and "scan every sink" callers. */
83
- declare const ALL_THREAT_CONTEXTS: readonly ["general_text", "html", "sql", "nosql", "shell", "filesystem", "url", "url_parameter", "http_header", "jwt", "identifier", "object_merge", "template", "xml", "ldap", "xpath", "spreadsheet", "log", "llm_prompt"];
84
+ export declare const ALL_THREAT_CONTEXTS: readonly ["general_text", "html", "sql", "nosql", "shell", "filesystem", "url", "url_parameter", "http_header", "jwt", "identifier", "object_merge", "template", "xml", "ldap", "xpath", "spreadsheet", "log", "llm_prompt"];
84
85
  /**
85
86
  * How bad the weakness is if the match is a true positive. Independent of how
86
87
  * likely the match is to *be* a true positive — that is {@link ThreatConfidence}.
87
88
  */
88
- type ThreatSeverity = "low" | "medium" | "high" | "critical";
89
+ export type ThreatSeverity = "low" | "medium" | "high" | "critical";
89
90
  /**
90
91
  * How likely a match is to be a real attack rather than benign content that
91
92
  * happens to look like one.
@@ -94,13 +95,13 @@ type ThreatSeverity = "low" | "medium" | "high" | "critical";
94
95
  * - `medium` — unusual in ordinary content but not impossible.
95
96
  * - `high` — has essentially no benign explanation in the declared context.
96
97
  */
97
- type ThreatConfidence = "low" | "medium" | "high";
98
+ export type ThreatConfidence = "low" | "medium" | "high";
98
99
  /** Base anomaly points contributed by a finding, before the confidence multiplier. */
99
- declare const SEVERITY_WEIGHTS: Readonly<Record<ThreatSeverity, number>>;
100
+ export declare const SEVERITY_WEIGHTS: Readonly<Record<ThreatSeverity, number>>;
100
101
  /** Scales {@link SEVERITY_WEIGHTS} by how trustworthy the signature is. */
101
- declare const CONFIDENCE_MULTIPLIERS: Readonly<Record<ThreatConfidence, number>>;
102
+ export declare const CONFIDENCE_MULTIPLIERS: Readonly<Record<ThreatConfidence, number>>;
102
103
  /** Ordering helper for `minSeverity` filtering. Higher is worse. */
103
- declare const SEVERITY_ORDER: Readonly<Record<ThreatSeverity, number>>;
104
+ export declare const SEVERITY_ORDER: Readonly<Record<ThreatSeverity, number>>;
104
105
  /**
105
106
  * Which representation of the input a rule matched against.
106
107
  *
@@ -110,9 +111,9 @@ declare const SEVERITY_ORDER: Readonly<Record<ThreatSeverity, number>>;
110
111
  * `../`" apart from "the request contained `%2e%2e%2f`, which only becomes `../` if
111
112
  * a downstream component decodes it".
112
113
  */
113
- type InputVariantKind = "raw" | "nfc" | "nfkc" | "percent_decoded" | "html_decoded";
114
+ export type InputVariantKind = "raw" | "nfc" | "nfkc" | "percent_decoded" | "html_decoded";
114
115
  /** One representation of the scanned input. */
115
- interface InputVariant {
116
+ export interface InputVariant {
116
117
  /** Which transformation produced {@link InputVariant.value}. */
117
118
  readonly kind: InputVariantKind;
118
119
  /** The transformed string. */
@@ -127,7 +128,7 @@ interface InputVariant {
127
128
  * and shared across every call — a stateful rule would silently skip matches on
128
129
  * alternating invocations. `assertRuleCatalogIsValid` enforces this at load time.
129
130
  */
130
- interface ThreatRule {
131
+ export interface ThreatRule {
131
132
  /** Stable identifier (e.g. `PATH-TRAVERSAL-ENCODED-001`). Never reused once retired. */
132
133
  readonly id: string;
133
134
  /** Weakness category this rule evidences. */
@@ -158,7 +159,7 @@ interface ThreatRule {
158
159
  readonly variants?: readonly InputVariantKind[];
159
160
  }
160
161
  /** A rule that matched, plus where and in which representation. */
161
- interface ThreatFinding {
162
+ export interface ThreatFinding {
162
163
  /** {@link ThreatRule.id} of the rule that fired. */
163
164
  readonly ruleId: string;
164
165
  /** Copied from the rule — the weakness category. */
@@ -193,7 +194,7 @@ interface ThreatFinding {
193
194
  * bound to a SQL query" is a bug report, while "SQL keywords arriving in a query
194
195
  * parameter from one account, forty times in a minute" is an incident.
195
196
  */
196
- type InputSource = "http.query" | "http.body" | "http.header" | "http.path" | "http.cookie" | "websocket.message" | "cli.argument" | "file.upload" | "database" | "external.api" | "internal";
197
+ export type InputSource = "http.query" | "http.body" | "http.header" | "http.path" | "http.cookie" | "websocket.message" | "cli.argument" | "file.upload" | "database" | "external.api" | "internal";
197
198
  /**
198
199
  * Who and what a scan belongs to, carried through so findings can be correlated.
199
200
  *
@@ -212,7 +213,7 @@ type InputSource = "http.query" | "http.body" | "http.header" | "http.path" | "h
212
213
  * an opaque identifier rather than an email address, or redact downstream with
213
214
  * `sanitizeForLogging`.
214
215
  */
215
- interface EventContext {
216
+ export interface EventContext {
216
217
  /** Correlates every finding raised while handling one request. */
217
218
  readonly requestId?: string;
218
219
  /** The authenticated principal, if any. Opaque — never an email address. */
@@ -230,16 +231,15 @@ interface EventContext {
230
231
  * not reject a form submission. This mirrors how OWASP CRS uses anomaly scoring with
231
232
  * tunable thresholds instead of one-rule-one-block.
232
233
  */
233
- type ThreatVerdict = "allow" | "review" | "block";
234
+ export type ThreatVerdict = "allow" | "review" | "block";
234
235
  /** Score thresholds separating {@link ThreatVerdict} bands. */
235
- interface ThreatPolicy {
236
+ export interface ThreatPolicy {
236
237
  /** Score at or above which the verdict becomes `review`. Default `4`. */
237
238
  readonly reviewAt: number;
238
239
  /** Score at or above which the verdict becomes `block`. Default `8`. */
239
240
  readonly blockAt: number;
240
241
  }
241
242
  /** Default thresholds: one high/high finding reviews, one critical finding blocks. */
242
- declare const DEFAULT_THREAT_POLICY: ThreatPolicy;
243
+ export declare const DEFAULT_THREAT_POLICY: ThreatPolicy;
243
244
  //#endregion
244
- export { ALL_THREAT_CONTEXTS, CONFIDENCE_MULTIPLIERS, DEFAULT_THREAT_POLICY, EventContext, InputSource, InputVariant, InputVariantKind, SEVERITY_ORDER, SEVERITY_WEIGHTS, ThreatConfidence, ThreatContext, ThreatFinding, ThreatPolicy, ThreatRule, ThreatSeverity, ThreatType, ThreatVerdict };
245
245
  //# sourceMappingURL=types.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../src/threats/types.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA4CY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA8DA;;cAsBC;;;;;KA8BD;;;;;;;;;KAUA;;cAGC,kBAAkB,SAAS,OAAO;;cAQlC,wBAAwB,SAAS,OAAO;;cAOxC,gBAAgB,SAAS,OAAO;;;;;;;;;;KAoBjC;;UAGK;;WAEP,MAAM;;WAEN;;;;;;;;;;;UAgBO;;WAEP;;WAEA,MAAM;;WAEN,mBAAmB;;WAEnB,UAAU;;WAEV,YAAY;;WAEZ;;WAEA;;;;;;WAMA;;WAEA,SAAS;;;;;;WAMT,oBAAoB;;;UAIb;;WAEP;;WAEA,MAAM;;WAEN,UAAU;;WAEV,YAAY;;WAEZ;;WAEA;;WAEA;;WAEA,SAAS;;WAET;;WAEA;;WAEA;;;;;;;;;;;;;KAkBE;;;;;;;;;;;;;;;;;;;UA+BK;;WAEP;;WAEA;;WAEA;;WAEA,SAAS;;;;;;;;;;KAeP;;UAGK;;WAEP;;WAEA;;;cAIG,uBAAuB"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../src/threats/types.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YA6CY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YA8DA;;qBAsBC;;;;;YA8BD;;;;;;;;;YAUA;;qBAGC,kBAAkB,SAAS,OAAO;;qBAQlC,wBAAwB,SAAS,OAAO;;qBAOxC,gBAAgB,SAAS,OAAO;;;;;;;;;;YAoBjC;;iBAGK;;WAEP,MAAM;;WAEN;;;;;;;;;;;iBAgBO;;WAEP;;WAEA,MAAM;;WAEN,mBAAmB;;WAEnB,UAAU;;WAEV,YAAY;;WAEZ;;WAEA;;;;;;WAMA;;WAEA,SAAS;;;;;;WAMT,oBAAoB;;;iBAIb;;WAEP;;WAEA,MAAM;;WAEN,UAAU;;WAEV,YAAY;;WAEZ;;WAEA;;WAEA;;WAEA,SAAS;;WAET;;WAEA;;WAEA;;;;;;;;;;;;;YAkBE;;;;;;;;;;;;;;;;;;;iBA+BK;;WAEP;;WAEA;;WAEA;;WAEA,SAAS;;;;;;;;;;YAeP;;iBAGK;;WAEP;;WAEA;;;qBAIG,uBAAuB"}