@resq-systems/security 2.1.0 → 2.1.2

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 (91) 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 +8 -8
  17. package/lib/controls/query.d.mts.map +1 -1
  18. package/lib/controls/query.mjs +1 -0
  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 +2 -1
  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 +4 -4
  64. package/lib/threats/rules/system.mjs.map +1 -1
  65. package/lib/threats/rules/web.d.mts +5 -6
  66. package/lib/threats/rules/web.d.mts.map +1 -1
  67. package/lib/threats/rules/web.mjs +1 -1
  68. package/lib/threats/rules/web.mjs.map +1 -1
  69. package/lib/threats/scoring.d.mts +5 -6
  70. package/lib/threats/scoring.d.mts.map +1 -1
  71. package/lib/threats/scoring.mjs +1 -0
  72. package/lib/threats/scoring.mjs.map +1 -1
  73. package/lib/threats/types.d.mts +18 -18
  74. package/lib/threats/types.d.mts.map +1 -1
  75. package/lib/threats/types.mjs.map +1 -1
  76. package/lib/threats/variants.d.mts +3 -4
  77. package/lib/threats/variants.d.mts.map +1 -1
  78. package/lib/threats/variants.mjs.map +1 -1
  79. package/lib/unicode/confusables.d.mts +5 -5
  80. package/lib/unicode/confusables.d.mts.map +1 -1
  81. package/lib/unicode/confusables.mjs +1 -0
  82. package/lib/unicode/confusables.mjs.map +1 -1
  83. package/lib/unicode/index.d.mts +11 -11
  84. package/lib/unicode/index.d.mts.map +1 -1
  85. package/lib/unicode/index.mjs +1 -0
  86. package/lib/unicode/index.mjs.map +1 -1
  87. package/lib/validators.d.mts +89 -39
  88. package/lib/validators.d.mts.map +1 -1
  89. package/lib/validators.mjs +285 -21
  90. package/lib/validators.mjs.map +1 -1
  91. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"datastore.mjs","names":[],"sources":["../../../src/threats/rules/datastore.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 Query-language signatures: SQL, NoSQL/document-store operators,\n * LDAP filters, and XPath expressions.\n *\n * These are the rules most prone to false positives, because ordinary English and\n * ordinary source code contain SQL keywords constantly. Two mitigations apply:\n *\n * 1. Every rule is scoped to a query context, so none of them runs against a bio, a\n * support ticket, or a search box whose value is bound as a parameter.\n * 2. Tautology rules require a quote or paren adjacent to the comparison, so the\n * bare substring `1=1` — which occurs in perfectly ordinary arithmetic — no\n * longer fires on its own. The previous catalog matched `/1\\s*=\\s*1/` outright.\n *\n * Detection here is telemetry. Parameterized queries are the control: a bound\n * parameter is safe no matter which keywords it contains, and an interpolated one is\n * unsafe no matter how many signatures it dodges.\n *\n * @module @resq-systems/security/threats/rules/datastore\n */\n\nimport type { ThreatRule } from \"../types.js\";\n\n//#region SQL\n\n/** Repeated across every SQL rule. */\nconst SQL_CONTROL = \"Bind values as query parameters; never concatenate SQL strings\";\n\n/** SQL-injection signatures. Scoped to the `sql` context. */\nexport const SQL_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"SQL-UNION-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"UNION SELECT — classic result-set grafting\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\bUNION\\s+(?:ALL\\s+)?SELECT\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-DROP-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"DROP of a schema object\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\bDROP\\s+(?:TABLE|DATABASE|SCHEMA|INDEX|VIEW|FUNCTION|PROCEDURE)\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-DELETE-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"DELETE FROM in a value position\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\bDELETE\\s+FROM\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-TRUNCATE-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"TRUNCATE TABLE in a value position\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\bTRUNCATE\\s+TABLE\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-STACKED-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Statement separator followed by a new statement\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /;\\s{0,8}(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|GRANT|EXEC|UNION)\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-TAUTOLOGY-QUOTED-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Quote-escaped always-true comparison\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /['\"]\\s{0,8}(?:OR|AND)\\s+['\"]?\\w{1,20}['\"]?\\s{0,8}=\\s{0,8}['\"]?\\w{1,20}/i,\n\t},\n\t{\n\t\tid: \"SQL-TAUTOLOGY-NUMERIC-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Quote- or paren-escaped numeric always-true comparison\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// Requires the escape character. A bare `1=1` is ordinary arithmetic and is\n\t\t// deliberately *not* matched — that was the old catalog's worst false positive.\n\t\tpattern: /['\")]\\s{0,8}(?:OR|AND)\\s+\\d{1,10}\\s{0,8}=\\s{0,8}\\d{1,10}/i,\n\t},\n\t{\n\t\tid: \"SQL-COMMENT-TERMINATOR-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Trailing SQL comment, used to truncate the rest of a statement\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// The comment must follow a quote, paren, or digit — the position it occupies\n\t\t// when it truncates an injected statement. A bare `(?:--|#)…$` fires on CSS\n\t\t// colours (`#ff00aa`), issue references (`#123`), and em-dash-style prose.\n\t\tpattern: /['\")\\d]\\s{0,8}(?:--|#)[^\\r\\n]{0,64}$/,\n\t},\n\t{\n\t\t// sqlmap ships this as its `versionedkeywords` tamper: MySQL executes the body of\n\t\t// a versioned comment while every other engine ignores it, so wrapping each\n\t\t// keyword hides the statement from a scanner looking for bare keywords. A value\n\t\t// reaching a SQL sink has no legitimate reason to contain one.\n\t\tid: \"SQL-VERSIONED-COMMENT-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"MySQL versioned comment, whose contents execute on MySQL only\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\/\\*!\\d{0,5}/,\n\t},\n\t{\n\t\tid: \"SQL-COMMENT-INLINE-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Inline block comment, used to split keywords past naive filters\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\/\\*[\\s\\S]{0,200}?\\*\\//,\n\t},\n\t{\n\t\tid: \"SQL-COMMENT-SPLIT-KEYWORD-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Inline comment separating two SQL keywords — keyword-filter evasion\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// `1 UNION/**/SELECT/**/1` defeats SQL-UNION-001, which requires whitespace\n\t\t// between the keywords, and previously scored only 1.0 from the generic\n\t\t// inline-comment rule — an `allow` verdict on a working injection. A comment\n\t\t// *between two keywords* has no benign explanation in a bound value, so this is\n\t\t// graded high/high where the bare-comment rule stays low/low.\n\t\tpattern:\n\t\t\t/\\b(?:UNION|SELECT|INSERT|UPDATE|DELETE|DROP|FROM|WHERE|ORDER|GROUP|HAVING|AND|OR)\\s{0,8}\\/\\*[^*]{0,64}\\*\\/\\s{0,8}(?:UNION|SELECT|ALL|DISTINCT|FROM|WHERE|INTO|TABLE|BY|\\d|['\"])/i,\n\t},\n\t{\n\t\tid: \"SQL-TIME-BLIND-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Time-delay function used for blind injection\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\b(?:SLEEP|PG_SLEEP|BENCHMARK)\\s{0,8}\\(|\\bWAITFOR\\s+DELAY\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-METADATA-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Access to database metadata catalogs\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:INFORMATION_SCHEMA|pg_catalog|sysobjects|syscolumns|sqlite_master|all_tables)\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-XP-PROC-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"SQL Server extended procedure enabling OS access\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\b(?:xp_cmdshell|xp_regread|xp_regwrite|xp_dirtree|sp_oacreate)\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-FILE-IO-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"SQL file read/write primitive\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern:\n\t\t\t/\\bINTO\\s+(?:OUT|DUMP)FILE\\b|\\bLOAD_FILE\\s{0,8}\\(|\\bCOPY\\s+\\w{1,64}\\s+FROM\\s+PROGRAM\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-ENGINE-FILE-IO-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Engine-specific file read/write primitive\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// SQL-FILE-IO-001 covers only the MySQL forms. Every primitive below scanned\n\t\t// clean unless the payload happened to carry a `;`, in which case\n\t\t// SQL-STACKED-001 caught it incidentally — remove the semicolon and each was\n\t\t// 0/allow.\n\t\tpattern:\n\t\t\t/\\bpg_(?:read_file|read_binary_file|ls_dir|stat_file)\\s{0,8}\\(|\\blo_(?:import|export)\\s{0,8}\\(|\\bOPENROWSET\\s{0,8}\\(|\\bOPENDATASOURCE\\s{0,8}\\(|\\bATTACH\\s+DATABASE\\b|\\bUTL_FILE\\s{0,8}\\./i,\n\t},\n\t{\n\t\tid: \"SQL-OUT-OF-BAND-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Out-of-band exfiltration or network primitive\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// Oracle's network packages and Postgres dblink are the standard out-of-band\n\t\t// channels for blind injection: they make the database itself perform the\n\t\t// exfiltration, so no data need come back through the response.\n\t\tpattern:\n\t\t\t/\\bUTL_HTTP\\s{0,8}\\.|\\bUTL_INADDR\\s{0,8}\\.|\\bUTL_SMTP\\s{0,8}\\.|\\bUTL_TCP\\s{0,8}\\.|\\bDBMS_LDAP\\s{0,8}\\.|\\bDBMS_PIPE\\s{0,8}\\.\\s{0,8}RECEIVE_MESSAGE\\b|\\bdblink(?:_connect|_exec)?\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SQL-HEX-LITERAL-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"low\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Long hex literal, sometimes used to smuggle string constants\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// Bounded at 8+ digits so ordinary colour codes (`0xFF00AA`) and short\n\t\t// constants do not fire. The old catalog used `/0x[0-9a-f]+/`, which matched\n\t\t// every CSS colour in the corpus.\n\t\tpattern: /\\b0x[0-9a-f]{8,}\\b/i,\n\t},\n];\n\n//#endregion\n\n//#region NoSQL\n\n/** Repeated across every NoSQL rule. */\nconst NOSQL_CONTROL =\n\t\"Cast query values to primitives and reject object-valued inputs; never pass raw request bodies to a query builder\";\n\n/** Document-store operator injection (MongoDB and compatible drivers). */\nexport const NOSQL_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"NOSQL-OPERATOR-OBJECT-001\",\n\t\ttype: \"nosql_injection\",\n\t\tcontexts: [\"nosql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Object literal whose first key is a query operator\",\n\t\tcwe: 943,\n\t\tprimaryControl: NOSQL_CONTROL,\n\t\tpattern: /\\{\\s{0,8}[\"']?\\$[a-z]{2,20}[\"']?\\s{0,8}:/i,\n\t},\n\t{\n\t\tid: \"NOSQL-JS-EXECUTION-001\",\n\t\ttype: \"nosql_injection\",\n\t\tcontexts: [\"nosql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Server-side JavaScript execution operator\",\n\t\tcwe: 943,\n\t\tprimaryControl: NOSQL_CONTROL,\n\t\tpattern: /\\$(?:where|function|accumulator)\\s{0,8}[\"']?\\s{0,8}:/i,\n\t},\n\t{\n\t\tid: \"NOSQL-OPERATOR-001\",\n\t\ttype: \"nosql_injection\",\n\t\tcontexts: [\"nosql\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Query operator token\",\n\t\tcwe: 943,\n\t\tprimaryControl: NOSQL_CONTROL,\n\t\tpattern:\n\t\t\t/\\$(?:gte?|lte?|ne|eq|nin?|and|or|not|nor|exists|type|mod|regex|text|all|elemMatch|size|slice|expr|jsonSchema)\\b/i,\n\t},\n\t{\n\t\tid: \"NOSQL-OPERATOR-ARRAY-001\",\n\t\ttype: \"nosql_injection\",\n\t\tcontexts: [\"nosql\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Array subscript naming a query operator\",\n\t\tcwe: 943,\n\t\tprimaryControl: NOSQL_CONTROL,\n\t\tpattern: /\\[\\s{0,8}[\"']?\\$[a-z]{2,20}[\"']?\\s{0,8}\\]/i,\n\t},\n];\n\n//#endregion\n\n//#region LDAP\n\n/** Repeated across every LDAP rule. */\nconst LDAP_CONTROL =\n\t\"Escape filter values per RFC 4515 and DN components per RFC 4514 using the LDAP client's escaping helper\";\n\n/** LDAP filter and DN injection. */\nexport const LDAP_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"LDAP-FILTER-WILDCARD-001\",\n\t\ttype: \"ldap_injection\",\n\t\tcontexts: [\"ldap\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Injected filter clause matching any value\",\n\t\tcwe: 90,\n\t\tprimaryControl: LDAP_CONTROL,\n\t\tpattern: /[()&|!]\\s{0,8}\\(\\s{0,8}[a-z0-9;.-]{1,64}\\s{0,8}=\\s{0,8}\\*/i,\n\t},\n\t{\n\t\tid: \"LDAP-FILTER-CLOSE-001\",\n\t\ttype: \"ldap_injection\",\n\t\tcontexts: [\"ldap\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Filter-terminating parenthesis followed by a boolean operator\",\n\t\tcwe: 90,\n\t\tprimaryControl: LDAP_CONTROL,\n\t\tpattern: /\\)\\s{0,8}[&|]\\s{0,8}\\(/,\n\t},\n\t{\n\t\tid: \"LDAP-METACHAR-001\",\n\t\ttype: \"ldap_injection\",\n\t\tcontexts: [\"ldap\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Unescaped LDAP filter metacharacter\",\n\t\tcwe: 90,\n\t\tprimaryControl: LDAP_CONTROL,\n\t\tpattern: /[*()\\\\]/,\n\t},\n\t{\n\t\tid: \"LDAP-DN-INJECTION-001\",\n\t\ttype: \"ldap_injection\",\n\t\tcontexts: [\"ldap\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"RDN separator in a value destined for a distinguished name\",\n\t\tcwe: 90,\n\t\tprimaryControl:\n\t\t\t\"Escape DN components per RFC 4514 using the LDAP client's DN escaping helper — filter escaping (RFC 4515) is a different function and does not cover these characters\",\n\t\t// The filter rules above cover RFC 4515 metacharacters (`*`, parens, backslash).\n\t\t// A DN uses a different grammar entirely: `,` and `+` separate relative\n\t\t// distinguished names, so `admin,ou=admins,dc=example,dc=com` re-parents the\n\t\t// entry. All three DN forms scanned 0/allow before this rule.\n\t\t// Confidence is medium because a common name can legitimately contain a comma\n\t\t// (\"Smith, John\") — which is exactly why it must be escaped, not rejected.\n\t\tpattern: /[,+]\\s{0,8}(?:cn|ou|dc|o|uid|sn|givenname|mail|member|objectclass)\\s{0,8}=/i,\n\t},\n\t{\n\t\tid: \"LDAP-DN-METACHAR-001\",\n\t\ttype: \"ldap_injection\",\n\t\tcontexts: [\"ldap\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Unescaped RFC 4514 distinguished-name metacharacter\",\n\t\tcwe: 90,\n\t\tprimaryControl: \"Escape DN components per RFC 4514 using the LDAP client's DN escaping helper\",\n\t\t// Leading/trailing space and `#` are also special in a DN, but flagging them\n\t\t// would fire on nearly every human name, so only the structural characters are\n\t\t// listed here.\n\t\tpattern: /[\"<>;]|^[+,]|[+,]$/,\n\t},\n];\n\n//#endregion\n\n//#region XPath\n\n/** Repeated across every XPath rule. */\nconst XPATH_CONTROL =\n\t\"Use a precompiled XPath with variable binding (XPathVariableResolver or equivalent); never concatenate expressions\";\n\n/** XPath/XQuery injection. */\nexport const XPATH_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"XPATH-TAUTOLOGY-001\",\n\t\ttype: \"xpath_injection\",\n\t\tcontexts: [\"xpath\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Quote-escaped always-true predicate\",\n\t\tcwe: 643,\n\t\tprimaryControl: XPATH_CONTROL,\n\t\tpattern: /['\"]\\s{0,8}(?:or|and)\\s+['\"]?\\w{1,20}['\"]?\\s{0,8}=\\s{0,8}['\"]?\\w{1,20}/i,\n\t},\n\t{\n\t\tid: \"XPATH-NODE-ESCAPE-001\",\n\t\ttype: \"xpath_injection\",\n\t\tcontexts: [\"xpath\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Predicate closed then unioned with a new node path\",\n\t\tcwe: 643,\n\t\tprimaryControl: XPATH_CONTROL,\n\t\tpattern: /\\]\\s{0,8}\\|\\s{0,8}\\/{1,2}/,\n\t},\n\t{\n\t\tid: \"XPATH-AXIS-001\",\n\t\ttype: \"xpath_injection\",\n\t\tcontexts: [\"xpath\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Explicit XPath axis, used to walk outside the intended subtree\",\n\t\tcwe: 643,\n\t\tprimaryControl: XPATH_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:ancestor|descendant|following|preceding|parent|child|self)(?:-or-self)?\\s{0,8}::/i,\n\t},\n\t{\n\t\tid: \"XPATH-FUNCTION-001\",\n\t\ttype: \"xpath_injection\",\n\t\tcontexts: [\"xpath\"],\n\t\tseverity: \"low\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"XPath function call in a value position\",\n\t\tcwe: 643,\n\t\tprimaryControl: XPATH_CONTROL,\n\t\tpattern: /\\b(?:count|name|local-name|namespace-uri|string-length|substring|position)\\s{0,8}\\(/i,\n\t},\n];\n\n//#endregion\n"],"mappings":"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"}
1
+ {"version":3,"file":"datastore.mjs","names":[],"sources":["../../../src/threats/rules/datastore.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 Query-language signatures: SQL, NoSQL/document-store operators,\n * LDAP filters, and XPath expressions.\n *\n * These are the rules most prone to false positives, because ordinary English and\n * ordinary source code contain SQL keywords constantly. Two mitigations apply:\n *\n * 1. Every rule is scoped to a query context, so none of them runs against a bio, a\n * support ticket, or a search box whose value is bound as a parameter.\n * 2. Tautology rules require a quote or paren adjacent to the comparison, so the\n * bare substring `1=1` — which occurs in perfectly ordinary arithmetic — no\n * longer fires on its own. The previous catalog matched `/1\\s*=\\s*1/` outright.\n *\n * Detection here is telemetry. Parameterized queries are the control: a bound\n * parameter is safe no matter which keywords it contains, and an interpolated one is\n * unsafe no matter how many signatures it dodges.\n *\n * @module @resq-systems/security/threats/rules/datastore\n */\n\nimport type { ThreatRule } from \"../types.js\";\n\n//#region SQL\n\n/** Repeated across every SQL rule. */\nconst SQL_CONTROL = \"Bind values as query parameters; never concatenate SQL strings\";\n\n/** SQL-injection signatures. Scoped to the `sql` context. */\nexport const SQL_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"SQL-UNION-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"UNION SELECT — classic result-set grafting\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\bUNION\\s+(?:ALL\\s+)?SELECT\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-DROP-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"DROP of a schema object\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\bDROP\\s+(?:TABLE|DATABASE|SCHEMA|INDEX|VIEW|FUNCTION|PROCEDURE)\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-DELETE-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"DELETE FROM in a value position\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\bDELETE\\s+FROM\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-TRUNCATE-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"TRUNCATE TABLE in a value position\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\bTRUNCATE\\s+TABLE\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-STACKED-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Statement separator followed by a new statement\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /;\\s{0,8}(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|GRANT|EXEC|UNION)\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-TAUTOLOGY-QUOTED-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Quote-escaped always-true comparison\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /['\"]\\s{0,8}(?:OR|AND)\\s+['\"]?\\w{1,20}['\"]?\\s{0,8}=\\s{0,8}['\"]?\\w{1,20}/i,\n\t},\n\t{\n\t\tid: \"SQL-TAUTOLOGY-NUMERIC-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Quote- or paren-escaped numeric always-true comparison\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// Requires the escape character. A bare `1=1` is ordinary arithmetic and is\n\t\t// deliberately *not* matched — that was the old catalog's worst false positive.\n\t\tpattern: /['\")]\\s{0,8}(?:OR|AND)\\s+\\d{1,10}\\s{0,8}=\\s{0,8}\\d{1,10}/i,\n\t},\n\t{\n\t\tid: \"SQL-COMMENT-TERMINATOR-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Trailing SQL comment, used to truncate the rest of a statement\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// The comment must follow a quote, paren, or digit — the position it occupies\n\t\t// when it truncates an injected statement. A bare `(?:--|#)…$` fires on CSS\n\t\t// colours (`#ff00aa`), issue references (`#123`), and em-dash-style prose.\n\t\tpattern: /['\")\\d]\\s{0,8}(?:--|#)[^\\r\\n]{0,64}$/,\n\t},\n\t{\n\t\t// sqlmap ships this as its `versionedkeywords` tamper: MySQL executes the body of\n\t\t// a versioned comment while every other engine ignores it, so wrapping each\n\t\t// keyword hides the statement from a scanner looking for bare keywords. A value\n\t\t// reaching a SQL sink has no legitimate reason to contain one.\n\t\tid: \"SQL-VERSIONED-COMMENT-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"MySQL versioned comment, whose contents execute on MySQL only\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\/\\*!\\d{0,5}/,\n\t},\n\t{\n\t\tid: \"SQL-COMMENT-INLINE-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Inline block comment, used to split keywords past naive filters\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\/\\*[\\s\\S]{0,200}?\\*\\//,\n\t},\n\t{\n\t\tid: \"SQL-COMMENT-SPLIT-KEYWORD-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Inline comment separating two SQL keywords — keyword-filter evasion\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// `1 UNION/**/SELECT/**/1` defeats SQL-UNION-001, which requires whitespace\n\t\t// between the keywords, and previously scored only 1.0 from the generic\n\t\t// inline-comment rule — an `allow` verdict on a working injection. A comment\n\t\t// *between two keywords* has no benign explanation in a bound value, so this is\n\t\t// graded high/high where the bare-comment rule stays low/low.\n\t\tpattern:\n\t\t\t/\\b(?:UNION|SELECT|INSERT|UPDATE|DELETE|DROP|FROM|WHERE|ORDER|GROUP|HAVING|AND|OR)\\s{0,8}\\/\\*[^*]{0,64}\\*\\/\\s{0,8}(?:UNION|SELECT|ALL|DISTINCT|FROM|WHERE|INTO|TABLE|BY|\\d|['\"])/i,\n\t},\n\t{\n\t\tid: \"SQL-TIME-BLIND-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Time-delay function used for blind injection\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\b(?:SLEEP|PG_SLEEP|BENCHMARK)\\s{0,8}\\(|\\bWAITFOR\\s+DELAY\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-METADATA-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Access to database metadata catalogs\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:INFORMATION_SCHEMA|pg_catalog|sysobjects|syscolumns|sqlite_master|all_tables)\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-XP-PROC-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"SQL Server extended procedure enabling OS access\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern: /\\b(?:xp_cmdshell|xp_regread|xp_regwrite|xp_dirtree|sp_oacreate)\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-FILE-IO-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"SQL file read/write primitive\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\tpattern:\n\t\t\t/\\bINTO\\s+(?:OUT|DUMP)FILE\\b|\\bLOAD_FILE\\s{0,8}\\(|\\bCOPY\\s+\\w{1,64}\\s+FROM\\s+PROGRAM\\b/i,\n\t},\n\t{\n\t\tid: \"SQL-ENGINE-FILE-IO-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Engine-specific file read/write primitive\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// SQL-FILE-IO-001 covers only the MySQL forms. Every primitive below scanned\n\t\t// clean unless the payload happened to carry a `;`, in which case\n\t\t// SQL-STACKED-001 caught it incidentally — remove the semicolon and each was\n\t\t// 0/allow.\n\t\tpattern:\n\t\t\t/\\bpg_(?:read_file|read_binary_file|ls_dir|stat_file)\\s{0,8}\\(|\\blo_(?:import|export)\\s{0,8}\\(|\\bOPENROWSET\\s{0,8}\\(|\\bOPENDATASOURCE\\s{0,8}\\(|\\bATTACH\\s+DATABASE\\b|\\bUTL_FILE\\s{0,8}\\./i,\n\t},\n\t{\n\t\tid: \"SQL-OUT-OF-BAND-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Out-of-band exfiltration or network primitive\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// Oracle's network packages and Postgres dblink are the standard out-of-band\n\t\t// channels for blind injection: they make the database itself perform the\n\t\t// exfiltration, so no data need come back through the response.\n\t\tpattern:\n\t\t\t/\\bUTL_HTTP\\s{0,8}\\.|\\bUTL_INADDR\\s{0,8}\\.|\\bUTL_SMTP\\s{0,8}\\.|\\bUTL_TCP\\s{0,8}\\.|\\bDBMS_LDAP\\s{0,8}\\.|\\bDBMS_PIPE\\s{0,8}\\.\\s{0,8}RECEIVE_MESSAGE\\b|\\bdblink(?:_connect|_exec)?\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SQL-HEX-LITERAL-001\",\n\t\ttype: \"sql_injection\",\n\t\tcontexts: [\"sql\"],\n\t\tseverity: \"low\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Long hex literal, sometimes used to smuggle string constants\",\n\t\tcwe: 89,\n\t\tprimaryControl: SQL_CONTROL,\n\t\t// Bounded at 8+ digits so ordinary colour codes (`0xFF00AA`) and short\n\t\t// constants do not fire. The old catalog used `/0x[0-9a-f]+/`, which matched\n\t\t// every CSS colour in the corpus.\n\t\tpattern: /\\b0x[0-9a-f]{8,}\\b/i,\n\t},\n];\n\n//#endregion\n\n//#region NoSQL\n\n/** Repeated across every NoSQL rule. */\nconst NOSQL_CONTROL =\n\t\"Cast query values to primitives and reject object-valued inputs; never pass raw request bodies to a query builder\";\n\n/** Document-store operator injection (MongoDB and compatible drivers). */\nexport const NOSQL_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"NOSQL-OPERATOR-OBJECT-001\",\n\t\ttype: \"nosql_injection\",\n\t\tcontexts: [\"nosql\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Object literal whose first key is a query operator\",\n\t\tcwe: 943,\n\t\tprimaryControl: NOSQL_CONTROL,\n\t\tpattern: /\\{\\s{0,8}[\"']?\\$[a-z]{2,20}[\"']?\\s{0,8}:/i,\n\t},\n\t{\n\t\tid: \"NOSQL-JS-EXECUTION-001\",\n\t\ttype: \"nosql_injection\",\n\t\tcontexts: [\"nosql\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Server-side JavaScript execution operator\",\n\t\tcwe: 943,\n\t\tprimaryControl: NOSQL_CONTROL,\n\t\tpattern: /\\$(?:where|function|accumulator)\\s{0,8}[\"']?\\s{0,8}:/i,\n\t},\n\t{\n\t\tid: \"NOSQL-OPERATOR-001\",\n\t\ttype: \"nosql_injection\",\n\t\tcontexts: [\"nosql\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Query operator token\",\n\t\tcwe: 943,\n\t\tprimaryControl: NOSQL_CONTROL,\n\t\tpattern:\n\t\t\t/\\$(?:gte?|lte?|ne|eq|nin?|and|or|not|nor|exists|type|mod|regex|text|all|elemMatch|size|slice|expr|jsonSchema)\\b/i,\n\t},\n\t{\n\t\tid: \"NOSQL-OPERATOR-ARRAY-001\",\n\t\ttype: \"nosql_injection\",\n\t\tcontexts: [\"nosql\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Array subscript naming a query operator\",\n\t\tcwe: 943,\n\t\tprimaryControl: NOSQL_CONTROL,\n\t\tpattern: /\\[\\s{0,8}[\"']?\\$[a-z]{2,20}[\"']?\\s{0,8}\\]/i,\n\t},\n];\n\n//#endregion\n\n//#region LDAP\n\n/** Repeated across every LDAP rule. */\nconst LDAP_CONTROL =\n\t\"Escape filter values per RFC 4515 and DN components per RFC 4514 using the LDAP client's escaping helper\";\n\n/** LDAP filter and DN injection. */\nexport const LDAP_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"LDAP-FILTER-WILDCARD-001\",\n\t\ttype: \"ldap_injection\",\n\t\tcontexts: [\"ldap\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Injected filter clause matching any value\",\n\t\tcwe: 90,\n\t\tprimaryControl: LDAP_CONTROL,\n\t\tpattern: /[()&|!]\\s{0,8}\\(\\s{0,8}[a-z0-9;.-]{1,64}\\s{0,8}=\\s{0,8}\\*/i,\n\t},\n\t{\n\t\tid: \"LDAP-FILTER-CLOSE-001\",\n\t\ttype: \"ldap_injection\",\n\t\tcontexts: [\"ldap\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Filter-terminating parenthesis followed by a boolean operator\",\n\t\tcwe: 90,\n\t\tprimaryControl: LDAP_CONTROL,\n\t\tpattern: /\\)\\s{0,8}[&|]\\s{0,8}\\(/,\n\t},\n\t{\n\t\tid: \"LDAP-METACHAR-001\",\n\t\ttype: \"ldap_injection\",\n\t\tcontexts: [\"ldap\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Unescaped LDAP filter metacharacter\",\n\t\tcwe: 90,\n\t\tprimaryControl: LDAP_CONTROL,\n\t\tpattern: /[*()\\\\]/,\n\t},\n\t{\n\t\tid: \"LDAP-DN-INJECTION-001\",\n\t\ttype: \"ldap_injection\",\n\t\tcontexts: [\"ldap\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"RDN separator in a value destined for a distinguished name\",\n\t\tcwe: 90,\n\t\tprimaryControl:\n\t\t\t\"Escape DN components per RFC 4514 using the LDAP client's DN escaping helper — filter escaping (RFC 4515) is a different function and does not cover these characters\",\n\t\t// The filter rules above cover RFC 4515 metacharacters (`*`, parens, backslash).\n\t\t// A DN uses a different grammar entirely: `,` and `+` separate relative\n\t\t// distinguished names, so `admin,ou=admins,dc=example,dc=com` re-parents the\n\t\t// entry. All three DN forms scanned 0/allow before this rule.\n\t\t// Confidence is medium because a common name can legitimately contain a comma\n\t\t// (\"Smith, John\") — which is exactly why it must be escaped, not rejected.\n\t\tpattern: /[,+]\\s{0,8}(?:cn|ou|dc|o|uid|sn|givenname|mail|member|objectclass)\\s{0,8}=/i,\n\t},\n\t{\n\t\tid: \"LDAP-DN-METACHAR-001\",\n\t\ttype: \"ldap_injection\",\n\t\tcontexts: [\"ldap\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Unescaped RFC 4514 distinguished-name metacharacter\",\n\t\tcwe: 90,\n\t\tprimaryControl: \"Escape DN components per RFC 4514 using the LDAP client's DN escaping helper\",\n\t\t// Leading/trailing space and `#` are also special in a DN, but flagging them\n\t\t// would fire on nearly every human name, so only the structural characters are\n\t\t// listed here.\n\t\tpattern: /[\"<>;]|^[+,]|[+,]$/,\n\t},\n];\n\n//#endregion\n\n//#region XPath\n\n/** Repeated across every XPath rule. */\nconst XPATH_CONTROL =\n\t\"Use a precompiled XPath with variable binding (XPathVariableResolver or equivalent); never concatenate expressions\";\n\n/** XPath/XQuery injection. */\nexport const XPATH_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"XPATH-TAUTOLOGY-001\",\n\t\ttype: \"xpath_injection\",\n\t\tcontexts: [\"xpath\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Quote-escaped always-true predicate\",\n\t\tcwe: 643,\n\t\tprimaryControl: XPATH_CONTROL,\n\t\tpattern: /['\"]\\s{0,8}(?:or|and)\\s+['\"]?\\w{1,20}['\"]?\\s{0,8}=\\s{0,8}['\"]?\\w{1,20}/i,\n\t},\n\t{\n\t\tid: \"XPATH-NODE-ESCAPE-001\",\n\t\ttype: \"xpath_injection\",\n\t\tcontexts: [\"xpath\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Predicate closed then unioned with a new node path\",\n\t\tcwe: 643,\n\t\tprimaryControl: XPATH_CONTROL,\n\t\tpattern: /\\]\\s{0,8}\\|\\s{0,8}\\/{1,2}/,\n\t},\n\t{\n\t\tid: \"XPATH-AXIS-001\",\n\t\ttype: \"xpath_injection\",\n\t\tcontexts: [\"xpath\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Explicit XPath axis, used to walk outside the intended subtree\",\n\t\tcwe: 643,\n\t\tprimaryControl: XPATH_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:ancestor|descendant|following|preceding|parent|child|self)(?:-or-self)?\\s{0,8}::/i,\n\t},\n\t{\n\t\tid: \"XPATH-FUNCTION-001\",\n\t\ttype: \"xpath_injection\",\n\t\tcontexts: [\"xpath\"],\n\t\tseverity: \"low\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"XPath function call in a value position\",\n\t\tcwe: 643,\n\t\tprimaryControl: XPATH_CONTROL,\n\t\tpattern: /\\b(?:count|name|local-name|namespace-uri|string-length|substring|position)\\s{0,8}\\(/i,\n\t},\n];\n\n//#endregion\n"],"mappings":"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"}
@@ -11,7 +11,7 @@ import { FORMULA_INJECTION_RULES, HEADER_INJECTION_RULES, LOG_INJECTION_RULES, P
11
11
  * Order affects only the sequence of findings in a result — scoring is
12
12
  * order-independent.
13
13
  */
14
- declare const THREAT_RULES: readonly ThreatRule[];
14
+ export declare const THREAT_RULES: readonly ThreatRule[];
15
15
  /**
16
16
  * Assert the catalog's structural invariants.
17
17
  *
@@ -23,7 +23,7 @@ declare const THREAT_RULES: readonly ThreatRule[];
23
23
  * stateful (`g`/`y`) pattern. The message lists every problem found, not just the
24
24
  * first.
25
25
  */
26
- declare function assertRuleCatalogIsValid(rules?: readonly ThreatRule[]): void;
26
+ export declare function assertRuleCatalogIsValid(rules?: readonly ThreatRule[]): void;
27
27
  /**
28
28
  * Rule IDs that once shipped and have since been withdrawn.
29
29
  *
@@ -36,7 +36,7 @@ declare function assertRuleCatalogIsValid(rules?: readonly ThreatRule[]): void;
36
36
  * runs at module load in every consumer process, and its checks are fatal because they
37
37
  * are runtime-correctness failures. This one is release hygiene.
38
38
  */
39
- declare const RETIRED_RULE_IDS: readonly string[];
39
+ export declare const RETIRED_RULE_IDS: readonly string[];
40
40
  /**
41
41
  * Collect the rules applicable to a set of sinks.
42
42
  *
@@ -48,7 +48,7 @@ declare const RETIRED_RULE_IDS: readonly string[];
48
48
  * @returns Deduplicated rules in {@link THREAT_RULES} order — stable regardless of
49
49
  * the order the caller listed contexts in.
50
50
  */
51
- declare function getRulesForContexts(contexts: readonly ThreatContext[]): readonly ThreatRule[];
51
+ export declare function getRulesForContexts(contexts: readonly ThreatContext[]): readonly ThreatRule[];
52
52
  //#endregion
53
- export { COMMAND_INJECTION_RULES, CREDENTIAL_EXPOSURE_RULES, DOUBLE_ENCODING_RULES, FILE_INCLUSION_RULES, FORMULA_INJECTION_RULES, HEADER_INJECTION_RULES, JWT_RULES, LDAP_INJECTION_RULES, LOG_INJECTION_RULES, NOSQL_INJECTION_RULES, PARAMETER_POLLUTION_RULES, PATH_TRAVERSAL_RULES, PROMPT_INJECTION_RULES, PROTOTYPE_POLLUTION_RULES, RETIRED_RULE_IDS, SQL_INJECTION_RULES, SSRF_RULES, TEMPLATE_INJECTION_RULES, THREAT_RULES, UNIVERSAL_RULES, XML_INJECTION_RULES, XPATH_INJECTION_RULES, XSS_RULES, assertRuleCatalogIsValid, getRulesForContexts };
53
+ export { COMMAND_INJECTION_RULES, CREDENTIAL_EXPOSURE_RULES, DOUBLE_ENCODING_RULES, FILE_INCLUSION_RULES, FORMULA_INJECTION_RULES, HEADER_INJECTION_RULES, JWT_RULES, LDAP_INJECTION_RULES, LOG_INJECTION_RULES, NOSQL_INJECTION_RULES, PARAMETER_POLLUTION_RULES, PATH_TRAVERSAL_RULES, PROMPT_INJECTION_RULES, PROTOTYPE_POLLUTION_RULES, SQL_INJECTION_RULES, SSRF_RULES, TEMPLATE_INJECTION_RULES, UNIVERSAL_RULES, XML_INJECTION_RULES, XPATH_INJECTION_RULES, XSS_RULES };
54
54
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/threats/rules/index.ts"],"mappings":";;;;;;;;;;;;;cA4Ea,uBAAuB;;;;;;;;;;;;iBAuCpB,yBAAyB,iBAAgB;;;;;;;;;;;;;cAyE5C;;;;;;;;;;;;iBAaG,oBAAoB,mBAAmB,2BAA2B"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/threats/rules/index.ts"],"mappings":";;;;;;;;;;;;;qBA6Ea,uBAAuB;;;;;;;;;;;;wBAuCpB,yBAAyB,iBAAgB;;;;;;;;;;;;;qBAyE5C;;;;;;;;;;;;wBAaG,oBAAoB,mBAAmB,2BAA2B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/threats/rules/index.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview The complete threat-rule catalog, assembled from the per-domain\n * groups and validated at module load.\n *\n * Validation is deliberately eager and fatal. A duplicate rule ID would make findings\n * ambiguous and break per-rule tuning; a stateful (`/g` or `/y`) pattern would make\n * detection depend on how many times the module had been called before. Both are\n * programming errors in the catalog itself rather than runtime conditions, so they\n * throw at import instead of degrading silently in production.\n *\n * @module @resq-systems/security/threats/rules\n */\n\nimport type { ThreatContext, ThreatRule } from \"../types.js\";\nimport {\n\tLDAP_INJECTION_RULES,\n\tNOSQL_INJECTION_RULES,\n\tSQL_INJECTION_RULES,\n\tXPATH_INJECTION_RULES,\n} from \"./datastore.js\";\nimport {\n\tPROMPT_INJECTION_RULES,\n\tTEMPLATE_INJECTION_RULES,\n\tUNIVERSAL_RULES,\n\tXML_INJECTION_RULES,\n} from \"./markup.js\";\nimport {\n\tCREDENTIAL_EXPOSURE_RULES,\n\tDOUBLE_ENCODING_RULES,\n\tJWT_RULES,\n\tPARAMETER_POLLUTION_RULES,\n} from \"./protocol.js\";\nimport {\n\tCOMMAND_INJECTION_RULES,\n\tFILE_INCLUSION_RULES,\n\tPATH_TRAVERSAL_RULES,\n\tSSRF_RULES,\n} from \"./system.js\";\nimport {\n\tFORMULA_INJECTION_RULES,\n\tHEADER_INJECTION_RULES,\n\tLOG_INJECTION_RULES,\n\tPROTOTYPE_POLLUTION_RULES,\n\tXSS_RULES,\n} from \"./web.js\";\n\nexport * from \"./datastore.js\";\nexport * from \"./markup.js\";\nexport * from \"./protocol.js\";\nexport * from \"./system.js\";\nexport * from \"./web.js\";\n\n//#region Catalog\n\n/**\n * Every signature the engine knows about, in a stable order.\n *\n * Order affects only the sequence of findings in a result — scoring is\n * order-independent.\n */\nexport const THREAT_RULES: readonly ThreatRule[] = [\n\t...XSS_RULES,\n\t...PROTOTYPE_POLLUTION_RULES,\n\t...HEADER_INJECTION_RULES,\n\t...FORMULA_INJECTION_RULES,\n\t...LOG_INJECTION_RULES,\n\t...SQL_INJECTION_RULES,\n\t...NOSQL_INJECTION_RULES,\n\t...LDAP_INJECTION_RULES,\n\t...XPATH_INJECTION_RULES,\n\t...COMMAND_INJECTION_RULES,\n\t...PATH_TRAVERSAL_RULES,\n\t...FILE_INCLUSION_RULES,\n\t...SSRF_RULES,\n\t...XML_INJECTION_RULES,\n\t...TEMPLATE_INJECTION_RULES,\n\t...PROMPT_INJECTION_RULES,\n\t...PARAMETER_POLLUTION_RULES,\n\t...CREDENTIAL_EXPOSURE_RULES,\n\t...JWT_RULES,\n\t...DOUBLE_ENCODING_RULES,\n\t...UNIVERSAL_RULES,\n];\n\n//#endregion\n\n//#region Validation\n\n/**\n * Assert the catalog's structural invariants.\n *\n * Runs once at module load and throws on violation — see the file overview for why\n * these are fatal rather than warnings.\n *\n * @param rules - Catalog to validate. Defaults to {@link THREAT_RULES}.\n * @throws {Error} If any rule has a duplicate ID, an empty context list, or a\n * stateful (`g`/`y`) pattern. The message lists every problem found, not just the\n * first.\n */\nexport function assertRuleCatalogIsValid(rules: readonly ThreatRule[] = THREAT_RULES): void {\n\tconst seen = new Set<string>();\n\tconst problems: string[] = [];\n\n\tfor (const rule of rules) {\n\t\tif (seen.has(rule.id)) {\n\t\t\tproblems.push(`duplicate rule id: ${rule.id}`);\n\t\t}\n\t\tseen.add(rule.id);\n\n\t\tif (rule.contexts.length === 0) {\n\t\t\tproblems.push(`${rule.id}: contexts must not be empty`);\n\t\t}\n\n\t\tif (rule.pattern.global || rule.pattern.sticky) {\n\t\t\tproblems.push(\n\t\t\t\t`${rule.id}: pattern must not use the g or y flag (lastIndex makes matching stateful)`,\n\t\t\t);\n\t\t}\n\t}\n\n\tif (problems.length > 0) {\n\t\tthrow new Error(`Invalid threat rule catalog:\\n ${problems.join(\"\\n \")}`);\n\t}\n}\n\nassertRuleCatalogIsValid();\n\n//#endregion\n\n//#region Lookup\n\n/**\n * Index from context to the rules that declare it, built once at module load so\n * per-call scanning is a map lookup rather than a filter over the whole catalog.\n */\nconst RULES_BY_CONTEXT: ReadonlyMap<ThreatContext, readonly ThreatRule[]> = (() => {\n\tconst index = new Map<ThreatContext, ThreatRule[]>();\n\tfor (const rule of THREAT_RULES) {\n\t\tfor (const context of rule.contexts) {\n\t\t\tconst bucket = index.get(context);\n\t\t\tif (bucket) {\n\t\t\t\tbucket.push(rule);\n\t\t\t} else {\n\t\t\t\tindex.set(context, [rule]);\n\t\t\t}\n\t\t}\n\t}\n\treturn index;\n})();\n\n/**\n * Memoized results of {@link getRulesForContexts} for multi-context calls.\n *\n * Keyed by the sorted, deduplicated, known-only context list, so it holds at most one\n * entry per distinct subset of the context enum — a hard ceiling that does not depend on\n * caller behaviour. The single-context path never reaches here; it already returns a\n * prebuilt bucket from {@link RULES_BY_CONTEXT}.\n */\nconst COMBINATION_CACHE = new Map<string, readonly ThreatRule[]>();\n\n/**\n * Rule IDs that once shipped and have since been withdrawn.\n *\n * A rule ID is public API: consumers pin them in `excludeRuleIds` to suppress a finding\n * they have accepted. Reusing a retired ID silently re-points that suppression at an\n * unrelated rule, which is worse than the original finding because nobody is looking.\n * Add the ID here when a rule is withdrawn; never remove an entry.\n *\n * Enforced from the test suite rather than from {@link assertRuleCatalogIsValid}: that\n * runs at module load in every consumer process, and its checks are fatal because they\n * are runtime-correctness failures. This one is release hygiene.\n */\nexport const RETIRED_RULE_IDS: readonly string[] = [];\n\n/**\n * Collect the rules applicable to a set of sinks.\n *\n * A rule declaring several contexts is returned once even when the caller names more\n * than one of them, so a value bound for both `html` and `url` is not double-scored\n * by a scheme rule that covers both.\n *\n * @param contexts - Sinks the value is destined for.\n * @returns Deduplicated rules in {@link THREAT_RULES} order — stable regardless of\n * the order the caller listed contexts in.\n */\nexport function getRulesForContexts(contexts: readonly ThreatContext[]): readonly ThreatRule[] {\n\tconst first = contexts[0];\n\tif (contexts.length === 1 && first !== undefined) {\n\t\treturn RULES_BY_CONTEXT.get(first) ?? [];\n\t}\n\n\t// The result is a pure function of the context set, so this re-derived the same\n\t// array on every call: ~214 Set insertions plus a 132-element filter allocation,\n\t// which on short inputs was a third of total scan time.\n\t//\n\t// The key is deduplicated, filtered to contexts the catalog knows, and sorted.\n\t// Sorting preserves the documented order-independence. The other two steps bound\n\t// the cache: this function is exported publicly, so a JavaScript caller can pass\n\t// arbitrary strings, and keying on those unfiltered would let any caller grow the\n\t// map without limit. An unknown context contributes no rules, so dropping it\n\t// cannot change the result -- it only collapses [\"html\", \"nonsense\"] onto\n\t// [\"html\"], which is the same answer. Keys are therefore subsets of a 19-member set.\n\tconst known = [...new Set(contexts)].filter((context) => RULES_BY_CONTEXT.has(context)).sort();\n\tconst key = known.join(\" \");\n\tconst cached = COMBINATION_CACHE.get(key);\n\tif (cached !== undefined) return cached;\n\n\tconst selected = new Set<ThreatRule>();\n\tfor (const context of known) {\n\t\tfor (const rule of RULES_BY_CONTEXT.get(context) ?? []) {\n\t\t\tselected.add(rule);\n\t\t}\n\t}\n\n\tconst computed = THREAT_RULES.filter((rule) => selected.has(rule));\n\tCOMBINATION_CACHE.set(key, computed);\n\treturn computed;\n}\n\n//#endregion\n"],"mappings":";;;;;;;;;;;;AA4EA,MAAa,eAAsC;CAClD,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;AACJ;;;;;;;;;;;;AAiBA,SAAgB,yBAAyB,QAA+B,cAAoB;CAC3F,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,WAAqB,CAAC;CAE5B,KAAK,MAAM,QAAQ,OAAO;EACzB,IAAI,KAAK,IAAI,KAAK,EAAE,GACnB,SAAS,KAAK,sBAAsB,KAAK,IAAI;EAE9C,KAAK,IAAI,KAAK,EAAE;EAEhB,IAAI,KAAK,SAAS,WAAW,GAC5B,SAAS,KAAK,GAAG,KAAK,GAAG,6BAA6B;EAGvD,IAAI,KAAK,QAAQ,UAAU,KAAK,QAAQ,QACvC,SAAS,KACR,GAAG,KAAK,GAAG,2EACZ;CAEF;CAEA,IAAI,SAAS,SAAS,GACrB,MAAM,IAAI,MAAM,mCAAmC,SAAS,KAAK,MAAM,GAAG;AAE5E;AAEA,yBAAyB;;;;;AAUzB,MAAM,0BAA6E;CAClF,MAAM,wBAAQ,IAAI,IAAiC;CACnD,KAAK,MAAM,QAAQ,cAClB,KAAK,MAAM,WAAW,KAAK,UAAU;EACpC,MAAM,SAAS,MAAM,IAAI,OAAO;EAChC,IAAI,QACH,OAAO,KAAK,IAAI;OAEhB,MAAM,IAAI,SAAS,CAAC,IAAI,CAAC;CAE3B;CAED,OAAO;AACR,EAAA,CAAG;;;;;;;;;AAUH,MAAM,oCAAoB,IAAI,IAAmC;;;;;;;;;;;;;AAcjE,MAAa,mBAAsC,CAAC;;;;;;;;;;;;AAapD,SAAgB,oBAAoB,UAA2D;CAC9F,MAAM,QAAQ,SAAS;CACvB,IAAI,SAAS,WAAW,KAAK,UAAU,KAAA,GACtC,OAAO,iBAAiB,IAAI,KAAK,KAAK,CAAC;CAcxC,MAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC,CAAC,CAAC,QAAQ,YAAY,iBAAiB,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK;CAC7F,MAAM,MAAM,MAAM,KAAK,GAAG;CAC1B,MAAM,SAAS,kBAAkB,IAAI,GAAG;CACxC,IAAI,WAAW,KAAA,GAAW,OAAO;CAEjC,MAAM,2BAAW,IAAI,IAAgB;CACrC,KAAK,MAAM,WAAW,OACrB,KAAK,MAAM,QAAQ,iBAAiB,IAAI,OAAO,KAAK,CAAC,GACpD,SAAS,IAAI,IAAI;CAInB,MAAM,WAAW,aAAa,QAAQ,SAAS,SAAS,IAAI,IAAI,CAAC;CACjE,kBAAkB,IAAI,KAAK,QAAQ;CACnC,OAAO;AACR"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/threats/rules/index.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n * SPDX-License-Identifier: Apache-2.0\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview The complete threat-rule catalog, assembled from the per-domain\n * groups and validated at module load.\n *\n * Validation is deliberately eager and fatal. A duplicate rule ID would make findings\n * ambiguous and break per-rule tuning; a stateful (`/g` or `/y`) pattern would make\n * detection depend on how many times the module had been called before. Both are\n * programming errors in the catalog itself rather than runtime conditions, so they\n * throw at import instead of degrading silently in production.\n *\n * @module @resq-systems/security/threats/rules\n */\n\nimport type { ThreatContext, ThreatRule } from \"../types.js\";\nimport {\n\tLDAP_INJECTION_RULES,\n\tNOSQL_INJECTION_RULES,\n\tSQL_INJECTION_RULES,\n\tXPATH_INJECTION_RULES,\n} from \"./datastore.js\";\nimport {\n\tPROMPT_INJECTION_RULES,\n\tTEMPLATE_INJECTION_RULES,\n\tUNIVERSAL_RULES,\n\tXML_INJECTION_RULES,\n} from \"./markup.js\";\nimport {\n\tCREDENTIAL_EXPOSURE_RULES,\n\tDOUBLE_ENCODING_RULES,\n\tJWT_RULES,\n\tPARAMETER_POLLUTION_RULES,\n} from \"./protocol.js\";\nimport {\n\tCOMMAND_INJECTION_RULES,\n\tFILE_INCLUSION_RULES,\n\tPATH_TRAVERSAL_RULES,\n\tSSRF_RULES,\n} from \"./system.js\";\nimport {\n\tFORMULA_INJECTION_RULES,\n\tHEADER_INJECTION_RULES,\n\tLOG_INJECTION_RULES,\n\tPROTOTYPE_POLLUTION_RULES,\n\tXSS_RULES,\n} from \"./web.js\";\n\nexport * from \"./datastore.js\";\nexport * from \"./markup.js\";\nexport * from \"./protocol.js\";\nexport * from \"./system.js\";\nexport * from \"./web.js\";\n\n//#region Catalog\n\n/**\n * Every signature the engine knows about, in a stable order.\n *\n * Order affects only the sequence of findings in a result — scoring is\n * order-independent.\n */\nexport const THREAT_RULES: readonly ThreatRule[] = [\n\t...XSS_RULES,\n\t...PROTOTYPE_POLLUTION_RULES,\n\t...HEADER_INJECTION_RULES,\n\t...FORMULA_INJECTION_RULES,\n\t...LOG_INJECTION_RULES,\n\t...SQL_INJECTION_RULES,\n\t...NOSQL_INJECTION_RULES,\n\t...LDAP_INJECTION_RULES,\n\t...XPATH_INJECTION_RULES,\n\t...COMMAND_INJECTION_RULES,\n\t...PATH_TRAVERSAL_RULES,\n\t...FILE_INCLUSION_RULES,\n\t...SSRF_RULES,\n\t...XML_INJECTION_RULES,\n\t...TEMPLATE_INJECTION_RULES,\n\t...PROMPT_INJECTION_RULES,\n\t...PARAMETER_POLLUTION_RULES,\n\t...CREDENTIAL_EXPOSURE_RULES,\n\t...JWT_RULES,\n\t...DOUBLE_ENCODING_RULES,\n\t...UNIVERSAL_RULES,\n];\n\n//#endregion\n\n//#region Validation\n\n/**\n * Assert the catalog's structural invariants.\n *\n * Runs once at module load and throws on violation — see the file overview for why\n * these are fatal rather than warnings.\n *\n * @param rules - Catalog to validate. Defaults to {@link THREAT_RULES}.\n * @throws {Error} If any rule has a duplicate ID, an empty context list, or a\n * stateful (`g`/`y`) pattern. The message lists every problem found, not just the\n * first.\n */\nexport function assertRuleCatalogIsValid(rules: readonly ThreatRule[] = THREAT_RULES): void {\n\tconst seen = new Set<string>();\n\tconst problems: string[] = [];\n\n\tfor (const rule of rules) {\n\t\tif (seen.has(rule.id)) {\n\t\t\tproblems.push(`duplicate rule id: ${rule.id}`);\n\t\t}\n\t\tseen.add(rule.id);\n\n\t\tif (rule.contexts.length === 0) {\n\t\t\tproblems.push(`${rule.id}: contexts must not be empty`);\n\t\t}\n\n\t\tif (rule.pattern.global || rule.pattern.sticky) {\n\t\t\tproblems.push(\n\t\t\t\t`${rule.id}: pattern must not use the g or y flag (lastIndex makes matching stateful)`,\n\t\t\t);\n\t\t}\n\t}\n\n\tif (problems.length > 0) {\n\t\tthrow new Error(`Invalid threat rule catalog:\\n ${problems.join(\"\\n \")}`);\n\t}\n}\n\nassertRuleCatalogIsValid();\n\n//#endregion\n\n//#region Lookup\n\n/**\n * Index from context to the rules that declare it, built once at module load so\n * per-call scanning is a map lookup rather than a filter over the whole catalog.\n */\nconst RULES_BY_CONTEXT: ReadonlyMap<ThreatContext, readonly ThreatRule[]> = (() => {\n\tconst index = new Map<ThreatContext, ThreatRule[]>();\n\tfor (const rule of THREAT_RULES) {\n\t\tfor (const context of rule.contexts) {\n\t\t\tconst bucket = index.get(context);\n\t\t\tif (bucket) {\n\t\t\t\tbucket.push(rule);\n\t\t\t} else {\n\t\t\t\tindex.set(context, [rule]);\n\t\t\t}\n\t\t}\n\t}\n\treturn index;\n})();\n\n/**\n * Memoized results of {@link getRulesForContexts} for multi-context calls.\n *\n * Keyed by the sorted, deduplicated, known-only context list, so it holds at most one\n * entry per distinct subset of the context enum — a hard ceiling that does not depend on\n * caller behaviour. The single-context path never reaches here; it already returns a\n * prebuilt bucket from {@link RULES_BY_CONTEXT}.\n */\nconst COMBINATION_CACHE = new Map<string, readonly ThreatRule[]>();\n\n/**\n * Rule IDs that once shipped and have since been withdrawn.\n *\n * A rule ID is public API: consumers pin them in `excludeRuleIds` to suppress a finding\n * they have accepted. Reusing a retired ID silently re-points that suppression at an\n * unrelated rule, which is worse than the original finding because nobody is looking.\n * Add the ID here when a rule is withdrawn; never remove an entry.\n *\n * Enforced from the test suite rather than from {@link assertRuleCatalogIsValid}: that\n * runs at module load in every consumer process, and its checks are fatal because they\n * are runtime-correctness failures. This one is release hygiene.\n */\nexport const RETIRED_RULE_IDS: readonly string[] = [];\n\n/**\n * Collect the rules applicable to a set of sinks.\n *\n * A rule declaring several contexts is returned once even when the caller names more\n * than one of them, so a value bound for both `html` and `url` is not double-scored\n * by a scheme rule that covers both.\n *\n * @param contexts - Sinks the value is destined for.\n * @returns Deduplicated rules in {@link THREAT_RULES} order — stable regardless of\n * the order the caller listed contexts in.\n */\nexport function getRulesForContexts(contexts: readonly ThreatContext[]): readonly ThreatRule[] {\n\tconst first = contexts[0];\n\tif (contexts.length === 1 && first !== undefined) {\n\t\treturn RULES_BY_CONTEXT.get(first) ?? [];\n\t}\n\n\t// The result is a pure function of the context set, so this re-derived the same\n\t// array on every call: ~214 Set insertions plus a 132-element filter allocation,\n\t// which on short inputs was a third of total scan time.\n\t//\n\t// The key is deduplicated, filtered to contexts the catalog knows, and sorted.\n\t// Sorting preserves the documented order-independence. The other two steps bound\n\t// the cache: this function is exported publicly, so a JavaScript caller can pass\n\t// arbitrary strings, and keying on those unfiltered would let any caller grow the\n\t// map without limit. An unknown context contributes no rules, so dropping it\n\t// cannot change the result -- it only collapses [\"html\", \"nonsense\"] onto\n\t// [\"html\"], which is the same answer. Keys are therefore subsets of a 19-member set.\n\tconst known = [...new Set(contexts)].filter((context) => RULES_BY_CONTEXT.has(context)).sort();\n\tconst key = known.join(\" \");\n\tconst cached = COMBINATION_CACHE.get(key);\n\tif (cached !== undefined) return cached;\n\n\tconst selected = new Set<ThreatRule>();\n\tfor (const context of known) {\n\t\tfor (const rule of RULES_BY_CONTEXT.get(context) ?? []) {\n\t\t\tselected.add(rule);\n\t\t}\n\t}\n\n\tconst computed = THREAT_RULES.filter((rule) => selected.has(rule));\n\tCOMBINATION_CACHE.set(key, computed);\n\treturn computed;\n}\n\n//#endregion\n"],"mappings":";;;;;;;;;;;;AA6EA,MAAa,eAAsC;CAClD,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;AACJ;;;;;;;;;;;;AAiBA,SAAgB,yBAAyB,QAA+B,cAAoB;CAC3F,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,WAAqB,CAAC;CAE5B,KAAK,MAAM,QAAQ,OAAO;EACzB,IAAI,KAAK,IAAI,KAAK,EAAE,GACnB,SAAS,KAAK,sBAAsB,KAAK,IAAI;EAE9C,KAAK,IAAI,KAAK,EAAE;EAEhB,IAAI,KAAK,SAAS,WAAW,GAC5B,SAAS,KAAK,GAAG,KAAK,GAAG,6BAA6B;EAGvD,IAAI,KAAK,QAAQ,UAAU,KAAK,QAAQ,QACvC,SAAS,KACR,GAAG,KAAK,GAAG,2EACZ;CAEF;CAEA,IAAI,SAAS,SAAS,GACrB,MAAM,IAAI,MAAM,mCAAmC,SAAS,KAAK,MAAM,GAAG;AAE5E;AAEA,yBAAyB;;;;;AAUzB,MAAM,0BAA6E;CAClF,MAAM,wBAAQ,IAAI,IAAiC;CACnD,KAAK,MAAM,QAAQ,cAClB,KAAK,MAAM,WAAW,KAAK,UAAU;EACpC,MAAM,SAAS,MAAM,IAAI,OAAO;EAChC,IAAI,QACH,OAAO,KAAK,IAAI;OAEhB,MAAM,IAAI,SAAS,CAAC,IAAI,CAAC;CAE3B;CAED,OAAO;AACR,EAAA,CAAG;;;;;;;;;AAUH,MAAM,oCAAoB,IAAI,IAAmC;;;;;;;;;;;;;AAcjE,MAAa,mBAAsC,CAAC;;;;;;;;;;;;AAapD,SAAgB,oBAAoB,UAA2D;CAC9F,MAAM,QAAQ,SAAS;CACvB,IAAI,SAAS,WAAW,KAAK,UAAU,KAAA,GACtC,OAAO,iBAAiB,IAAI,KAAK,KAAK,CAAC;CAcxC,MAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC,CAAC,CAAC,QAAQ,YAAY,iBAAiB,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK;CAC7F,MAAM,MAAM,MAAM,KAAK,GAAG;CAC1B,MAAM,SAAS,kBAAkB,IAAI,GAAG;CACxC,IAAI,WAAW,KAAA,GAAW,OAAO;CAEjC,MAAM,2BAAW,IAAI,IAAgB;CACrC,KAAK,MAAM,WAAW,OACrB,KAAK,MAAM,QAAQ,iBAAiB,IAAI,OAAO,KAAK,CAAC,GACpD,SAAS,IAAI,IAAI;CAInB,MAAM,WAAW,aAAa,QAAQ,SAAS,SAAS,IAAI,IAAI,CAAC;CACjE,kBAAkB,IAAI,KAAK,QAAQ;CACnC,OAAO;AACR"}
@@ -1,9 +1,9 @@
1
1
  import { ThreatRule } from "../types.mjs";
