@resq-systems/security 1.0.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -33
- package/lib/controls/address.d.mts +142 -0
- package/lib/controls/address.d.mts.map +1 -0
- package/lib/controls/address.mjs +533 -0
- package/lib/controls/address.mjs.map +1 -0
- package/lib/controls/csrf.d.mts +91 -0
- package/lib/controls/csrf.d.mts.map +1 -0
- package/lib/controls/csrf.mjs +200 -0
- package/lib/controls/csrf.mjs.map +1 -0
- package/lib/controls/index.d.mts +8 -0
- package/lib/controls/index.mjs +8 -0
- package/lib/controls/origin.d.mts +95 -0
- package/lib/controls/origin.d.mts.map +1 -0
- package/lib/controls/origin.mjs +156 -0
- package/lib/controls/origin.mjs.map +1 -0
- package/lib/controls/payload.d.mts +84 -0
- package/lib/controls/payload.d.mts.map +1 -0
- package/lib/controls/payload.mjs +147 -0
- package/lib/controls/payload.mjs.map +1 -0
- package/lib/controls/query.d.mts +157 -0
- package/lib/controls/query.d.mts.map +1 -0
- package/lib/controls/query.mjs +368 -0
- package/lib/controls/query.mjs.map +1 -0
- package/lib/controls/redirect.d.mts +92 -0
- package/lib/controls/redirect.d.mts.map +1 -0
- package/lib/controls/redirect.mjs +110 -0
- package/lib/controls/redirect.mjs.map +1 -0
- package/lib/controls/upload.d.mts +108 -0
- package/lib/controls/upload.d.mts.map +1 -0
- package/lib/controls/upload.mjs +374 -0
- package/lib/controls/upload.mjs.map +1 -0
- package/lib/crypto.d.mts +18 -5
- package/lib/crypto.d.mts.map +1 -1
- package/lib/crypto.mjs +35 -24
- package/lib/crypto.mjs.map +1 -1
- package/lib/hash.d.mts +51 -6
- package/lib/hash.d.mts.map +1 -1
- package/lib/hash.mjs +51 -6
- package/lib/hash.mjs.map +1 -1
- package/lib/index.d.mts +17 -2
- package/lib/index.mjs +19 -2
- package/lib/paths.d.mts +92 -0
- package/lib/paths.d.mts.map +1 -0
- package/lib/paths.mjs +140 -0
- package/lib/paths.mjs.map +1 -0
- package/lib/sanitize.d.mts +137 -35
- package/lib/sanitize.d.mts.map +1 -1
- package/lib/sanitize.mjs +170 -46
- package/lib/sanitize.mjs.map +1 -1
- package/lib/threats/capec.generated.d.mts +59 -0
- package/lib/threats/capec.generated.d.mts.map +1 -0
- package/lib/threats/capec.generated.mjs +644 -0
- package/lib/threats/capec.generated.mjs.map +1 -0
- package/lib/threats/engine.d.mts +94 -0
- package/lib/threats/engine.d.mts.map +1 -0
- package/lib/threats/engine.mjs +167 -0
- package/lib/threats/engine.mjs.map +1 -0
- package/lib/threats/index.d.mts +11 -0
- package/lib/threats/index.mjs +11 -0
- package/lib/threats/rules/datastore.d.mts +13 -0
- package/lib/threats/rules/datastore.d.mts.map +1 -0
- package/lib/threats/rules/datastore.mjs +366 -0
- package/lib/threats/rules/datastore.mjs.map +1 -0
- package/lib/threats/rules/index.d.mts +54 -0
- package/lib/threats/rules/index.d.mts.map +1 -0
- package/lib/threats/rules/index.mjs +121 -0
- package/lib/threats/rules/index.mjs.map +1 -0
- package/lib/threats/rules/markup.d.mts +28 -0
- package/lib/threats/rules/markup.d.mts.map +1 -0
- package/lib/threats/rules/markup.mjs +373 -0
- package/lib/threats/rules/markup.mjs.map +1 -0
- package/lib/threats/rules/protocol.d.mts +49 -0
- package/lib/threats/rules/protocol.d.mts.map +1 -0
- package/lib/threats/rules/protocol.mjs +175 -0
- package/lib/threats/rules/protocol.mjs.map +1 -0
- package/lib/threats/rules/system.d.mts +19 -0
- package/lib/threats/rules/system.d.mts.map +1 -0
- package/lib/threats/rules/system.mjs +455 -0
- package/lib/threats/rules/system.mjs.map +1 -0
- package/lib/threats/rules/web.d.mts +26 -0
- package/lib/threats/rules/web.d.mts.map +1 -0
- package/lib/threats/rules/web.mjs +412 -0
- package/lib/threats/rules/web.mjs.map +1 -0
- package/lib/threats/scoring.d.mts +59 -0
- package/lib/threats/scoring.d.mts.map +1 -0
- package/lib/threats/scoring.mjs +111 -0
- package/lib/threats/scoring.mjs.map +1 -0
- package/lib/threats/types.d.mts +245 -0
- package/lib/threats/types.d.mts.map +1 -0
- package/lib/threats/types.mjs +52 -0
- package/lib/threats/types.mjs.map +1 -0
- package/lib/threats/variants.d.mts +57 -0
- package/lib/threats/variants.d.mts.map +1 -0
- package/lib/threats/variants.mjs +144 -0
- package/lib/threats/variants.mjs.map +1 -0
- package/lib/unicode/confusables.d.mts +82 -0
- package/lib/unicode/confusables.d.mts.map +1 -0
- package/lib/unicode/confusables.mjs +954 -0
- package/lib/unicode/confusables.mjs.map +1 -0
- package/lib/unicode/index.d.mts +126 -0
- package/lib/unicode/index.d.mts.map +1 -0
- package/lib/unicode/index.mjs +288 -0
- package/lib/unicode/index.mjs.map +1 -0
- package/lib/validators.d.mts +341 -164
- package/lib/validators.d.mts.map +1 -1
- package/lib/validators.mjs +519 -338
- package/lib/validators.mjs.map +1 -1
- package/package.json +35 -8
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
//#region src/threats/rules/datastore.ts
|
|
2
|
+
/** Repeated across every SQL rule. */
|
|
3
|
+
const SQL_CONTROL = "Bind values as query parameters; never concatenate SQL strings";
|
|
4
|
+
/** SQL-injection signatures. Scoped to the `sql` context. */
|
|
5
|
+
const SQL_INJECTION_RULES = [
|
|
6
|
+
{
|
|
7
|
+
id: "SQL-UNION-001",
|
|
8
|
+
type: "sql_injection",
|
|
9
|
+
contexts: ["sql"],
|
|
10
|
+
severity: "high",
|
|
11
|
+
confidence: "high",
|
|
12
|
+
description: "UNION SELECT — classic result-set grafting",
|
|
13
|
+
cwe: 89,
|
|
14
|
+
primaryControl: SQL_CONTROL,
|
|
15
|
+
pattern: /\bUNION\s+(?:ALL\s+)?SELECT\b/i
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "SQL-DROP-001",
|
|
19
|
+
type: "sql_injection",
|
|
20
|
+
contexts: ["sql"],
|
|
21
|
+
severity: "critical",
|
|
22
|
+
confidence: "high",
|
|
23
|
+
description: "DROP of a schema object",
|
|
24
|
+
cwe: 89,
|
|
25
|
+
primaryControl: SQL_CONTROL,
|
|
26
|
+
pattern: /\bDROP\s+(?:TABLE|DATABASE|SCHEMA|INDEX|VIEW|FUNCTION|PROCEDURE)\b/i
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "SQL-DELETE-001",
|
|
30
|
+
type: "sql_injection",
|
|
31
|
+
contexts: ["sql"],
|
|
32
|
+
severity: "high",
|
|
33
|
+
confidence: "medium",
|
|
34
|
+
description: "DELETE FROM in a value position",
|
|
35
|
+
cwe: 89,
|
|
36
|
+
primaryControl: SQL_CONTROL,
|
|
37
|
+
pattern: /\bDELETE\s+FROM\b/i
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "SQL-TRUNCATE-001",
|
|
41
|
+
type: "sql_injection",
|
|
42
|
+
contexts: ["sql"],
|
|
43
|
+
severity: "high",
|
|
44
|
+
confidence: "medium",
|
|
45
|
+
description: "TRUNCATE TABLE in a value position",
|
|
46
|
+
cwe: 89,
|
|
47
|
+
primaryControl: SQL_CONTROL,
|
|
48
|
+
pattern: /\bTRUNCATE\s+TABLE\b/i
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "SQL-STACKED-001",
|
|
52
|
+
type: "sql_injection",
|
|
53
|
+
contexts: ["sql"],
|
|
54
|
+
severity: "high",
|
|
55
|
+
confidence: "high",
|
|
56
|
+
description: "Statement separator followed by a new statement",
|
|
57
|
+
cwe: 89,
|
|
58
|
+
primaryControl: SQL_CONTROL,
|
|
59
|
+
pattern: /;\s{0,8}(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|GRANT|EXEC|UNION)\b/i
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "SQL-TAUTOLOGY-QUOTED-001",
|
|
63
|
+
type: "sql_injection",
|
|
64
|
+
contexts: ["sql"],
|
|
65
|
+
severity: "high",
|
|
66
|
+
confidence: "high",
|
|
67
|
+
description: "Quote-escaped always-true comparison",
|
|
68
|
+
cwe: 89,
|
|
69
|
+
primaryControl: SQL_CONTROL,
|
|
70
|
+
pattern: /['"]\s{0,8}(?:OR|AND)\s+['"]?\w{1,20}['"]?\s{0,8}=\s{0,8}['"]?\w{1,20}/i
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "SQL-TAUTOLOGY-NUMERIC-001",
|
|
74
|
+
type: "sql_injection",
|
|
75
|
+
contexts: ["sql"],
|
|
76
|
+
severity: "high",
|
|
77
|
+
confidence: "high",
|
|
78
|
+
description: "Quote- or paren-escaped numeric always-true comparison",
|
|
79
|
+
cwe: 89,
|
|
80
|
+
primaryControl: SQL_CONTROL,
|
|
81
|
+
pattern: /['")]\s{0,8}(?:OR|AND)\s+\d{1,10}\s{0,8}=\s{0,8}\d{1,10}/i
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "SQL-COMMENT-TERMINATOR-001",
|
|
85
|
+
type: "sql_injection",
|
|
86
|
+
contexts: ["sql"],
|
|
87
|
+
severity: "medium",
|
|
88
|
+
confidence: "low",
|
|
89
|
+
description: "Trailing SQL comment, used to truncate the rest of a statement",
|
|
90
|
+
cwe: 89,
|
|
91
|
+
primaryControl: SQL_CONTROL,
|
|
92
|
+
pattern: /['")\d]\s{0,8}(?:--|#)[^\r\n]{0,64}$/
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "SQL-VERSIONED-COMMENT-001",
|
|
96
|
+
type: "sql_injection",
|
|
97
|
+
contexts: ["sql"],
|
|
98
|
+
severity: "high",
|
|
99
|
+
confidence: "high",
|
|
100
|
+
description: "MySQL versioned comment, whose contents execute on MySQL only",
|
|
101
|
+
cwe: 89,
|
|
102
|
+
primaryControl: SQL_CONTROL,
|
|
103
|
+
pattern: /\/\*!\d{0,5}/
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "SQL-COMMENT-INLINE-001",
|
|
107
|
+
type: "sql_injection",
|
|
108
|
+
contexts: ["sql"],
|
|
109
|
+
severity: "medium",
|
|
110
|
+
confidence: "low",
|
|
111
|
+
description: "Inline block comment, used to split keywords past naive filters",
|
|
112
|
+
cwe: 89,
|
|
113
|
+
primaryControl: SQL_CONTROL,
|
|
114
|
+
pattern: /\/\*[\s\S]{0,200}?\*\//
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "SQL-COMMENT-SPLIT-KEYWORD-001",
|
|
118
|
+
type: "sql_injection",
|
|
119
|
+
contexts: ["sql"],
|
|
120
|
+
severity: "high",
|
|
121
|
+
confidence: "high",
|
|
122
|
+
description: "Inline comment separating two SQL keywords — keyword-filter evasion",
|
|
123
|
+
cwe: 89,
|
|
124
|
+
primaryControl: SQL_CONTROL,
|
|
125
|
+
pattern: /\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
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "SQL-TIME-BLIND-001",
|
|
129
|
+
type: "sql_injection",
|
|
130
|
+
contexts: ["sql"],
|
|
131
|
+
severity: "high",
|
|
132
|
+
confidence: "high",
|
|
133
|
+
description: "Time-delay function used for blind injection",
|
|
134
|
+
cwe: 89,
|
|
135
|
+
primaryControl: SQL_CONTROL,
|
|
136
|
+
pattern: /\b(?:SLEEP|PG_SLEEP|BENCHMARK)\s{0,8}\(|\bWAITFOR\s+DELAY\b/i
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "SQL-METADATA-001",
|
|
140
|
+
type: "sql_injection",
|
|
141
|
+
contexts: ["sql"],
|
|
142
|
+
severity: "high",
|
|
143
|
+
confidence: "medium",
|
|
144
|
+
description: "Access to database metadata catalogs",
|
|
145
|
+
cwe: 89,
|
|
146
|
+
primaryControl: SQL_CONTROL,
|
|
147
|
+
pattern: /\b(?:INFORMATION_SCHEMA|pg_catalog|sysobjects|syscolumns|sqlite_master|all_tables)\b/i
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "SQL-XP-PROC-001",
|
|
151
|
+
type: "sql_injection",
|
|
152
|
+
contexts: ["sql"],
|
|
153
|
+
severity: "critical",
|
|
154
|
+
confidence: "high",
|
|
155
|
+
description: "SQL Server extended procedure enabling OS access",
|
|
156
|
+
cwe: 89,
|
|
157
|
+
primaryControl: SQL_CONTROL,
|
|
158
|
+
pattern: /\b(?:xp_cmdshell|xp_regread|xp_regwrite|xp_dirtree|sp_oacreate)\b/i
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "SQL-FILE-IO-001",
|
|
162
|
+
type: "sql_injection",
|
|
163
|
+
contexts: ["sql"],
|
|
164
|
+
severity: "critical",
|
|
165
|
+
confidence: "high",
|
|
166
|
+
description: "SQL file read/write primitive",
|
|
167
|
+
cwe: 89,
|
|
168
|
+
primaryControl: SQL_CONTROL,
|
|
169
|
+
pattern: /\bINTO\s+(?:OUT|DUMP)FILE\b|\bLOAD_FILE\s{0,8}\(|\bCOPY\s+\w{1,64}\s+FROM\s+PROGRAM\b/i
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "SQL-ENGINE-FILE-IO-001",
|
|
173
|
+
type: "sql_injection",
|
|
174
|
+
contexts: ["sql"],
|
|
175
|
+
severity: "critical",
|
|
176
|
+
confidence: "high",
|
|
177
|
+
description: "Engine-specific file read/write primitive",
|
|
178
|
+
cwe: 89,
|
|
179
|
+
primaryControl: SQL_CONTROL,
|
|
180
|
+
pattern: /\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
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "SQL-OUT-OF-BAND-001",
|
|
184
|
+
type: "sql_injection",
|
|
185
|
+
contexts: ["sql"],
|
|
186
|
+
severity: "critical",
|
|
187
|
+
confidence: "high",
|
|
188
|
+
description: "Out-of-band exfiltration or network primitive",
|
|
189
|
+
cwe: 89,
|
|
190
|
+
primaryControl: SQL_CONTROL,
|
|
191
|
+
pattern: /\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
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "SQL-HEX-LITERAL-001",
|
|
195
|
+
type: "sql_injection",
|
|
196
|
+
contexts: ["sql"],
|
|
197
|
+
severity: "low",
|
|
198
|
+
confidence: "low",
|
|
199
|
+
description: "Long hex literal, sometimes used to smuggle string constants",
|
|
200
|
+
cwe: 89,
|
|
201
|
+
primaryControl: SQL_CONTROL,
|
|
202
|
+
pattern: /\b0x[0-9a-f]{8,}\b/i
|
|
203
|
+
}
|
|
204
|
+
];
|
|
205
|
+
/** Repeated across every NoSQL rule. */
|
|
206
|
+
const NOSQL_CONTROL = "Cast query values to primitives and reject object-valued inputs; never pass raw request bodies to a query builder";
|
|
207
|
+
/** Document-store operator injection (MongoDB and compatible drivers). */
|
|
208
|
+
const NOSQL_INJECTION_RULES = [
|
|
209
|
+
{
|
|
210
|
+
id: "NOSQL-OPERATOR-OBJECT-001",
|
|
211
|
+
type: "nosql_injection",
|
|
212
|
+
contexts: ["nosql"],
|
|
213
|
+
severity: "high",
|
|
214
|
+
confidence: "high",
|
|
215
|
+
description: "Object literal whose first key is a query operator",
|
|
216
|
+
cwe: 943,
|
|
217
|
+
primaryControl: NOSQL_CONTROL,
|
|
218
|
+
pattern: /\{\s{0,8}["']?\$[a-z]{2,20}["']?\s{0,8}:/i
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "NOSQL-JS-EXECUTION-001",
|
|
222
|
+
type: "nosql_injection",
|
|
223
|
+
contexts: ["nosql"],
|
|
224
|
+
severity: "critical",
|
|
225
|
+
confidence: "high",
|
|
226
|
+
description: "Server-side JavaScript execution operator",
|
|
227
|
+
cwe: 943,
|
|
228
|
+
primaryControl: NOSQL_CONTROL,
|
|
229
|
+
pattern: /\$(?:where|function|accumulator)\s{0,8}["']?\s{0,8}:/i
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: "NOSQL-OPERATOR-001",
|
|
233
|
+
type: "nosql_injection",
|
|
234
|
+
contexts: ["nosql"],
|
|
235
|
+
severity: "medium",
|
|
236
|
+
confidence: "medium",
|
|
237
|
+
description: "Query operator token",
|
|
238
|
+
cwe: 943,
|
|
239
|
+
primaryControl: NOSQL_CONTROL,
|
|
240
|
+
pattern: /\$(?:gte?|lte?|ne|eq|nin?|and|or|not|nor|exists|type|mod|regex|text|all|elemMatch|size|slice|expr|jsonSchema)\b/i
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: "NOSQL-OPERATOR-ARRAY-001",
|
|
244
|
+
type: "nosql_injection",
|
|
245
|
+
contexts: ["nosql"],
|
|
246
|
+
severity: "medium",
|
|
247
|
+
confidence: "medium",
|
|
248
|
+
description: "Array subscript naming a query operator",
|
|
249
|
+
cwe: 943,
|
|
250
|
+
primaryControl: NOSQL_CONTROL,
|
|
251
|
+
pattern: /\[\s{0,8}["']?\$[a-z]{2,20}["']?\s{0,8}\]/i
|
|
252
|
+
}
|
|
253
|
+
];
|
|
254
|
+
/** Repeated across every LDAP rule. */
|
|
255
|
+
const LDAP_CONTROL = "Escape filter values per RFC 4515 and DN components per RFC 4514 using the LDAP client's escaping helper";
|
|
256
|
+
/** LDAP filter and DN injection. */
|
|
257
|
+
const LDAP_INJECTION_RULES = [
|
|
258
|
+
{
|
|
259
|
+
id: "LDAP-FILTER-WILDCARD-001",
|
|
260
|
+
type: "ldap_injection",
|
|
261
|
+
contexts: ["ldap"],
|
|
262
|
+
severity: "high",
|
|
263
|
+
confidence: "high",
|
|
264
|
+
description: "Injected filter clause matching any value",
|
|
265
|
+
cwe: 90,
|
|
266
|
+
primaryControl: LDAP_CONTROL,
|
|
267
|
+
pattern: /[()&|!]\s{0,8}\(\s{0,8}[a-z0-9;.-]{1,64}\s{0,8}=\s{0,8}\*/i
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: "LDAP-FILTER-CLOSE-001",
|
|
271
|
+
type: "ldap_injection",
|
|
272
|
+
contexts: ["ldap"],
|
|
273
|
+
severity: "high",
|
|
274
|
+
confidence: "medium",
|
|
275
|
+
description: "Filter-terminating parenthesis followed by a boolean operator",
|
|
276
|
+
cwe: 90,
|
|
277
|
+
primaryControl: LDAP_CONTROL,
|
|
278
|
+
pattern: /\)\s{0,8}[&|]\s{0,8}\(/
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "LDAP-METACHAR-001",
|
|
282
|
+
type: "ldap_injection",
|
|
283
|
+
contexts: ["ldap"],
|
|
284
|
+
severity: "medium",
|
|
285
|
+
confidence: "low",
|
|
286
|
+
description: "Unescaped LDAP filter metacharacter",
|
|
287
|
+
cwe: 90,
|
|
288
|
+
primaryControl: LDAP_CONTROL,
|
|
289
|
+
pattern: /[*()\\]/
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: "LDAP-DN-INJECTION-001",
|
|
293
|
+
type: "ldap_injection",
|
|
294
|
+
contexts: ["ldap"],
|
|
295
|
+
severity: "high",
|
|
296
|
+
confidence: "medium",
|
|
297
|
+
description: "RDN separator in a value destined for a distinguished name",
|
|
298
|
+
cwe: 90,
|
|
299
|
+
primaryControl: "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",
|
|
300
|
+
pattern: /[,+]\s{0,8}(?:cn|ou|dc|o|uid|sn|givenname|mail|member|objectclass)\s{0,8}=/i
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: "LDAP-DN-METACHAR-001",
|
|
304
|
+
type: "ldap_injection",
|
|
305
|
+
contexts: ["ldap"],
|
|
306
|
+
severity: "medium",
|
|
307
|
+
confidence: "low",
|
|
308
|
+
description: "Unescaped RFC 4514 distinguished-name metacharacter",
|
|
309
|
+
cwe: 90,
|
|
310
|
+
primaryControl: "Escape DN components per RFC 4514 using the LDAP client's DN escaping helper",
|
|
311
|
+
pattern: /["<>;]|^[+,]|[+,]$/
|
|
312
|
+
}
|
|
313
|
+
];
|
|
314
|
+
/** Repeated across every XPath rule. */
|
|
315
|
+
const XPATH_CONTROL = "Use a precompiled XPath with variable binding (XPathVariableResolver or equivalent); never concatenate expressions";
|
|
316
|
+
/** XPath/XQuery injection. */
|
|
317
|
+
const XPATH_INJECTION_RULES = [
|
|
318
|
+
{
|
|
319
|
+
id: "XPATH-TAUTOLOGY-001",
|
|
320
|
+
type: "xpath_injection",
|
|
321
|
+
contexts: ["xpath"],
|
|
322
|
+
severity: "high",
|
|
323
|
+
confidence: "high",
|
|
324
|
+
description: "Quote-escaped always-true predicate",
|
|
325
|
+
cwe: 643,
|
|
326
|
+
primaryControl: XPATH_CONTROL,
|
|
327
|
+
pattern: /['"]\s{0,8}(?:or|and)\s+['"]?\w{1,20}['"]?\s{0,8}=\s{0,8}['"]?\w{1,20}/i
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: "XPATH-NODE-ESCAPE-001",
|
|
331
|
+
type: "xpath_injection",
|
|
332
|
+
contexts: ["xpath"],
|
|
333
|
+
severity: "high",
|
|
334
|
+
confidence: "high",
|
|
335
|
+
description: "Predicate closed then unioned with a new node path",
|
|
336
|
+
cwe: 643,
|
|
337
|
+
primaryControl: XPATH_CONTROL,
|
|
338
|
+
pattern: /\]\s{0,8}\|\s{0,8}\/{1,2}/
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: "XPATH-AXIS-001",
|
|
342
|
+
type: "xpath_injection",
|
|
343
|
+
contexts: ["xpath"],
|
|
344
|
+
severity: "medium",
|
|
345
|
+
confidence: "medium",
|
|
346
|
+
description: "Explicit XPath axis, used to walk outside the intended subtree",
|
|
347
|
+
cwe: 643,
|
|
348
|
+
primaryControl: XPATH_CONTROL,
|
|
349
|
+
pattern: /\b(?:ancestor|descendant|following|preceding|parent|child|self)(?:-or-self)?\s{0,8}::/i
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: "XPATH-FUNCTION-001",
|
|
353
|
+
type: "xpath_injection",
|
|
354
|
+
contexts: ["xpath"],
|
|
355
|
+
severity: "low",
|
|
356
|
+
confidence: "low",
|
|
357
|
+
description: "XPath function call in a value position",
|
|
358
|
+
cwe: 643,
|
|
359
|
+
primaryControl: XPATH_CONTROL,
|
|
360
|
+
pattern: /\b(?:count|name|local-name|namespace-uri|string-length|substring|position)\s{0,8}\(/i
|
|
361
|
+
}
|
|
362
|
+
];
|
|
363
|
+
//#endregion
|
|
364
|
+
export { LDAP_INJECTION_RULES, NOSQL_INJECTION_RULES, SQL_INJECTION_RULES, XPATH_INJECTION_RULES };
|
|
365
|
+
|
|
366
|
+
//# sourceMappingURL=datastore.mjs.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ThreatContext, ThreatRule } from "../types.mjs";
|
|
2
|
+
import { LDAP_INJECTION_RULES, NOSQL_INJECTION_RULES, SQL_INJECTION_RULES, XPATH_INJECTION_RULES } from "./datastore.mjs";
|
|
3
|
+
import { PROMPT_INJECTION_RULES, TEMPLATE_INJECTION_RULES, UNIVERSAL_RULES, XML_INJECTION_RULES } from "./markup.mjs";
|
|
4
|
+
import { CREDENTIAL_EXPOSURE_RULES, DOUBLE_ENCODING_RULES, JWT_RULES, PARAMETER_POLLUTION_RULES } from "./protocol.mjs";
|
|
5
|
+
import { COMMAND_INJECTION_RULES, FILE_INCLUSION_RULES, PATH_TRAVERSAL_RULES, SSRF_RULES } from "./system.mjs";
|
|
6
|
+
import { FORMULA_INJECTION_RULES, HEADER_INJECTION_RULES, LOG_INJECTION_RULES, PROTOTYPE_POLLUTION_RULES, XSS_RULES } from "./web.mjs";
|
|
7
|
+
//#region src/threats/rules/index.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Every signature the engine knows about, in a stable order.
|
|
10
|
+
*
|
|
11
|
+
* Order affects only the sequence of findings in a result — scoring is
|
|
12
|
+
* order-independent.
|
|
13
|
+
*/
|
|
14
|
+
declare const THREAT_RULES: readonly ThreatRule[];
|
|
15
|
+
/**
|
|
16
|
+
* Assert the catalog's structural invariants.
|
|
17
|
+
*
|
|
18
|
+
* Runs once at module load and throws on violation — see the file overview for why
|
|
19
|
+
* these are fatal rather than warnings.
|
|
20
|
+
*
|
|
21
|
+
* @param rules - Catalog to validate. Defaults to {@link THREAT_RULES}.
|
|
22
|
+
* @throws {Error} If any rule has a duplicate ID, an empty context list, or a
|
|
23
|
+
* stateful (`g`/`y`) pattern. The message lists every problem found, not just the
|
|
24
|
+
* first.
|
|
25
|
+
*/
|
|
26
|
+
declare function assertRuleCatalogIsValid(rules?: readonly ThreatRule[]): void;
|
|
27
|
+
/**
|
|
28
|
+
* Rule IDs that once shipped and have since been withdrawn.
|
|
29
|
+
*
|
|
30
|
+
* A rule ID is public API: consumers pin them in `excludeRuleIds` to suppress a finding
|
|
31
|
+
* they have accepted. Reusing a retired ID silently re-points that suppression at an
|
|
32
|
+
* unrelated rule, which is worse than the original finding because nobody is looking.
|
|
33
|
+
* Add the ID here when a rule is withdrawn; never remove an entry.
|
|
34
|
+
*
|
|
35
|
+
* Enforced from the test suite rather than from {@link assertRuleCatalogIsValid}: that
|
|
36
|
+
* runs at module load in every consumer process, and its checks are fatal because they
|
|
37
|
+
* are runtime-correctness failures. This one is release hygiene.
|
|
38
|
+
*/
|
|
39
|
+
declare const RETIRED_RULE_IDS: readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* Collect the rules applicable to a set of sinks.
|
|
42
|
+
*
|
|
43
|
+
* A rule declaring several contexts is returned once even when the caller names more
|
|
44
|
+
* than one of them, so a value bound for both `html` and `url` is not double-scored
|
|
45
|
+
* by a scheme rule that covers both.
|
|
46
|
+
*
|
|
47
|
+
* @param contexts - Sinks the value is destined for.
|
|
48
|
+
* @returns Deduplicated rules in {@link THREAT_RULES} order — stable regardless of
|
|
49
|
+
* the order the caller listed contexts in.
|
|
50
|
+
*/
|
|
51
|
+
declare function getRulesForContexts(contexts: readonly ThreatContext[]): readonly ThreatRule[];
|
|
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 };
|
|
54
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { LDAP_INJECTION_RULES, NOSQL_INJECTION_RULES, SQL_INJECTION_RULES, XPATH_INJECTION_RULES } from "./datastore.mjs";
|
|
2
|
+
import { PROMPT_INJECTION_RULES, TEMPLATE_INJECTION_RULES, UNIVERSAL_RULES, XML_INJECTION_RULES } from "./markup.mjs";
|
|
3
|
+
import { CREDENTIAL_EXPOSURE_RULES, DOUBLE_ENCODING_RULES, JWT_RULES, PARAMETER_POLLUTION_RULES } from "./protocol.mjs";
|
|
4
|
+
import { COMMAND_INJECTION_RULES, FILE_INCLUSION_RULES, PATH_TRAVERSAL_RULES, SSRF_RULES } from "./system.mjs";
|
|
5
|
+
import { FORMULA_INJECTION_RULES, HEADER_INJECTION_RULES, LOG_INJECTION_RULES, PROTOTYPE_POLLUTION_RULES, XSS_RULES } from "./web.mjs";
|
|
6
|
+
//#region src/threats/rules/index.ts
|
|
7
|
+
/**
|
|
8
|
+
* Every signature the engine knows about, in a stable order.
|
|
9
|
+
*
|
|
10
|
+
* Order affects only the sequence of findings in a result — scoring is
|
|
11
|
+
* order-independent.
|
|
12
|
+
*/
|
|
13
|
+
const THREAT_RULES = [
|
|
14
|
+
...XSS_RULES,
|
|
15
|
+
...PROTOTYPE_POLLUTION_RULES,
|
|
16
|
+
...HEADER_INJECTION_RULES,
|
|
17
|
+
...FORMULA_INJECTION_RULES,
|
|
18
|
+
...LOG_INJECTION_RULES,
|
|
19
|
+
...SQL_INJECTION_RULES,
|
|
20
|
+
...NOSQL_INJECTION_RULES,
|
|
21
|
+
...LDAP_INJECTION_RULES,
|
|
22
|
+
...XPATH_INJECTION_RULES,
|
|
23
|
+
...COMMAND_INJECTION_RULES,
|
|
24
|
+
...PATH_TRAVERSAL_RULES,
|
|
25
|
+
...FILE_INCLUSION_RULES,
|
|
26
|
+
...SSRF_RULES,
|
|
27
|
+
...XML_INJECTION_RULES,
|
|
28
|
+
...TEMPLATE_INJECTION_RULES,
|
|
29
|
+
...PROMPT_INJECTION_RULES,
|
|
30
|
+
...PARAMETER_POLLUTION_RULES,
|
|
31
|
+
...CREDENTIAL_EXPOSURE_RULES,
|
|
32
|
+
...JWT_RULES,
|
|
33
|
+
...DOUBLE_ENCODING_RULES,
|
|
34
|
+
...UNIVERSAL_RULES
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Assert the catalog's structural invariants.
|
|
38
|
+
*
|
|
39
|
+
* Runs once at module load and throws on violation — see the file overview for why
|
|
40
|
+
* these are fatal rather than warnings.
|
|
41
|
+
*
|
|
42
|
+
* @param rules - Catalog to validate. Defaults to {@link THREAT_RULES}.
|
|
43
|
+
* @throws {Error} If any rule has a duplicate ID, an empty context list, or a
|
|
44
|
+
* stateful (`g`/`y`) pattern. The message lists every problem found, not just the
|
|
45
|
+
* first.
|
|
46
|
+
*/
|
|
47
|
+
function assertRuleCatalogIsValid(rules = THREAT_RULES) {
|
|
48
|
+
const seen = /* @__PURE__ */ new Set();
|
|
49
|
+
const problems = [];
|
|
50
|
+
for (const rule of rules) {
|
|
51
|
+
if (seen.has(rule.id)) problems.push(`duplicate rule id: ${rule.id}`);
|
|
52
|
+
seen.add(rule.id);
|
|
53
|
+
if (rule.contexts.length === 0) problems.push(`${rule.id}: contexts must not be empty`);
|
|
54
|
+
if (rule.pattern.global || rule.pattern.sticky) problems.push(`${rule.id}: pattern must not use the g or y flag (lastIndex makes matching stateful)`);
|
|
55
|
+
}
|
|
56
|
+
if (problems.length > 0) throw new Error(`Invalid threat rule catalog:\n ${problems.join("\n ")}`);
|
|
57
|
+
}
|
|
58
|
+
assertRuleCatalogIsValid();
|
|
59
|
+
/**
|
|
60
|
+
* Index from context to the rules that declare it, built once at module load so
|
|
61
|
+
* per-call scanning is a map lookup rather than a filter over the whole catalog.
|
|
62
|
+
*/
|
|
63
|
+
const RULES_BY_CONTEXT = (() => {
|
|
64
|
+
const index = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const rule of THREAT_RULES) for (const context of rule.contexts) {
|
|
66
|
+
const bucket = index.get(context);
|
|
67
|
+
if (bucket) bucket.push(rule);
|
|
68
|
+
else index.set(context, [rule]);
|
|
69
|
+
}
|
|
70
|
+
return index;
|
|
71
|
+
})();
|
|
72
|
+
/**
|
|
73
|
+
* Memoized results of {@link getRulesForContexts} for multi-context calls.
|
|
74
|
+
*
|
|
75
|
+
* Keyed by the sorted, deduplicated, known-only context list, so it holds at most one
|
|
76
|
+
* entry per distinct subset of the context enum — a hard ceiling that does not depend on
|
|
77
|
+
* caller behaviour. The single-context path never reaches here; it already returns a
|
|
78
|
+
* prebuilt bucket from {@link RULES_BY_CONTEXT}.
|
|
79
|
+
*/
|
|
80
|
+
const COMBINATION_CACHE = /* @__PURE__ */ new Map();
|
|
81
|
+
/**
|
|
82
|
+
* Rule IDs that once shipped and have since been withdrawn.
|
|
83
|
+
*
|
|
84
|
+
* A rule ID is public API: consumers pin them in `excludeRuleIds` to suppress a finding
|
|
85
|
+
* they have accepted. Reusing a retired ID silently re-points that suppression at an
|
|
86
|
+
* unrelated rule, which is worse than the original finding because nobody is looking.
|
|
87
|
+
* Add the ID here when a rule is withdrawn; never remove an entry.
|
|
88
|
+
*
|
|
89
|
+
* Enforced from the test suite rather than from {@link assertRuleCatalogIsValid}: that
|
|
90
|
+
* runs at module load in every consumer process, and its checks are fatal because they
|
|
91
|
+
* are runtime-correctness failures. This one is release hygiene.
|
|
92
|
+
*/
|
|
93
|
+
const RETIRED_RULE_IDS = [];
|
|
94
|
+
/**
|
|
95
|
+
* Collect the rules applicable to a set of sinks.
|
|
96
|
+
*
|
|
97
|
+
* A rule declaring several contexts is returned once even when the caller names more
|
|
98
|
+
* than one of them, so a value bound for both `html` and `url` is not double-scored
|
|
99
|
+
* by a scheme rule that covers both.
|
|
100
|
+
*
|
|
101
|
+
* @param contexts - Sinks the value is destined for.
|
|
102
|
+
* @returns Deduplicated rules in {@link THREAT_RULES} order — stable regardless of
|
|
103
|
+
* the order the caller listed contexts in.
|
|
104
|
+
*/
|
|
105
|
+
function getRulesForContexts(contexts) {
|
|
106
|
+
const first = contexts[0];
|
|
107
|
+
if (contexts.length === 1 && first !== void 0) return RULES_BY_CONTEXT.get(first) ?? [];
|
|
108
|
+
const known = [...new Set(contexts)].filter((context) => RULES_BY_CONTEXT.has(context)).sort();
|
|
109
|
+
const key = known.join(" ");
|
|
110
|
+
const cached = COMBINATION_CACHE.get(key);
|
|
111
|
+
if (cached !== void 0) return cached;
|
|
112
|
+
const selected = /* @__PURE__ */ new Set();
|
|
113
|
+
for (const context of known) for (const rule of RULES_BY_CONTEXT.get(context) ?? []) selected.add(rule);
|
|
114
|
+
const computed = THREAT_RULES.filter((rule) => selected.has(rule));
|
|
115
|
+
COMBINATION_CACHE.set(key, computed);
|
|
116
|
+
return computed;
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
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 };
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ThreatRule } from "../types.mjs";
|
|
2
|
+
//#region src/threats/rules/markup.d.ts
|
|
3
|
+
/** XML external entity and entity-expansion signatures. */
|
|
4
|
+
declare const XML_INJECTION_RULES: readonly ThreatRule[];
|
|
5
|
+
/** Server-side template injection and SSI directives. */
|
|
6
|
+
declare const TEMPLATE_INJECTION_RULES: readonly ThreatRule[];
|
|
7
|
+
/**
|
|
8
|
+
* LLM prompt injection.
|
|
9
|
+
*
|
|
10
|
+
* These are the weakest signatures in the catalog by construction — prompt injection
|
|
11
|
+
* is expressed in natural language, so no finite pattern set bounds it. They are
|
|
12
|
+
* graded accordingly (mostly `medium` confidence) and are useful as telemetry and as
|
|
13
|
+
* a tripwire for human review, never as an authorization decision.
|
|
14
|
+
*/
|
|
15
|
+
declare const PROMPT_INJECTION_RULES: readonly ThreatRule[];
|
|
16
|
+
/**
|
|
17
|
+
* Rules that apply in every context, including `general_text`.
|
|
18
|
+
*
|
|
19
|
+
* The bar for membership is high: a rule belongs here only if there is no legitimate
|
|
20
|
+
* reason for the character to appear in *any* user-supplied value. Bidirectional
|
|
21
|
+
* overrides (the "Trojan Source" class, CVE-2021-42574), zero-width characters used
|
|
22
|
+
* to spoof identity, and C0 control characters all clear it. SQL keywords and `../`
|
|
23
|
+
* emphatically do not, which is why they are context-scoped instead.
|
|
24
|
+
*/
|
|
25
|
+
declare const UNIVERSAL_RULES: readonly ThreatRule[];
|
|
26
|
+
//#endregion
|
|
27
|
+
export { PROMPT_INJECTION_RULES, TEMPLATE_INJECTION_RULES, UNIVERSAL_RULES, XML_INJECTION_RULES };
|
|
28
|
+
//# sourceMappingURL=markup.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markup.d.mts","names":[],"sources":["../../../src/threats/rules/markup.ts"],"mappings":";;;cAuCa,8BAA8B;;cAmE9B,mCAAmC;;;;;;;;;cA0KnC,iCAAiC;;;;;;;;;;cAgHjC,0BAA0B"}
|