2
2
  //#region src/threats/rules/markup.d.ts
3
3
  /** XML external entity and entity-expansion signatures. */
4
- declare const XML_INJECTION_RULES: readonly ThreatRule[];
4
+ export declare const XML_INJECTION_RULES: readonly ThreatRule[];
5
5
  /** Server-side template injection and SSI directives. */
6
- declare const TEMPLATE_INJECTION_RULES: readonly ThreatRule[];
6
+ export declare const TEMPLATE_INJECTION_RULES: readonly ThreatRule[];
7
7
  /**
8
8
  * LLM prompt injection.
9
9
  *
@@ -12,7 +12,7 @@ declare const TEMPLATE_INJECTION_RULES: readonly ThreatRule[];
12
12
  * graded accordingly (mostly `medium` confidence) and are useful as telemetry and as
13
13
  * a tripwire for human review, never as an authorization decision.
14
14
  */
15
- declare const PROMPT_INJECTION_RULES: readonly ThreatRule[];
15
+ export declare const PROMPT_INJECTION_RULES: readonly ThreatRule[];
16
16
  /**
17
17
  * Rules that apply in every context, including `general_text`.
18
18
  *
@@ -22,7 +22,6 @@ declare const PROMPT_INJECTION_RULES: readonly ThreatRule[];
22
22
  * to spoof identity, and C0 control characters all clear it. SQL keywords and `../`
23
23
  * emphatically do not, which is why they are context-scoped instead.
24
24
  */
25
- declare const UNIVERSAL_RULES: readonly ThreatRule[];
25
+ export declare const UNIVERSAL_RULES: readonly ThreatRule[];
26
26
  //#endregion
27
- export { PROMPT_INJECTION_RULES, TEMPLATE_INJECTION_RULES, UNIVERSAL_RULES, XML_INJECTION_RULES };
28
27
  //# sourceMappingURL=markup.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"markup.d.mts","names":[],"sources":["../../../src/threats/rules/markup.ts"],"mappings":";;;cAuCa,8BAA8B;;cAmE9B,mCAAmC;;;;;;;;;cA0KnC,iCAAiC;;;;;;;;;;cAgHjC,0BAA0B"}
1
+ {"version":3,"file":"markup.d.mts","names":[],"sources":["../../../src/threats/rules/markup.ts"],"mappings":";;;qBAwCa,8BAA8B;;qBAmE9B,mCAAmC;;;;;;;;;qBA0KnC,iCAAiC;;;;;;;;;;qBAqHjC,0BAA0B"}
@@ -2,6 +2,7 @@ import { ALL_THREAT_CONTEXTS } from "../types.mjs";
2
2
  //#region src/threats/rules/markup.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.
@@ -267,7 +268,7 @@ const PROMPT_INJECTION_RULES = [
267
268
  description: "Line beginning with a conversational role label",
268
269
  cwe: 1427,
269
270
  primaryControl: PROMPT_CONTROL,
270
- pattern: /^\s{0,8}(?:system|assistant|developer|tool)\s{0,8}:/im
271
+ pattern: /^[^\S\r\n\u2028\u2029]*(?:system|assistant|developer|tool)\s{0,8}:/im
271
272
  },
272
273
  {
273
274
  id: "PROMPT-EXFIL-001",
@@ -1 +1 @@
1
- {"version":3,"file":"markup.mjs","names":[],"sources":["../../../src/threats/rules/markup.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 Document- and prompt-level signatures: XML/XXE, server-side template\n * and SSI injection, LLM prompt injection, plus the small set of universal rules that\n * apply in every context because no legitimate input needs them.\n *\n * XXE and SSTI are configuration weaknesses, not filtering ones. XXE is prevented by\n * disabling DTD processing and external entity resolution in the parser; SSTI is\n * prevented by passing untrusted values as template *data* rather than splicing them\n * into template *source*. Both rule groups exist to catch a payload that a\n * misconfigured parser or a `renderString(userInput)` call would otherwise execute.\n *\n * @module @resq-systems/security/threats/rules/markup\n */\n\nimport { ALL_THREAT_CONTEXTS, type ThreatRule } from \"../types.js\";\n\n//#region XML / XXE\n\n/** Repeated across every XML rule. */\nconst XML_CONTROL =\n\t\"Disable DTD processing and external entity resolution in the XML parser (noent:false, resolveExternals:false)\";\n\n/** XML external entity and entity-expansion signatures. */\nexport const XML_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"XML-ENTITY-DECL-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Inline ENTITY declaration\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!ENTITY\\b/i,\n\t},\n\t{\n\t\tid: \"XML-EXTERNAL-ID-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"SYSTEM or PUBLIC external identifier\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /\\b(?:SYSTEM|PUBLIC)\\s+[\"']/,\n\t},\n\t{\n\t\tid: \"XML-DOCTYPE-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"DOCTYPE declaration in untrusted XML\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!DOCTYPE\\b/i,\n\t},\n\t{\n\t\tid: \"XML-XINCLUDE-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"XInclude directive — an entity-free route to the same file read\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<\\s{0,8}xi:include\\b|www\\.w3\\.org\\/2001\\/XInclude/i,\n\t},\n\t{\n\t\tid: \"XML-CDATA-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"low\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"CDATA section, used to smuggle markup past naive filters\",\n\t\tcwe: 91,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!\\[CDATA\\[/,\n\t},\n];\n\n//#endregion\n\n//#region Template injection\n\n/** Repeated across most template-injection rules. */\nconst SSTI_CONTROL =\n\t\"Pass untrusted values as template data (context variables), never as template source; render with autoescaping on\";\n\n/** Server-side template injection and SSI directives. */\nexport const TEMPLATE_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"SSTI-SANDBOX-ESCAPE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Python object-graph traversal used to escape a template sandbox\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /\\b__(?:class|globals|subclasses|mro|builtins|import|base|init|reduce)__\\b/,\n\t},\n\t{\n\t\tid: \"SSTI-OGNL-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"OGNL/EL expression reaching a runtime execution class\",\n\t\tcwe: 917,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /_memberAccess|\\bRuntime\\s{0,8}\\.\\s{0,8}getRuntime|\\bProcessBuilder\\b|@java\\.lang\\b/i,\n\t},\n\t{\n\t\tid: \"SSTI-MUSTACHE-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Handlebars/Jinja/Twig expression delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// The inner class excludes `{` as well as `}`. With only `}` excluded, an\n\t\t// input of 100 000 `{` characters makes the engine consume 200 of them at\n\t\t// every start position and then backtrack — ~40ms per scan, measured by\n\t\t// tests/regex-safety.test.ts. Excluding `{` makes the failure immediate, and\n\t\t// costs nothing: a template expression does not contain a bare `{`.\n\t\tpattern: /\\{\\{[^{}]{0,200}\\}\\}/,\n\t},\n\t{\n\t\tid: \"SSTI-STATEMENT-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Jinja/Twig statement delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /\\{%[^%]{0,200}%\\}/,\n\t},\n\t{\n\t\tid: \"SSTI-EL-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"JSP EL, Freemarker, or JS template-literal interpolation\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// `{` excluded from the inner class for the same reason as the rule above.\n\t\tpattern: /[$#]\\{[^{}]{0,200}\\}/,\n\t},\n\t{\n\t\tid: \"SSI-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\", \"html\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Server-side include directive\",\n\t\tcwe: 97,\n\t\tprimaryControl:\n\t\t\t\"Disable server-side includes, or serve untrusted content from a path with SSI turned off\",\n\t\tpattern: /<!--#\\s{0,8}(?:exec|include|echo|config|fsize|flastmod|printenv|set)\\b/i,\n\t},\n\t{\n\t\tid: \"SSTI-SCRIPTLET-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\t// `template` only. A scriptlet spliced into an already-rendered response does\n\t\t// not execute — only template *source* is an execution path. SSI-DIRECTIVE-001\n\t\t// is in both because Apache and nginx post-process served HTML; no equivalent\n\t\t// stage exists for ERB, JSP, ASP, or EJS.\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"ERB / JSP / ASP / EJS scriptlet delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /<%[=\\-@#!]|<%[^%]{0,200}%>/,\n\t},\n\t{\n\t\tid: \"SSTI-FREEMARKER-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"FreeMarker directive or user-defined directive call\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern:\n\t\t\t/<#\\s{0,8}(?:assign|list|if|else|import|include|macro|function|setting|attempt|global|local|nested|recurse|switch|visit|compress|noparse|outputformat)\\b|<@[\\w.$]{1,64}\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-VELOCITY-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Velocity directive\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /#(?:set|parse|evaluate|macro|foreach|include)\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-DOTNET-REFLECTION-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Razor/.NET expression reaching a process or assembly-loading class\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// A bare Razor `@(` / `@{` delimiter rule was rejected: two characters of common\n\t\t// punctuation that matched Objective-C literals and `@media` queries. This\n\t\t// targets capability reach instead, mirroring how SSTI-OGNL-001 handles Java.\n\t\t// `Process\\.Start` is required literally — the spaced form matched the sentence\n\t\t// \"Our deployment process. Start(ing) Monday\".\n\t\tpattern:\n\t\t\t/\\bSystem\\.Diagnostics\\.Process|\\bProcess\\.Start\\s{0,8}\\(|\\bActivator\\.CreateInstance\\s{0,8}\\(|\\bAssembly\\.Load\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-NODE-REQUIRE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Node module-graph traversal reaching a process or filesystem module\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern:\n\t\t\t/\\bprocess\\s{0,8}\\.\\s{0,8}(?:mainModule|binding|constructor)\\b|\\brequire\\s{0,8}\\(\\s{0,8}[\"'](?:node:)?(?:child_process|fs|vm|os)[\"']/i,\n\t},\n\t{\n\t\tid: \"SSTI-SMARTY-PHP-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Smarty {php} tag\",\n\t\tcwe: 94,\n\t\tprimaryControl: \"Disable the {php} tag; it was removed entirely in Smarty 4\",\n\t\tpattern: /\\{\\s{0,8}\\/?\\s{0,8}php\\s{0,8}\\}/i,\n\t},\n];\n\n//#endregion\n\n//#region Prompt injection\n\n/** Repeated across every prompt-injection rule. */\nconst PROMPT_CONTROL =\n\t\"Keep untrusted content in a clearly delimited data channel, constrain tool scopes, and require confirmation for consequential tool calls\";\n\n/**\n * LLM prompt injection.\n *\n * These are the weakest signatures in the catalog by construction — prompt injection\n * is expressed in natural language, so no finite pattern set bounds it. They are\n * graded accordingly (mostly `medium` confidence) and are useful as telemetry and as\n * a tripwire for human review, never as an authorization decision.\n */\nexport const PROMPT_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PROMPT-OVERRIDE-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Instruction to disregard prior directives\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:ignore|disregard|forget|override|bypass)\\s+(?:all\\s+|any\\s+|the\\s+|your\\s+|previous\\s+|prior\\s+|earlier\\s+|above\\s+){0,3}(?:instructions?|prompts?|rules?|directions?|guidelines?|constraints?)\\b/i,\n\t},\n\t{\n\t\tid: \"PROMPT-DELIMITER-SPOOF-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Chat-template control token forged in user content\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern: /<\\|(?:im_start|im_end|endoftext|system|user|assistant|channel)\\|>/i,\n\t},\n\t{\n\t\tid: \"PROMPT-ROLE-SPOOF-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line beginning with a conversational role label\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern: /^\\s{0,8}(?:system|assistant|developer|tool)\\s{0,8}:/im,\n\t},\n\t{\n\t\tid: \"PROMPT-EXFIL-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Request to disclose the system prompt or hidden instructions\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:reveal|repeat|print|output|show|display|summarize)\\s+(?:me\\s+)?(?:your|the)\\s+(?:system\\s+|initial\\s+|original\\s+|hidden\\s+){0,2}(?:prompt|instructions?|rules?|directives?)\\b/i,\n\t},\n\t{\n\t\tid: \"PROMPT-TOOL-COERCION-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Directive coercing a specific tool or function invocation\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:you\\s+must|always|immediately|be\\s+sure\\s+to)\\s+(?:call|invoke|run|use|execute)\\s+(?:the\\s+)?[\\w.-]{1,64}\\s+(?:tool|function|command|api)\\b/i,\n\t},\n];\n\n//#endregion\n\n//#region Universal\n\n/**\n * Every sink. An invisible formatting character has no legitimate place in a value\n * bound to any of them.\n *\n * This list previously excluded `sql`, `shell`, `ldap` and the other machine-syntax\n * sinks, on the stated grounds that those \"get the stricter {@link UNIVERSAL_RULES}\n * bidi and control-character rules instead\". Mutation testing showed that reasoning was\n * wrong: the bidi rule matches only U+202A-202E and U+2066-2069, and the control-character\n * rule only C0/C1 — neither matches U+200B. A zero-width space inside a SQL keyword\n * therefore raised **no finding at all**, and `1 U<ZWSP>NION SELECT` scored zero.\n *\n * The severity stays `medium`/`medium` rather than rising, because at the human-text\n * sinks the same code points are orthographic in Persian and Hindi and structural in\n * every ZWJ emoji sequence. The finding is raised everywhere; the verdict is left to\n * scoring.\n */\nconst INVISIBLE_CONTEXTS = [\n\t\"general_text\",\n\t\"html\",\n\t\"identifier\",\n\t\"object_merge\",\n\t\"url\",\n\t\"url_parameter\",\n\t\"sql\",\n\t\"nosql\",\n\t\"shell\",\n\t\"filesystem\",\n\t\"ldap\",\n\t\"xpath\",\n\t\"xml\",\n\t\"template\",\n\t\"spreadsheet\",\n\t\"jwt\",\n\t\"http_header\",\n\t\"log\",\n\t\"llm_prompt\",\n] as const;\n\n/**\n * Rules that apply in every context, including `general_text`.\n *\n * The bar for membership is high: a rule belongs here only if there is no legitimate\n * reason for the character to appear in *any* user-supplied value. Bidirectional\n * overrides (the \"Trojan Source\" class, CVE-2021-42574), zero-width characters used\n * to spoof identity, and C0 control characters all clear it. SQL keywords and `../`\n * emphatically do not, which is why they are context-scoped instead.\n */\nexport const UNIVERSAL_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"UNICODE-BIDI-OVERRIDE-001\",\n\t\ttype: \"homoglyph\",\n\t\tcontexts: ALL_THREAT_CONTEXTS,\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription:\n\t\t\t\"Bidirectional override character — reorders rendered text away from its logical order\",\n\t\tcwe: 451,\n\t\tprimaryControl: \"Reject bidi controls outright, or render with them stripped\",\n\t\tpattern: /[\\u202a-\\u202e\\u2066-\\u2069]/,\n\t},\n\t{\n\t\tid: \"UNICODE-INVISIBLE-001\",\n\t\ttype: \"homoglyph\",\n\t\tcontexts: INVISIBLE_CONTEXTS,\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Zero-width or invisible formatting character\",\n\t\tcwe: 1007,\n\t\tprimaryControl: \"Strip default-ignorable code points before comparison or storage\",\n\t\tpattern: /[\\u00ad\\u200b-\\u200f\\u2060-\\u2064\\ufeff]/,\n\t},\n\t{\n\t\tid: \"UNICODE-INVISIBLE-IDENTIFIER-001\",\n\t\ttype: \"homoglyph\",\n\t\t// Scoped to identifiers alone, and graded harder than UNICODE-INVISIBLE-001.\n\t\t// ZWNJ and ZWJ are required for correct Persian and Hindi rendering and appear\n\t\t// in ordinary emoji sequences, so the general rule stays at medium/medium. In a\n\t\t// username, domain, or package name there is no such defence: an invisible\n\t\t// character exists only to make two different identifiers render alike.\n\t\tcontexts: [\"identifier\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Invisible formatting character in a protected identifier\",\n\t\tcwe: 1007,\n\t\tprimaryControl:\n\t\t\t\"Strip default-ignorable code points, then compare UTS #39 skeletons at registration time\",\n\t\tpattern: /[\\u00ad\\u200b-\\u200f\\u2060-\\u2064\\ufeff]/,\n\t},\n\t{\n\t\tid: \"CONTROL-CHAR-001\",\n\t\ttype: \"resource_abuse\",\n\t\tcontexts: ALL_THREAT_CONTEXTS,\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"C0/C1 control character other than tab, CR, or LF\",\n\t\tcwe: 74,\n\t\tprimaryControl: \"Reject or strip control characters at the trust boundary\",\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: detecting control characters is this rule's entire purpose\n\t\tpattern: /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/,\n\t},\n];\n\n//#endregion\n"],"mappings":"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ACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;AACD;;;;;;;;;;AAqDA,MAAa,kBAAyC;CACrD;EACC,IAAI;EACJ,MAAM;EACN,UAAU;EACV,UAAU;EACV,YAAY;EACZ,aACC;EACD,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU;GA9CX;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EA4BW;EACV,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EAMN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBACC;EACD,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU;EACV,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAEhB,SAAS;CACV;AACD"}
1
+ {"version":3,"file":"markup.mjs","names":[],"sources":["../../../src/threats/rules/markup.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 Document- and prompt-level signatures: XML/XXE, server-side template\n * and SSI injection, LLM prompt injection, plus the small set of universal rules that\n * apply in every context because no legitimate input needs them.\n *\n * XXE and SSTI are configuration weaknesses, not filtering ones. XXE is prevented by\n * disabling DTD processing and external entity resolution in the parser; SSTI is\n * prevented by passing untrusted values as template *data* rather than splicing them\n * into template *source*. Both rule groups exist to catch a payload that a\n * misconfigured parser or a `renderString(userInput)` call would otherwise execute.\n *\n * @module @resq-systems/security/threats/rules/markup\n */\n\nimport { ALL_THREAT_CONTEXTS, type ThreatRule } from \"../types.js\";\n\n//#region XML / XXE\n\n/** Repeated across every XML rule. */\nconst XML_CONTROL =\n\t\"Disable DTD processing and external entity resolution in the XML parser (noent:false, resolveExternals:false)\";\n\n/** XML external entity and entity-expansion signatures. */\nexport const XML_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"XML-ENTITY-DECL-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Inline ENTITY declaration\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!ENTITY\\b/i,\n\t},\n\t{\n\t\tid: \"XML-EXTERNAL-ID-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"SYSTEM or PUBLIC external identifier\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /\\b(?:SYSTEM|PUBLIC)\\s+[\"']/,\n\t},\n\t{\n\t\tid: \"XML-DOCTYPE-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"DOCTYPE declaration in untrusted XML\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!DOCTYPE\\b/i,\n\t},\n\t{\n\t\tid: \"XML-XINCLUDE-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"XInclude directive — an entity-free route to the same file read\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<\\s{0,8}xi:include\\b|www\\.w3\\.org\\/2001\\/XInclude/i,\n\t},\n\t{\n\t\tid: \"XML-CDATA-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"low\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"CDATA section, used to smuggle markup past naive filters\",\n\t\tcwe: 91,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!\\[CDATA\\[/,\n\t},\n];\n\n//#endregion\n\n//#region Template injection\n\n/** Repeated across most template-injection rules. */\nconst SSTI_CONTROL =\n\t\"Pass untrusted values as template data (context variables), never as template source; render with autoescaping on\";\n\n/** Server-side template injection and SSI directives. */\nexport const TEMPLATE_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"SSTI-SANDBOX-ESCAPE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Python object-graph traversal used to escape a template sandbox\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /\\b__(?:class|globals|subclasses|mro|builtins|import|base|init|reduce)__\\b/,\n\t},\n\t{\n\t\tid: \"SSTI-OGNL-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"OGNL/EL expression reaching a runtime execution class\",\n\t\tcwe: 917,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /_memberAccess|\\bRuntime\\s{0,8}\\.\\s{0,8}getRuntime|\\bProcessBuilder\\b|@java\\.lang\\b/i,\n\t},\n\t{\n\t\tid: \"SSTI-MUSTACHE-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Handlebars/Jinja/Twig expression delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// The inner class excludes `{` as well as `}`. With only `}` excluded, an\n\t\t// input of 100 000 `{` characters makes the engine consume 200 of them at\n\t\t// every start position and then backtrack — ~40ms per scan, measured by\n\t\t// tests/regex-safety.test.ts. Excluding `{` makes the failure immediate, and\n\t\t// costs nothing: a template expression does not contain a bare `{`.\n\t\tpattern: /\\{\\{[^{}]{0,200}\\}\\}/,\n\t},\n\t{\n\t\tid: \"SSTI-STATEMENT-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Jinja/Twig statement delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /\\{%[^%]{0,200}%\\}/,\n\t},\n\t{\n\t\tid: \"SSTI-EL-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"JSP EL, Freemarker, or JS template-literal interpolation\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// `{` excluded from the inner class for the same reason as the rule above.\n\t\tpattern: /[$#]\\{[^{}]{0,200}\\}/,\n\t},\n\t{\n\t\tid: \"SSI-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\", \"html\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Server-side include directive\",\n\t\tcwe: 97,\n\t\tprimaryControl:\n\t\t\t\"Disable server-side includes, or serve untrusted content from a path with SSI turned off\",\n\t\tpattern: /<!--#\\s{0,8}(?:exec|include|echo|config|fsize|flastmod|printenv|set)\\b/i,\n\t},\n\t{\n\t\tid: \"SSTI-SCRIPTLET-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\t// `template` only. A scriptlet spliced into an already-rendered response does\n\t\t// not execute — only template *source* is an execution path. SSI-DIRECTIVE-001\n\t\t// is in both because Apache and nginx post-process served HTML; no equivalent\n\t\t// stage exists for ERB, JSP, ASP, or EJS.\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"ERB / JSP / ASP / EJS scriptlet delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /<%[=\\-@#!]|<%[^%]{0,200}%>/,\n\t},\n\t{\n\t\tid: \"SSTI-FREEMARKER-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"FreeMarker directive or user-defined directive call\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern:\n\t\t\t/<#\\s{0,8}(?:assign|list|if|else|import|include|macro|function|setting|attempt|global|local|nested|recurse|switch|visit|compress|noparse|outputformat)\\b|<@[\\w.$]{1,64}\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-VELOCITY-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Velocity directive\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /#(?:set|parse|evaluate|macro|foreach|include)\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-DOTNET-REFLECTION-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Razor/.NET expression reaching a process or assembly-loading class\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// A bare Razor `@(` / `@{` delimiter rule was rejected: two characters of common\n\t\t// punctuation that matched Objective-C literals and `@media` queries. This\n\t\t// targets capability reach instead, mirroring how SSTI-OGNL-001 handles Java.\n\t\t// `Process\\.Start` is required literally — the spaced form matched the sentence\n\t\t// \"Our deployment process. Start(ing) Monday\".\n\t\tpattern:\n\t\t\t/\\bSystem\\.Diagnostics\\.Process|\\bProcess\\.Start\\s{0,8}\\(|\\bActivator\\.CreateInstance\\s{0,8}\\(|\\bAssembly\\.Load\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-NODE-REQUIRE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Node module-graph traversal reaching a process or filesystem module\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern:\n\t\t\t/\\bprocess\\s{0,8}\\.\\s{0,8}(?:mainModule|binding|constructor)\\b|\\brequire\\s{0,8}\\(\\s{0,8}[\"'](?:node:)?(?:child_process|fs|vm|os)[\"']/i,\n\t},\n\t{\n\t\tid: \"SSTI-SMARTY-PHP-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Smarty {php} tag\",\n\t\tcwe: 94,\n\t\tprimaryControl: \"Disable the {php} tag; it was removed entirely in Smarty 4\",\n\t\tpattern: /\\{\\s{0,8}\\/?\\s{0,8}php\\s{0,8}\\}/i,\n\t},\n];\n\n//#endregion\n\n//#region Prompt injection\n\n/** Repeated across every prompt-injection rule. */\nconst PROMPT_CONTROL =\n\t\"Keep untrusted content in a clearly delimited data channel, constrain tool scopes, and require confirmation for consequential tool calls\";\n\n/**\n * LLM prompt injection.\n *\n * These are the weakest signatures in the catalog by construction — prompt injection\n * is expressed in natural language, so no finite pattern set bounds it. They are\n * graded accordingly (mostly `medium` confidence) and are useful as telemetry and as\n * a tripwire for human review, never as an authorization decision.\n */\nexport const PROMPT_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PROMPT-OVERRIDE-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Instruction to disregard prior directives\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:ignore|disregard|forget|override|bypass)\\s+(?:all\\s+|any\\s+|the\\s+|your\\s+|previous\\s+|prior\\s+|earlier\\s+|above\\s+){0,3}(?:instructions?|prompts?|rules?|directions?|guidelines?|constraints?)\\b/i,\n\t},\n\t{\n\t\tid: \"PROMPT-DELIMITER-SPOOF-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Chat-template control token forged in user content\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern: /<\\|(?:im_start|im_end|endoftext|system|user|assistant|channel)\\|>/i,\n\t},\n\t{\n\t\tid: \"PROMPT-ROLE-SPOOF-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line beginning with a conversational role label\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\t// The leading run is unbounded, because a reader that strips indentation strips all\n\t\t// of it, and horizontal. Under `/m`, `^` matches after every line terminator, so a\n\t\t// run that could cross one would rescan the rest of the input from each line start\n\t\t// and go quadratic. `\\s` holds all four terminators (LF, CR, U+2028, U+2029), so the\n\t\t// class removes each; the run then never leaves its line and the scan stays linear.\n\t\tpattern: /^[^\\S\\r\\n\\u2028\\u2029]*(?:system|assistant|developer|tool)\\s{0,8}:/im,\n\t},\n\t{\n\t\tid: \"PROMPT-EXFIL-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Request to disclose the system prompt or hidden instructions\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:reveal|repeat|print|output|show|display|summarize)\\s+(?:me\\s+)?(?:your|the)\\s+(?:system\\s+|initial\\s+|original\\s+|hidden\\s+){0,2}(?:prompt|instructions?|rules?|directives?)\\b/i,\n\t},\n\t{\n\t\tid: \"PROMPT-TOOL-COERCION-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Directive coercing a specific tool or function invocation\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:you\\s+must|always|immediately|be\\s+sure\\s+to)\\s+(?:call|invoke|run|use|execute)\\s+(?:the\\s+)?[\\w.-]{1,64}\\s+(?:tool|function|command|api)\\b/i,\n\t},\n];\n\n//#endregion\n\n//#region Universal\n\n/**\n * Every sink. An invisible formatting character has no legitimate place in a value\n * bound to any of them.\n *\n * This list previously excluded `sql`, `shell`, `ldap` and the other machine-syntax\n * sinks, on the stated grounds that those \"get the stricter {@link UNIVERSAL_RULES}\n * bidi and control-character rules instead\". Mutation testing showed that reasoning was\n * wrong: the bidi rule matches only U+202A-202E and U+2066-2069, and the control-character\n * rule only C0/C1 — neither matches U+200B. A zero-width space inside a SQL keyword\n * therefore raised **no finding at all**, and `1 U<ZWSP>NION SELECT` scored zero.\n *\n * The severity stays `medium`/`medium` rather than rising, because at the human-text\n * sinks the same code points are orthographic in Persian and Hindi and structural in\n * every ZWJ emoji sequence. The finding is raised everywhere; the verdict is left to\n * scoring.\n */\nconst INVISIBLE_CONTEXTS = [\n\t\"general_text\",\n\t\"html\",\n\t\"identifier\",\n\t\"object_merge\",\n\t\"url\",\n\t\"url_parameter\",\n\t\"sql\",\n\t\"nosql\",\n\t\"shell\",\n\t\"filesystem\",\n\t\"ldap\",\n\t\"xpath\",\n\t\"xml\",\n\t\"template\",\n\t\"spreadsheet\",\n\t\"jwt\",\n\t\"http_header\",\n\t\"log\",\n\t\"llm_prompt\",\n] as const;\n\n/**\n * Rules that apply in every context, including `general_text`.\n *\n * The bar for membership is high: a rule belongs here only if there is no legitimate\n * reason for the character to appear in *any* user-supplied value. Bidirectional\n * overrides (the \"Trojan Source\" class, CVE-2021-42574), zero-width characters used\n * to spoof identity, and C0 control characters all clear it. SQL keywords and `../`\n * emphatically do not, which is why they are context-scoped instead.\n */\nexport const UNIVERSAL_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"UNICODE-BIDI-OVERRIDE-001\",\n\t\ttype: \"homoglyph\",\n\t\tcontexts: ALL_THREAT_CONTEXTS,\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription:\n\t\t\t\"Bidirectional override character — reorders rendered text away from its logical order\",\n\t\tcwe: 451,\n\t\tprimaryControl: \"Reject bidi controls outright, or render with them stripped\",\n\t\tpattern: /[\\u202a-\\u202e\\u2066-\\u2069]/,\n\t},\n\t{\n\t\tid: \"UNICODE-INVISIBLE-001\",\n\t\ttype: \"homoglyph\",\n\t\tcontexts: INVISIBLE_CONTEXTS,\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Zero-width or invisible formatting character\",\n\t\tcwe: 1007,\n\t\tprimaryControl: \"Strip default-ignorable code points before comparison or storage\",\n\t\tpattern: /[\\u00ad\\u200b-\\u200f\\u2060-\\u2064\\ufeff]/,\n\t},\n\t{\n\t\tid: \"UNICODE-INVISIBLE-IDENTIFIER-001\",\n\t\ttype: \"homoglyph\",\n\t\t// Scoped to identifiers alone, and graded harder than UNICODE-INVISIBLE-001.\n\t\t// ZWNJ and ZWJ are required for correct Persian and Hindi rendering and appear\n\t\t// in ordinary emoji sequences, so the general rule stays at medium/medium. In a\n\t\t// username, domain, or package name there is no such defence: an invisible\n\t\t// character exists only to make two different identifiers render alike.\n\t\tcontexts: [\"identifier\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Invisible formatting character in a protected identifier\",\n\t\tcwe: 1007,\n\t\tprimaryControl:\n\t\t\t\"Strip default-ignorable code points, then compare UTS #39 skeletons at registration time\",\n\t\tpattern: /[\\u00ad\\u200b-\\u200f\\u2060-\\u2064\\ufeff]/,\n\t},\n\t{\n\t\tid: \"CONTROL-CHAR-001\",\n\t\ttype: \"resource_abuse\",\n\t\tcontexts: ALL_THREAT_CONTEXTS,\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"C0/C1 control character other than tab, CR, or LF\",\n\t\tcwe: 74,\n\t\tprimaryControl: \"Reject or strip control characters at the trust boundary\",\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: detecting control characters is this rule's entire purpose\n\t\tpattern: /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/,\n\t},\n];\n\n//#endregion\n"],"mappings":"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EACb,KAAK;EACL,gBAAgB;EAMhB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;AACD;;;;;;;;;;AAqDA,MAAa,kBAAyC;CACrD;EACC,IAAI;EACJ,MAAM;EACN,UAAU;EACV,UAAU;EACV,YAAY;EACZ,aACC;EACD,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU;GA9CX;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EA4BW;EACV,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EAMN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBACC;EACD,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU;EACV,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAEhB,SAAS;CACV;AACD"}
@@ -9,7 +9,7 @@ import { ThreatRule } from "../types.mjs";
9
9
  * duplicate keys are legal and routine (`?tag=a&tag=b`). That half is a parsing
10
10
  * decision, documented as such rather than faked with a signature.
11
11
  */
12
- declare const PARAMETER_POLLUTION_RULES: readonly ThreatRule[];
12
+ export declare const PARAMETER_POLLUTION_RULES: readonly ThreatRule[];
13
13
  /**
14
14
  * Credential material appearing where it will be stored, logged, or sent onward.
15
15
  *
@@ -18,7 +18,7 @@ declare const PARAMETER_POLLUTION_RULES: readonly ThreatRule[];
18
18
  * `"api_key: required for this endpoint"` and `"validation failed: password: too_short"`.
19
19
  * Shipping it weakened would trade the category's credibility for coverage.
20
20
  */
21
- declare const CREDENTIAL_EXPOSURE_RULES: readonly ThreatRule[];
21
+ export declare const CREDENTIAL_EXPOSURE_RULES: readonly ThreatRule[];
22
22
  /**
23
23
  * Double percent-encoding.
24
24
  *
@@ -32,7 +32,7 @@ declare const CREDENTIAL_EXPOSURE_RULES: readonly ThreatRule[];
32
32
  * one decoding pass" — is itself the finding. The real fix is to decode exactly once,
33
33
  * at a defined boundary, and never again.
34
34
  */
35
- declare const DOUBLE_ENCODING_RULES: readonly ThreatRule[];
35
+ export declare const DOUBLE_ENCODING_RULES: readonly ThreatRule[];
36
36
  /**
37
37
  * JWT tampering.
38
38
  *
@@ -43,7 +43,6 @@ declare const DOUBLE_ENCODING_RULES: readonly ThreatRule[];
43
43
  * opaque token with every detector would be the run-everything-against-everything
44
44
  * anti-pattern the engine exists to avoid.
45
45
  */
46
- declare const JWT_RULES: readonly ThreatRule[];
46
+ export declare const JWT_RULES: readonly ThreatRule[];
47
47
  //#endregion
48
- export { CREDENTIAL_EXPOSURE_RULES, DOUBLE_ENCODING_RULES, JWT_RULES, PARAMETER_POLLUTION_RULES };
49
48
  //# sourceMappingURL=protocol.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.mts","names":[],"sources":["../../../src/threats/rules/protocol.ts"],"mappings":";;;;;;;;;;;cA0Ca,oCAAoC;;;;;;;;;cA2DpC,oCAAoC;;;;;;;;;;;;;;cAwEpC,gCAAgC;;;;;;;;;;;cAyDhC,oBAAoB"}
1
+ {"version":3,"file":"protocol.d.mts","names":[],"sources":["../../../src/threats/rules/protocol.ts"],"mappings":";;;;;;;;;;;qBA2Ca,oCAAoC;;;;;;;;;qBA2DpC,oCAAoC;;;;;;;;;;;;;;qBAwEpC,gCAAgC;;;;;;;;;;;qBAyDhC,oBAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.mjs","names":[],"sources":["../../../src/threats/rules/protocol.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 Protocol- and token-level signatures: HTTP parameter pollution,\n * credential exposure, and JWT tampering.\n *\n * `credential_exposure` inverts the direction of every other category in the catalog.\n * The rest detect a hostile value arriving; these detect the application's own secret\n * *leaving* — in a URL it is about to fetch, or a line it is about to log. Both are\n * outbound sinks, so the context model still fits, but the user-facing message must\n * not accuse the submitter of anything.\n *\n * @module @resq-systems/security/threats/rules/protocol\n */\n\nimport type { ThreatRule } from \"../types.js\";\n\n//#region HTTP parameter pollution\n\n/**\n * Parameter pollution.\n *\n * Only the *injected-parameter* half is detectable here. Duplicate-key pollution —\n * `?id=1&id=2` exploiting parser disagreement between ASP.NET, PHP, and Servlet\n * stacks — is not: the engine sees one string and never the parameter map, and\n * duplicate keys are legal and routine (`?tag=a&tag=b`). That half is a parsing\n * decision, documented as such rather than faked with a signature.\n */\nexport const PARAMETER_POLLUTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PARAM-POLLUTION-PRIVILEGED-001\",\n\t\ttype: \"parameter_pollution\",\n\t\t// `url_parameter`, never `url`. In a whole URL `&role=` is ordinary grammar;\n\t\t// inside a single value about to be concatenated into a query string it is an\n\t\t// injected parameter. Attaching these to `url` produced measured false positives\n\t\t// on `?sku=99&color=red` and on OAuth authorize URLs.\n\t\tcontexts: [\"url_parameter\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Injected parameter naming a privilege, price, or credential field\",\n\t\tcwe: 235,\n\t\tprimaryControl:\n\t\t\t\"Build query strings with URLSearchParams or encodeURIComponent per value; read parameters through one parser and reject duplicate keys server-side\",\n\t\t// `client_id`, `scope`, `state`, `next`, `redirect`, `callback`, and `lang` are\n\t\t// deliberately absent — that exclusion is what lets nested OAuth URLs pass.\n\t\tpattern:\n\t\t\t/[&;][a-z0-9_.[-]{0,24}(?:role|is_?admin|admin|user_?id|userid|uid|account_?id|amount|price|total|quantity|qty|access_?token|api_?key|apikey|client_?secret|secret|password|passwd|pwd|session_?id|sessionid|signature|hmac|permissions?|privilege|is_?staff|superuser|debug)[\\]\"']{0,2}\\s{0,4}=/i,\n\t},\n\t{\n\t\tid: \"PARAM-POLLUTION-APPEND-001\",\n\t\ttype: \"parameter_pollution\",\n\t\tcontexts: [\"url_parameter\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Value appends an additional query parameter\",\n\t\tcwe: 235,\n\t\tprimaryControl:\n\t\t\t\"Build query strings with URLSearchParams or encodeURIComponent per value; read parameters through one parser and reject duplicate keys server-side\",\n\t\t// `&`-only: including `;` fired on connection strings (`Server=a;Database=b`),\n\t\t// so `;`-separated pollution against a non-privileged name stays undetected.\n\t\t// The `^[^?]{0,256}` guard keeps nested `redirect_uri` values from firing, but\n\t\t// it is positional — a bare sub-query with no leading `?` still matches. Graded\n\t\t// medium/low (1.0) so it never reaches review on its own.\n\t\tpattern: /^[^?]{0,256}&[a-z_][a-z0-9_.-]{1,48}(?:\\[[a-z0-9_.-]{0,32}\\])?\\s{0,4}=/i,\n\t},\n];\n\n//#endregion\n\n//#region Credential exposure\n\n/** Repeated across the URL-borne credential rules. */\nconst CRED_URL_CONTROL =\n\t\"Carry credentials in the Authorization header or a POST body, never a query string; set Referrer-Policy: no-referrer and rotate any token that reached a URL\";\n\n/** Repeated across the log-borne credential rules. */\nconst CRED_LOG_CONTROL =\n\t\"Log structured records with an allowlist of loggable fields; redact by key with safeStringify\";\n\n/**\n * Credential material appearing where it will be stored, logged, or sent onward.\n *\n * A generic `password=…` / `api_key: …` assignment rule was written and then dropped:\n * it produced false positives on every realistic log line of the form\n * `\"api_key: required for this endpoint\"` and `\"validation failed: password: too_short\"`.\n * Shipping it weakened would trade the category's credibility for coverage.\n */\nexport const CREDENTIAL_EXPOSURE_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"CRED-URL-QUERY-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"url\", \"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Credential-bearing query parameter — leaks via Referer, proxies, and logs\",\n\t\tcwe: 598,\n\t\tprimaryControl: CRED_URL_CONTROL,\n\t\t// `sid`, `auth`, `key`, and bare `token` are excluded: `?sid=` is routinely a\n\t\t// store identifier. `sig`/`signature`/`X-Amz-*` are excluded because S3\n\t\t// presigned URLs legitimately carry them.\n\t\tpattern:\n\t\t\t/[?&](?:access[_-]?token|auth[_-]?token|id[_-]?token|refresh[_-]?token|api[_-]?key|api[_-]?secret|client[_-]?secret|secret[_-]?key|session[_-]?id|session[_-]?token|jsessionid|phpsessid|password|passwd|pwd)=[^&#\\s]{4,256}/i,\n\t},\n\t{\n\t\tid: \"CRED-AUTH-SCHEME-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Authorization header value written to a log\",\n\t\tcwe: 532,\n\t\tprimaryControl: CRED_LOG_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:proxy-)?authorization\\s{0,4}:\\s{0,4}(?:bearer|basic|digest|token|apikey)\\s{0,4}[A-Za-z0-9+/=._~-]{8,512}/i,\n\t},\n\t{\n\t\tid: \"CRED-JWT-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"url\", \"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"JSON Web Token in a URL or log line\",\n\t\tcwe: 522,\n\t\tprimaryControl: CRED_URL_CONTROL,\n\t\tpattern: /\\beyJ[A-Za-z0-9_-]{10,400}\\.eyJ[A-Za-z0-9_-]{10,400}\\./,\n\t},\n\t{\n\t\tid: \"CRED-PROVIDER-KEY-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"url\", \"log\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Provider API key or personal access token\",\n\t\tcwe: 532,\n\t\tprimaryControl:\n\t\t\t\"Load provider keys from a secret manager, never from source or a URL; rotate immediately on exposure\",\n\t\t// Case-sensitive by design — an `i` flag collides with ordinary hex digests.\n\t\tpattern:\n\t\t\t/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b|\\bgh[pousr]_[A-Za-z0-9]{36}\\b|\\bgithub_pat_[A-Za-z0-9_]{22,82}\\b|\\bxox[abprs]-[A-Za-z0-9-]{10,120}|\\bsk_live_[A-Za-z0-9]{16,64}\\b|\\bsk-(?:ant|proj)-[A-Za-z0-9_-]{20,120}/,\n\t},\n];\n\n//#endregion\n\n//#region Double encoding\n\n/**\n * Double percent-encoding.\n *\n * `%253Cscript%253E` decodes once to `%3Cscript%3E` and twice to `<script>`. The\n * scanner decodes once, so the payload was invisible to every signature — verified at\n * 0/allow before this rule existed. The traversal case had an explicit\n * `%252e%252e%252f` rule; nothing covered the general class.\n *\n * Its own category rather than a subtype of whatever it eventually decodes to: at scan\n * time the eventual sink is unknown, and the signal — \"this input is shaped to survive\n * one decoding pass\" — is itself the finding. The real fix is to decode exactly once,\n * at a defined boundary, and never again.\n */\nexport const DOUBLE_ENCODING_RULES: readonly ThreatRule[] = [\n\t{\n\t\t// The sibling rule below matches the form where only the percent sign is\n\t\t// re-encoded. Encoding the hex digits as well yields the same byte after two\n\t\t// decodes, and is invisible to a pattern that expects those digits to follow the\n\t\t// escaped percent literally. Measured: the half-encoded spelling of a script tag\n\t\t// scored review, while the fully-encoded spelling of the identical payload\n\t\t// scored zero.\n\t\tid: \"ENCODING-DOUBLE-PERCENT-002\",\n\t\ttype: \"double_encoding\",\n\t\tcontexts: [\"url\", \"url_parameter\", \"html\", \"filesystem\", \"sql\", \"http_header\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Percent sign and its hex digits each encoded separately\",\n\t\tcwe: 177,\n\t\tprimaryControl:\n\t\t\t\"Decode exactly once at a defined boundary and never re-decode; reject input that still contains an escape prefix after decoding\",\n\t\tpattern: /%25%[0-9a-f]{2}%[0-9a-f]{2}/i,\n\t},\n\n\t{\n\t\tid: \"ENCODING-DOUBLE-PERCENT-001\",\n\t\ttype: \"double_encoding\",\n\t\tcontexts: [\"url\", \"url_parameter\", \"html\", \"filesystem\", \"sql\", \"http_header\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Escape prefix that decodes to a metacharacter only after a second pass\",\n\t\tcwe: 177,\n\t\tprimaryControl:\n\t\t\t\"Decode exactly once at a defined boundary and never re-decode; reject input that still contains an escape prefix after decoding\",\n\t\t// Only sequences decoding to a character with syntactic meaning: space, quote,\n\t\t// percent, apostrophe, parens, dot, slash, angle brackets, semicolon, ampersand,\n\t\t// equals, backslash, NUL, CR, LF. A bare `%25` is not enough — \"100%25 off\" is\n\t\t// an ordinary encoded string and must not fire.\n\t\tvariants: [\"raw\"],\n\t\tpattern: /%25(?:2[0257CEFcef]|3[CEce]|22|26|27|28|29|3[BbDd]|5[Cc]|00|0[ADad])/,\n\t},\n];\n\n//#endregion\n\n//#region JWT\n\n/** Repeated across the JWT rules. */\nconst JWT_CONTROL =\n\t\"Verify with an explicit algorithm allowlist (jwtVerify(token, key, { algorithms: ['RS256'] })); never let the token's own header select the algorithm, and reject 'none' unconditionally\";\n\n/**\n * JWT tampering.\n *\n * Scoped to the unsecured-token case alone. `kid`, `jku`, and `x5u` injection are\n * *already covered* — extract the claim and declare its real sink, and the existing\n * rules fire: `kid=../../../../dev/null` with `filesystem` hits PATH-TRAVERSAL-001,\n * `jku=http://169.254.169.254/` with `url` hits SSRF-METADATA-001. Re-scanning a whole\n * opaque token with every detector would be the run-everything-against-everything\n * anti-pattern the engine exists to avoid.\n */\nexport const JWT_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"JWT-ALG-NONE-UNSECURED-001\",\n\t\ttype: \"jwt_tampering\",\n\t\tcontexts: [\"jwt\", \"http_header\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Unsecured JWT — three segments with an empty signature\",\n\t\tcwe: 347,\n\t\tprimaryControl: JWT_CONTROL,\n\t\t// Structural, not lexical. RFC 7519 §6.1 requires an empty signature segment,\n\t\t// so this is immune to base64 alignment, key ordering, whitespace, and `alg`\n\t\t// casing — all of which change how `\"alg\":\"none\"` encodes (`hbGciOiJub25l`,\n\t\t// `YWxnIjoibm9uZ`, `ImFsZyI6Im5vbmUi`, depending on offset mod 3).\n\t\t// Requiring `eyJ` on *both* segments, not just `ey`, removes false positives on\n\t\t// filenames such as `eyewitness_statement_final.v2.`.\n\t\tpattern:\n\t\t\t/(?:^|[\\s,;=(\"'[])eyJ[A-Za-z0-9_-]{16,2000}\\.eyJ[A-Za-z0-9_-]{8,4000}\\.(?:$|[\\s,;)\"'\\]&#])/,\n\t},\n\t{\n\t\tid: \"JWT-ALG-NONE-HEADER-001\",\n\t\ttype: \"jwt_tampering\",\n\t\tcontexts: [\"jwt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Decoded JWT header selecting the 'none' algorithm\",\n\t\tcwe: 347,\n\t\tprimaryControl: JWT_CONTROL,\n\t\t// Covers what the structural rule cannot: `alg:none` carrying a non-empty\n\t\t// signature, which naive verifiers still accept.\n\t\tpattern: /\"alg\"\\s{0,8}:\\s{0,8}\"\\s{0,8}none\\s{0,8}\"/i,\n\t},\n];\n\n//#endregion\n"],"mappings":";;;;;;;;;;AA0CA,MAAa,4BAAmD,CAC/D;CACC,IAAI;CACJ,MAAM;CAKN,UAAU,CAAC,eAAe;CAC1B,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CAGD,SACC;AACF,GACA;CACC,IAAI;CACJ,MAAM;CACN,UAAU,CAAC,eAAe;CAC1B,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CAMD,SAAS;AACV,CACD;;AAOA,MAAM,mBACL;;;;;;;;;AAcD,MAAa,4BAAmD;CAC/D;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,OAAO,KAAK;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAIhB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,KAAK;EAChB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,OAAO,KAAK;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,OAAO,KAAK;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBACC;EAED,SACC;CACF;AACD;;;;;;;;;;;;;;AAmBA,MAAa,wBAA+C,CAC3D;CAOC,IAAI;CACJ,MAAM;CACN,UAAU;EAAC;EAAO;EAAiB;EAAQ;EAAc;EAAO;EAAe;CAAK;CACpF,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CACD,SAAS;AACV,GAEA;CACC,IAAI;CACJ,MAAM;CACN,UAAU;EAAC;EAAO;EAAiB;EAAQ;EAAc;EAAO;EAAe;CAAK;CACpF,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CAKD,UAAU,CAAC,KAAK;CAChB,SAAS;AACV,CACD;;AAOA,MAAM,cACL;;;;;;;;;;;AAYD,MAAa,YAAmC,CAC/C;CACC,IAAI;CACJ,MAAM;CACN,UAAU,CAAC,OAAO,aAAa;CAC/B,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBAAgB;CAOhB,SACC;AACF,GACA;CACC,IAAI;CACJ,MAAM;CACN,UAAU,CAAC,KAAK;CAChB,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBAAgB;CAGhB,SAAS;AACV,CACD"}
1
+ {"version":3,"file":"protocol.mjs","names":[],"sources":["../../../src/threats/rules/protocol.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 Protocol- and token-level signatures: HTTP parameter pollution,\n * credential exposure, and JWT tampering.\n *\n * `credential_exposure` inverts the direction of every other category in the catalog.\n * The rest detect a hostile value arriving; these detect the application's own secret\n * *leaving* — in a URL it is about to fetch, or a line it is about to log. Both are\n * outbound sinks, so the context model still fits, but the user-facing message must\n * not accuse the submitter of anything.\n *\n * @module @resq-systems/security/threats/rules/protocol\n */\n\nimport type { ThreatRule } from \"../types.js\";\n\n//#region HTTP parameter pollution\n\n/**\n * Parameter pollution.\n *\n * Only the *injected-parameter* half is detectable here. Duplicate-key pollution —\n * `?id=1&id=2` exploiting parser disagreement between ASP.NET, PHP, and Servlet\n * stacks — is not: the engine sees one string and never the parameter map, and\n * duplicate keys are legal and routine (`?tag=a&tag=b`). That half is a parsing\n * decision, documented as such rather than faked with a signature.\n */\nexport const PARAMETER_POLLUTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PARAM-POLLUTION-PRIVILEGED-001\",\n\t\ttype: \"parameter_pollution\",\n\t\t// `url_parameter`, never `url`. In a whole URL `&role=` is ordinary grammar;\n\t\t// inside a single value about to be concatenated into a query string it is an\n\t\t// injected parameter. Attaching these to `url` produced measured false positives\n\t\t// on `?sku=99&color=red` and on OAuth authorize URLs.\n\t\tcontexts: [\"url_parameter\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Injected parameter naming a privilege, price, or credential field\",\n\t\tcwe: 235,\n\t\tprimaryControl:\n\t\t\t\"Build query strings with URLSearchParams or encodeURIComponent per value; read parameters through one parser and reject duplicate keys server-side\",\n\t\t// `client_id`, `scope`, `state`, `next`, `redirect`, `callback`, and `lang` are\n\t\t// deliberately absent — that exclusion is what lets nested OAuth URLs pass.\n\t\tpattern:\n\t\t\t/[&;][a-z0-9_.[-]{0,24}(?:role|is_?admin|admin|user_?id|userid|uid|account_?id|amount|price|total|quantity|qty|access_?token|api_?key|apikey|client_?secret|secret|password|passwd|pwd|session_?id|sessionid|signature|hmac|permissions?|privilege|is_?staff|superuser|debug)[\\]\"']{0,2}\\s{0,4}=/i,\n\t},\n\t{\n\t\tid: \"PARAM-POLLUTION-APPEND-001\",\n\t\ttype: \"parameter_pollution\",\n\t\tcontexts: [\"url_parameter\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Value appends an additional query parameter\",\n\t\tcwe: 235,\n\t\tprimaryControl:\n\t\t\t\"Build query strings with URLSearchParams or encodeURIComponent per value; read parameters through one parser and reject duplicate keys server-side\",\n\t\t// `&`-only: including `;` fired on connection strings (`Server=a;Database=b`),\n\t\t// so `;`-separated pollution against a non-privileged name stays undetected.\n\t\t// The `^[^?]{0,256}` guard keeps nested `redirect_uri` values from firing, but\n\t\t// it is positional — a bare sub-query with no leading `?` still matches. Graded\n\t\t// medium/low (1.0) so it never reaches review on its own.\n\t\tpattern: /^[^?]{0,256}&[a-z_][a-z0-9_.-]{1,48}(?:\\[[a-z0-9_.-]{0,32}\\])?\\s{0,4}=/i,\n\t},\n];\n\n//#endregion\n\n//#region Credential exposure\n\n/** Repeated across the URL-borne credential rules. */\nconst CRED_URL_CONTROL =\n\t\"Carry credentials in the Authorization header or a POST body, never a query string; set Referrer-Policy: no-referrer and rotate any token that reached a URL\";\n\n/** Repeated across the log-borne credential rules. */\nconst CRED_LOG_CONTROL =\n\t\"Log structured records with an allowlist of loggable fields; redact by key with safeStringify\";\n\n/**\n * Credential material appearing where it will be stored, logged, or sent onward.\n *\n * A generic `password=…` / `api_key: …` assignment rule was written and then dropped:\n * it produced false positives on every realistic log line of the form\n * `\"api_key: required for this endpoint\"` and `\"validation failed: password: too_short\"`.\n * Shipping it weakened would trade the category's credibility for coverage.\n */\nexport const CREDENTIAL_EXPOSURE_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"CRED-URL-QUERY-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"url\", \"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Credential-bearing query parameter — leaks via Referer, proxies, and logs\",\n\t\tcwe: 598,\n\t\tprimaryControl: CRED_URL_CONTROL,\n\t\t// `sid`, `auth`, `key`, and bare `token` are excluded: `?sid=` is routinely a\n\t\t// store identifier. `sig`/`signature`/`X-Amz-*` are excluded because S3\n\t\t// presigned URLs legitimately carry them.\n\t\tpattern:\n\t\t\t/[?&](?:access[_-]?token|auth[_-]?token|id[_-]?token|refresh[_-]?token|api[_-]?key|api[_-]?secret|client[_-]?secret|secret[_-]?key|session[_-]?id|session[_-]?token|jsessionid|phpsessid|password|passwd|pwd)=[^&#\\s]{4,256}/i,\n\t},\n\t{\n\t\tid: \"CRED-AUTH-SCHEME-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Authorization header value written to a log\",\n\t\tcwe: 532,\n\t\tprimaryControl: CRED_LOG_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:proxy-)?authorization\\s{0,4}:\\s{0,4}(?:bearer|basic|digest|token|apikey)\\s{0,4}[A-Za-z0-9+/=._~-]{8,512}/i,\n\t},\n\t{\n\t\tid: \"CRED-JWT-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"url\", \"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"JSON Web Token in a URL or log line\",\n\t\tcwe: 522,\n\t\tprimaryControl: CRED_URL_CONTROL,\n\t\tpattern: /\\beyJ[A-Za-z0-9_-]{10,400}\\.eyJ[A-Za-z0-9_-]{10,400}\\./,\n\t},\n\t{\n\t\tid: \"CRED-PROVIDER-KEY-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"url\", \"log\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Provider API key or personal access token\",\n\t\tcwe: 532,\n\t\tprimaryControl:\n\t\t\t\"Load provider keys from a secret manager, never from source or a URL; rotate immediately on exposure\",\n\t\t// Case-sensitive by design — an `i` flag collides with ordinary hex digests.\n\t\tpattern:\n\t\t\t/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b|\\bgh[pousr]_[A-Za-z0-9]{36}\\b|\\bgithub_pat_[A-Za-z0-9_]{22,82}\\b|\\bxox[abprs]-[A-Za-z0-9-]{10,120}|\\bsk_live_[A-Za-z0-9]{16,64}\\b|\\bsk-(?:ant|proj)-[A-Za-z0-9_-]{20,120}/,\n\t},\n];\n\n//#endregion\n\n//#region Double encoding\n\n/**\n * Double percent-encoding.\n *\n * `%253Cscript%253E` decodes once to `%3Cscript%3E` and twice to `<script>`. The\n * scanner decodes once, so the payload was invisible to every signature — verified at\n * 0/allow before this rule existed. The traversal case had an explicit\n * `%252e%252e%252f` rule; nothing covered the general class.\n *\n * Its own category rather than a subtype of whatever it eventually decodes to: at scan\n * time the eventual sink is unknown, and the signal — \"this input is shaped to survive\n * one decoding pass\" — is itself the finding. The real fix is to decode exactly once,\n * at a defined boundary, and never again.\n */\nexport const DOUBLE_ENCODING_RULES: readonly ThreatRule[] = [\n\t{\n\t\t// The sibling rule below matches the form where only the percent sign is\n\t\t// re-encoded. Encoding the hex digits as well yields the same byte after two\n\t\t// decodes, and is invisible to a pattern that expects those digits to follow the\n\t\t// escaped percent literally. Measured: the half-encoded spelling of a script tag\n\t\t// scored review, while the fully-encoded spelling of the identical payload\n\t\t// scored zero.\n\t\tid: \"ENCODING-DOUBLE-PERCENT-002\",\n\t\ttype: \"double_encoding\",\n\t\tcontexts: [\"url\", \"url_parameter\", \"html\", \"filesystem\", \"sql\", \"http_header\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Percent sign and its hex digits each encoded separately\",\n\t\tcwe: 177,\n\t\tprimaryControl:\n\t\t\t\"Decode exactly once at a defined boundary and never re-decode; reject input that still contains an escape prefix after decoding\",\n\t\tpattern: /%25%[0-9a-f]{2}%[0-9a-f]{2}/i,\n\t},\n\n\t{\n\t\tid: \"ENCODING-DOUBLE-PERCENT-001\",\n\t\ttype: \"double_encoding\",\n\t\tcontexts: [\"url\", \"url_parameter\", \"html\", \"filesystem\", \"sql\", \"http_header\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Escape prefix that decodes to a metacharacter only after a second pass\",\n\t\tcwe: 177,\n\t\tprimaryControl:\n\t\t\t\"Decode exactly once at a defined boundary and never re-decode; reject input that still contains an escape prefix after decoding\",\n\t\t// Only sequences decoding to a character with syntactic meaning: space, quote,\n\t\t// percent, apostrophe, parens, dot, slash, angle brackets, semicolon, ampersand,\n\t\t// equals, backslash, NUL, CR, LF. A bare `%25` is not enough — \"100%25 off\" is\n\t\t// an ordinary encoded string and must not fire.\n\t\tvariants: [\"raw\"],\n\t\tpattern: /%25(?:2[0257CEFcef]|3[CEce]|22|26|27|28|29|3[BbDd]|5[Cc]|00|0[ADad])/,\n\t},\n];\n\n//#endregion\n\n//#region JWT\n\n/** Repeated across the JWT rules. */\nconst JWT_CONTROL =\n\t\"Verify with an explicit algorithm allowlist (jwtVerify(token, key, { algorithms: ['RS256'] })); never let the token's own header select the algorithm, and reject 'none' unconditionally\";\n\n/**\n * JWT tampering.\n *\n * Scoped to the unsecured-token case alone. `kid`, `jku`, and `x5u` injection are\n * *already covered* — extract the claim and declare its real sink, and the existing\n * rules fire: `kid=../../../../dev/null` with `filesystem` hits PATH-TRAVERSAL-001,\n * `jku=http://169.254.169.254/` with `url` hits SSRF-METADATA-001. Re-scanning a whole\n * opaque token with every detector would be the run-everything-against-everything\n * anti-pattern the engine exists to avoid.\n */\nexport const JWT_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"JWT-ALG-NONE-UNSECURED-001\",\n\t\ttype: \"jwt_tampering\",\n\t\tcontexts: [\"jwt\", \"http_header\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Unsecured JWT — three segments with an empty signature\",\n\t\tcwe: 347,\n\t\tprimaryControl: JWT_CONTROL,\n\t\t// Structural, not lexical. RFC 7519 §6.1 requires an empty signature segment,\n\t\t// so this is immune to base64 alignment, key ordering, whitespace, and `alg`\n\t\t// casing — all of which change how `\"alg\":\"none\"` encodes (`hbGciOiJub25l`,\n\t\t// `YWxnIjoibm9uZ`, `ImFsZyI6Im5vbmUi`, depending on offset mod 3).\n\t\t// Requiring `eyJ` on *both* segments, not just `ey`, removes false positives on\n\t\t// filenames such as `eyewitness_statement_final.v2.`.\n\t\tpattern:\n\t\t\t/(?:^|[\\s,;=(\"'[])eyJ[A-Za-z0-9_-]{16,2000}\\.eyJ[A-Za-z0-9_-]{8,4000}\\.(?:$|[\\s,;)\"'\\]&#])/,\n\t},\n\t{\n\t\tid: \"JWT-ALG-NONE-HEADER-001\",\n\t\ttype: \"jwt_tampering\",\n\t\tcontexts: [\"jwt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Decoded JWT header selecting the 'none' algorithm\",\n\t\tcwe: 347,\n\t\tprimaryControl: JWT_CONTROL,\n\t\t// Covers what the structural rule cannot: `alg:none` carrying a non-empty\n\t\t// signature, which naive verifiers still accept.\n\t\tpattern: /\"alg\"\\s{0,8}:\\s{0,8}\"\\s{0,8}none\\s{0,8}\"/i,\n\t},\n];\n\n//#endregion\n"],"mappings":";;;;;;;;;;AA2CA,MAAa,4BAAmD,CAC/D;CACC,IAAI;CACJ,MAAM;CAKN,UAAU,CAAC,eAAe;CAC1B,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CAGD,SACC;AACF,GACA;CACC,IAAI;CACJ,MAAM;CACN,UAAU,CAAC,eAAe;CAC1B,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CAMD,SAAS;AACV,CACD;;AAOA,MAAM,mBACL;;;;;;;;;AAcD,MAAa,4BAAmD;CAC/D;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,OAAO,KAAK;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAIhB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,KAAK;EAChB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,OAAO,KAAK;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,OAAO,KAAK;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBACC;EAED,SACC;CACF;AACD;;;;;;;;;;;;;;AAmBA,MAAa,wBAA+C,CAC3D;CAOC,IAAI;CACJ,MAAM;CACN,UAAU;EAAC;EAAO;EAAiB;EAAQ;EAAc;EAAO;EAAe;CAAK;CACpF,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CACD,SAAS;AACV,GAEA;CACC,IAAI;CACJ,MAAM;CACN,UAAU;EAAC;EAAO;EAAiB;EAAQ;EAAc;EAAO;EAAe;CAAK;CACpF,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CAKD,UAAU,CAAC,KAAK;CAChB,SAAS;AACV,CACD;;AAOA,MAAM,cACL;;;;;;;;;;;AAYD,MAAa,YAAmC,CAC/C;CACC,IAAI;CACJ,MAAM;CACN,UAAU,CAAC,OAAO,aAAa;CAC/B,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBAAgB;CAOhB,SACC;AACF,GACA;CACC,IAAI;CACJ,MAAM;CACN,UAAU,CAAC,KAAK;CAChB,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBAAgB;CAGhB,SAAS;AACV,CACD"}
@@ -1,11 +1,11 @@
1
1
  import { ThreatRule } from "../types.mjs";
2
2
  //#region src/threats/rules/system.d.ts
3
3
  /** OS command injection. Scoped to the `shell` context — never runs elsewhere. */
4
- declare const COMMAND_INJECTION_RULES: readonly ThreatRule[];
4
+ export declare const COMMAND_INJECTION_RULES: readonly ThreatRule[];
5
5
  /** Directory-traversal and sensitive-path signatures. Scoped to `filesystem`. */
6
- declare const PATH_TRAVERSAL_RULES: readonly ThreatRule[];
6
+ export declare const PATH_TRAVERSAL_RULES: readonly ThreatRule[];
7
7
  /** Local/remote file inclusion via stream wrappers and alternate schemes. */
8
- declare const FILE_INCLUSION_RULES: readonly ThreatRule[];
8
+ export declare const FILE_INCLUSION_RULES: readonly ThreatRule[];
9
9
  /**
10
10
  * Server-side request forgery.
11
11
  *
@@ -13,7 +13,6 @@ declare const FILE_INCLUSION_RULES: readonly ThreatRule[];
13
13
  * resolves to `169.254.169.254` passes every rule here — which is exactly why
14
14
  * {@link SSRF_CONTROL} names resolution and egress control as the real defense.
15
15
  */
16
- declare const SSRF_RULES: readonly ThreatRule[];
16
+ export declare const SSRF_RULES: readonly ThreatRule[];
17
17
  //#endregion
18
- export { COMMAND_INJECTION_RULES, FILE_INCLUSION_RULES, PATH_TRAVERSAL_RULES, SSRF_RULES };
19
18
  //# sourceMappingURL=system.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"system.d.mts","names":[],"sources":["../../../src/threats/rules/system.ts"],"mappings":";;;cA4Ca,kCAAkC;;cA8HlC,+BAA+B;;cA2J/B,+BAA+B;;;;;;;;cAiF/B,qBAAqB"}
1
+ {"version":3,"file":"system.d.mts","names":[],"sources":["../../../src/threats/rules/system.ts"],"mappings":";;;qBA6Ca,kCAAkC;;qBA8HlC,+BAA+B;;qBA2J/B,+BAA+B;;;;;;;;qBAsF/B,qBAAqB"}
@@ -270,7 +270,7 @@ const FILE_INCLUSION_RULES = [
270
270
  description: "Remote scheme where a local path was expected",
271
271
  cwe: 98,
272
272
  primaryControl: RFI_CONTROL,
273
- pattern: /(?:^\s{0,8}|[/\\=])(?:https?|ftps?|smb|cifs|nfs|webdav|ssh2|rar|zlib|compress\.(?:zlib|bzip2)):\/\//i
273
+ pattern: /(?:^\s*|[/\\=])(?:https?|ftps?|smb|cifs|nfs|webdav|ssh2|rar|zlib|compress\.(?:zlib|bzip2)):\/\//i
274
274
  },
275
275
  {
276
276
  id: "RFI-DATA-URI-001",
@@ -281,7 +281,7 @@ const FILE_INCLUSION_RULES = [
281
281
  description: "data: URI where a local path was expected",
282
282
  cwe: 98,
283
283
  primaryControl: RFI_CONTROL,
284
- pattern: /(?:^\s{0,8}|[/\\=])data:(?:\/\/)?(?:[a-z][a-z0-9+.-]{0,32}\/[a-z0-9+.-]{1,32}[a-z0-9;=+.-]{0,32}|;base64|),/i
284
+ pattern: /(?:^\s*|[/\\=])data:(?:\/\/)?(?:[a-z][a-z0-9+.-]{0,32}\/[a-z0-9+.-]{1,32}[a-z0-9;=+.-]{0,32}|;base64|),/i
285
285
  },
286
286
  {
287
287
  id: "RFI-REMOTE-HOST-PATH-001",
@@ -292,7 +292,7 @@ const FILE_INCLUSION_RULES = [
292
292
  description: "Protocol-relative or UNC host path where a local path was expected",
293
293
  cwe: 98,
294
294
  primaryControl: RFI_CONTROL,
295
- pattern: /^\s{0,8}(?:\/\/|\\\\)[a-z0-9][a-z0-9-]{0,62}(?:\.[a-z0-9-]{1,63}){1,4}[/\\]/i
295
+ pattern: /^\s*(?:\/\/|\\\\)[a-z0-9][a-z0-9-]{0,62}(?:\.[a-z0-9-]{1,63}){1,4}[/\\]/i
296
296
  }
297
297
  ];
298
298
  /** Repeated across every SSRF rule. */
@@ -369,7 +369,7 @@ const SSRF_RULES = [
369
369
  description: "Non-HTTP scheme in a fetchable URL",
370
370
  cwe: 918,
371
371
  primaryControl: SSRF_CONTROL,
372
- pattern: /^\s{0,8}(?:file|gopher|dict|tftp|ldaps?|jar|netdoc|sftp):/i
372
+ pattern: /^\s*(?:file|gopher|dict|tftp|ldaps?|jar|netdoc|sftp):/i
373
373
  },
374
374
  {
375
375
  id: "SSRF-USERINFO-INTERNAL-HOST-001",