@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,94 @@
|
|
|
1
|
+
import { EventContext, ThreatContext, ThreatFinding, ThreatPolicy, ThreatSeverity, ThreatType, ThreatVerdict } from "./types.mjs";
|
|
2
|
+
//#region src/threats/engine.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Characters scanned before truncation.
|
|
5
|
+
*
|
|
6
|
+
* Bounds worst-case regex cost per call. Individual rules are bounded too — no
|
|
7
|
+
* unbounded nested quantifiers — so this is a second line of defense rather than the
|
|
8
|
+
* only one, but a 50 MB paste should not become a 50 MB × 89-rule scan regardless.
|
|
9
|
+
*/
|
|
10
|
+
declare const MAX_SCAN_LENGTH = 100000;
|
|
11
|
+
/** Controls what {@link scanForThreats} evaluates and how it grades the outcome. */
|
|
12
|
+
interface ThreatScanOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Correlation metadata, echoed onto the result untouched.
|
|
15
|
+
*
|
|
16
|
+
* Affects nothing about detection. See {@link EventContext} for why it exists.
|
|
17
|
+
*/
|
|
18
|
+
readonly event?: EventContext;
|
|
19
|
+
/**
|
|
20
|
+
* Sinks the value is destined for.
|
|
21
|
+
*
|
|
22
|
+
* Defaults to `["general_text"]`, which enables only the universal rules — bidi
|
|
23
|
+
* overrides, invisible characters, control characters. Any caller that wants
|
|
24
|
+
* injection detection must name the sink it is protecting; that requirement is
|
|
25
|
+
* the package's primary false-positive control, not an inconvenience.
|
|
26
|
+
*/
|
|
27
|
+
readonly contexts?: readonly ThreatContext[];
|
|
28
|
+
/** Truncation bound. Defaults to {@link MAX_SCAN_LENGTH}. */
|
|
29
|
+
readonly maxLength?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Scan encoded representations in addition to the raw string. Defaults to `true`.
|
|
32
|
+
* Pass `false` when the caller has already decoded the value and re-decoding
|
|
33
|
+
* would misrepresent it.
|
|
34
|
+
*/
|
|
35
|
+
readonly scanVariants?: boolean;
|
|
36
|
+
/** Drop findings below this severity before scoring. Defaults to `"low"`. */
|
|
37
|
+
readonly minSeverity?: ThreatSeverity;
|
|
38
|
+
/**
|
|
39
|
+
* Rule IDs to skip. The tuning knob to reach for first: silencing one noisy rule
|
|
40
|
+
* on one route beats disabling a whole category.
|
|
41
|
+
*/
|
|
42
|
+
readonly excludeRuleIds?: readonly string[];
|
|
43
|
+
/** Score thresholds. Defaults to {@link DEFAULT_THREAT_POLICY}. */
|
|
44
|
+
readonly policy?: ThreatPolicy;
|
|
45
|
+
}
|
|
46
|
+
/** Outcome of a scan. */
|
|
47
|
+
interface ThreatScanResult {
|
|
48
|
+
/**
|
|
49
|
+
* The {@link EventContext} the caller supplied, echoed verbatim.
|
|
50
|
+
*
|
|
51
|
+
* Present only when one was passed. The scanner neither resolves nor validates it.
|
|
52
|
+
*/
|
|
53
|
+
readonly event?: EventContext;
|
|
54
|
+
/** `true` when nothing fired at all — strictly stronger than `verdict === "allow"`. */
|
|
55
|
+
readonly isSafe: boolean;
|
|
56
|
+
/** Anomaly score. See {@link calculateThreatScore}. */
|
|
57
|
+
readonly score: number;
|
|
58
|
+
/** Policy band derived from {@link ThreatScanResult.score}. */
|
|
59
|
+
readonly verdict: ThreatVerdict;
|
|
60
|
+
/** Every finding, in catalog order then variant order. */
|
|
61
|
+
readonly findings: readonly ThreatFinding[];
|
|
62
|
+
/** Distinct weakness categories present, in first-seen order. */
|
|
63
|
+
readonly types: readonly ThreatType[];
|
|
64
|
+
/** `true` when the input exceeded `maxLength` and the tail went unscanned. */
|
|
65
|
+
readonly truncated: boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Scan a value against the rules for its declared sinks.
|
|
69
|
+
*
|
|
70
|
+
* Every applicable rule is evaluated against every applicable representation, so the
|
|
71
|
+
* finding list may contain one rule more than once with different `variant` values.
|
|
72
|
+
* Scoring collapses those — see {@link calculateThreatScore} — so an encoded payload
|
|
73
|
+
* is not penalized twice merely for being encoded.
|
|
74
|
+
*
|
|
75
|
+
* Non-string input (`null`, `undefined`, a number) is reported safe rather than
|
|
76
|
+
* throwing: this is a detector, and rejecting the wrong *type* belongs to the
|
|
77
|
+
* caller's schema layer.
|
|
78
|
+
*
|
|
79
|
+
* @param input - The candidate value.
|
|
80
|
+
* @param options - Contexts and tuning. See {@link ThreatScanOptions}.
|
|
81
|
+
* @returns A {@link ThreatScanResult}. Never throws.
|
|
82
|
+
*
|
|
83
|
+
* @example Scoping to the actual sink
|
|
84
|
+
* ```ts
|
|
85
|
+
* const bio = "I maintain build scripts under C:\\Windows\\System32";
|
|
86
|
+
*
|
|
87
|
+
* scanForThreats(bio, { contexts: ["general_text"] }).verdict; // "allow"
|
|
88
|
+
* scanForThreats(bio, { contexts: ["filesystem"] }).verdict; // "review"
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function scanForThreats(input: string, options?: ThreatScanOptions): ThreatScanResult;
|
|
92
|
+
//#endregion
|
|
93
|
+
export { MAX_SCAN_LENGTH, ThreatScanOptions, ThreatScanResult, scanForThreats };
|
|
94
|
+
//# sourceMappingURL=engine.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.mts","names":[],"sources":["../../src/threats/engine.ts"],"mappings":";;;;;;;;;cAwDa;;UAgBI;;;;;;WAMP,QAAQ;;;;;;;;;WASR,oBAAoB;;WAEpB;;;;;;WAMA;;WAEA,cAAc;;;;;WAKd;;WAEA,SAAS;;;UAIF;;;;;;WAMP,QAAQ;;WAER;;WAEA;;WAEA,SAAS;;WAET,mBAAmB;;WAEnB,gBAAgB;;WAEhB;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkFM,eAAe,eAAe,UAAS,oBAAyB"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { DEFAULT_THREAT_POLICY, SEVERITY_ORDER } from "./types.mjs";
|
|
2
|
+
import { getRulesForContexts } from "./rules/index.mjs";
|
|
3
|
+
import { calculateThreatScore, verdictForScore } from "./scoring.mjs";
|
|
4
|
+
import { buildInputVariants } from "./variants.mjs";
|
|
5
|
+
//#region src/threats/engine.ts
|
|
6
|
+
/**
|
|
7
|
+
* Copyright 2026 ResQ Systems, Inc.
|
|
8
|
+
*
|
|
9
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
+
* you may not use this file except in compliance with the License.
|
|
11
|
+
* You may obtain a copy of the License at
|
|
12
|
+
*
|
|
13
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
+
*
|
|
15
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
16
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
+
* See the License for the specific language governing permissions and
|
|
19
|
+
* limitations under the License.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* @fileoverview The scan engine: evaluate the rules applicable to a declared set of
|
|
23
|
+
* sinks against a bounded set of input representations, then score the result.
|
|
24
|
+
*
|
|
25
|
+
* The engine's defining constraint is that **the caller must say where the value is
|
|
26
|
+
* going**. Running every detector against every field is what makes signature-based
|
|
27
|
+
* validation unusable in practice — a biography containing `C:\Windows`, a support
|
|
28
|
+
* ticket containing `1=1`, and a programming question containing `eval(` are all
|
|
29
|
+
* ordinary text, and only become evidence when the value actually reaches a
|
|
30
|
+
* filesystem, SQL, or HTML sink. Declaring the context is the difference between a
|
|
31
|
+
* detector that catches attacks and one that rejects customers.
|
|
32
|
+
*
|
|
33
|
+
* @module @resq-systems/security/threats/engine
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Characters scanned before truncation.
|
|
37
|
+
*
|
|
38
|
+
* Bounds worst-case regex cost per call. Individual rules are bounded too — no
|
|
39
|
+
* unbounded nested quantifiers — so this is a second line of defense rather than the
|
|
40
|
+
* only one, but a 50 MB paste should not become a 50 MB × 89-rule scan regardless.
|
|
41
|
+
*/
|
|
42
|
+
const MAX_SCAN_LENGTH = 1e5;
|
|
43
|
+
/** Matched substrings are truncated to this many characters before entering a finding. */
|
|
44
|
+
const MAX_MATCH_EXCERPT = 50;
|
|
45
|
+
/**
|
|
46
|
+
* Run length at which a repeated single character is reported as resource abuse.
|
|
47
|
+
* Set well above anything that occurs in prose or formatted text.
|
|
48
|
+
*/
|
|
49
|
+
const REPETITION_THRESHOLD = 200;
|
|
50
|
+
/** Result returned for empty or non-string input. */
|
|
51
|
+
const SAFE_RESULT = {
|
|
52
|
+
isSafe: true,
|
|
53
|
+
score: 0,
|
|
54
|
+
verdict: "allow",
|
|
55
|
+
findings: [],
|
|
56
|
+
types: [],
|
|
57
|
+
truncated: false
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Structural check that is cheaper and more informative computed than matched.
|
|
61
|
+
*
|
|
62
|
+
* A backreference like `/(.)\1{200,}/` expresses "long repeated run" but costs far
|
|
63
|
+
* more than the single linear pass below, and the pass can report the actual run
|
|
64
|
+
* length in the finding.
|
|
65
|
+
*
|
|
66
|
+
* @param value - Bounded input.
|
|
67
|
+
* @returns At most one finding, for the first over-threshold run.
|
|
68
|
+
*/
|
|
69
|
+
function detectResourceAbuse(value) {
|
|
70
|
+
let runStart = 0;
|
|
71
|
+
for (let i = 1; i <= value.length; i++) {
|
|
72
|
+
if (i < value.length && value[i] === value[runStart]) continue;
|
|
73
|
+
const runLength = i - runStart;
|
|
74
|
+
if (runLength >= REPETITION_THRESHOLD) return [{
|
|
75
|
+
ruleId: "RESOURCE-REPETITION-001",
|
|
76
|
+
type: "resource_abuse",
|
|
77
|
+
severity: "medium",
|
|
78
|
+
confidence: "medium",
|
|
79
|
+
description: `Single character repeated ${runLength} times`,
|
|
80
|
+
cwe: 1333,
|
|
81
|
+
primaryControl: "Bound input length at the boundary and use linear-time matching (RE2) for untrusted patterns",
|
|
82
|
+
variant: "raw",
|
|
83
|
+
matchedPattern: value.slice(runStart, runStart + MAX_MATCH_EXCERPT),
|
|
84
|
+
start: runStart,
|
|
85
|
+
end: i
|
|
86
|
+
}];
|
|
87
|
+
runStart = i;
|
|
88
|
+
}
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Scan a value against the rules for its declared sinks.
|
|
93
|
+
*
|
|
94
|
+
* Every applicable rule is evaluated against every applicable representation, so the
|
|
95
|
+
* finding list may contain one rule more than once with different `variant` values.
|
|
96
|
+
* Scoring collapses those — see {@link calculateThreatScore} — so an encoded payload
|
|
97
|
+
* is not penalized twice merely for being encoded.
|
|
98
|
+
*
|
|
99
|
+
* Non-string input (`null`, `undefined`, a number) is reported safe rather than
|
|
100
|
+
* throwing: this is a detector, and rejecting the wrong *type* belongs to the
|
|
101
|
+
* caller's schema layer.
|
|
102
|
+
*
|
|
103
|
+
* @param input - The candidate value.
|
|
104
|
+
* @param options - Contexts and tuning. See {@link ThreatScanOptions}.
|
|
105
|
+
* @returns A {@link ThreatScanResult}. Never throws.
|
|
106
|
+
*
|
|
107
|
+
* @example Scoping to the actual sink
|
|
108
|
+
* ```ts
|
|
109
|
+
* const bio = "I maintain build scripts under C:\\Windows\\System32";
|
|
110
|
+
*
|
|
111
|
+
* scanForThreats(bio, { contexts: ["general_text"] }).verdict; // "allow"
|
|
112
|
+
* scanForThreats(bio, { contexts: ["filesystem"] }).verdict; // "review"
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
function scanForThreats(input, options = {}) {
|
|
116
|
+
if (typeof input !== "string" || input.length === 0) return SAFE_RESULT;
|
|
117
|
+
const { contexts = ["general_text"], maxLength = MAX_SCAN_LENGTH, scanVariants = true, minSeverity = "low", excludeRuleIds, policy = DEFAULT_THREAT_POLICY } = options;
|
|
118
|
+
const truncated = input.length > maxLength;
|
|
119
|
+
const bounded = truncated ? input.slice(0, maxLength) : input;
|
|
120
|
+
const excluded = excludeRuleIds && excludeRuleIds.length > 0 ? new Set(excludeRuleIds) : null;
|
|
121
|
+
const severityFloor = SEVERITY_ORDER[minSeverity];
|
|
122
|
+
const rules = getRulesForContexts(contexts);
|
|
123
|
+
const variants = scanVariants ? buildInputVariants(bounded) : [{
|
|
124
|
+
kind: "raw",
|
|
125
|
+
value: bounded
|
|
126
|
+
}];
|
|
127
|
+
const findings = [];
|
|
128
|
+
for (const rule of rules) {
|
|
129
|
+
if (excluded?.has(rule.id)) continue;
|
|
130
|
+
if (SEVERITY_ORDER[rule.severity] < severityFloor) continue;
|
|
131
|
+
for (const variant of variants) {
|
|
132
|
+
if (rule.variants && !rule.variants.includes(variant.kind)) continue;
|
|
133
|
+
const match = rule.pattern.exec(variant.value);
|
|
134
|
+
if (match === null) continue;
|
|
135
|
+
findings.push({
|
|
136
|
+
ruleId: rule.id,
|
|
137
|
+
type: rule.type,
|
|
138
|
+
severity: rule.severity,
|
|
139
|
+
confidence: rule.confidence,
|
|
140
|
+
description: rule.description,
|
|
141
|
+
...rule.cwe === void 0 ? {} : { cwe: rule.cwe },
|
|
142
|
+
primaryControl: rule.primaryControl,
|
|
143
|
+
variant: variant.kind,
|
|
144
|
+
matchedPattern: match[0].slice(0, MAX_MATCH_EXCERPT),
|
|
145
|
+
start: match.index,
|
|
146
|
+
end: match.index + match[0].length
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (SEVERITY_ORDER.medium >= severityFloor && !excluded?.has("RESOURCE-REPETITION-001")) findings.push(...detectResourceAbuse(bounded));
|
|
151
|
+
const score = calculateThreatScore(findings);
|
|
152
|
+
const types = [];
|
|
153
|
+
for (const finding of findings) if (!types.includes(finding.type)) types.push(finding.type);
|
|
154
|
+
return {
|
|
155
|
+
isSafe: findings.length === 0,
|
|
156
|
+
score,
|
|
157
|
+
verdict: verdictForScore(score, policy),
|
|
158
|
+
findings,
|
|
159
|
+
types,
|
|
160
|
+
truncated,
|
|
161
|
+
...options.event === void 0 ? {} : { event: options.event }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
export { MAX_SCAN_LENGTH, scanForThreats };
|
|
166
|
+
|
|
167
|
+
//# sourceMappingURL=engine.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.mjs","names":[],"sources":["../../src/threats/engine.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview The scan engine: evaluate the rules applicable to a declared set of\n * sinks against a bounded set of input representations, then score the result.\n *\n * The engine's defining constraint is that **the caller must say where the value is\n * going**. Running every detector against every field is what makes signature-based\n * validation unusable in practice — a biography containing `C:\\Windows`, a support\n * ticket containing `1=1`, and a programming question containing `eval(` are all\n * ordinary text, and only become evidence when the value actually reaches a\n * filesystem, SQL, or HTML sink. Declaring the context is the difference between a\n * detector that catches attacks and one that rejects customers.\n *\n * @module @resq-systems/security/threats/engine\n */\n\nimport { getRulesForContexts } from \"./rules/index.js\";\nimport { calculateThreatScore, verdictForScore } from \"./scoring.js\";\nimport {\n\tDEFAULT_THREAT_POLICY,\n\ttype EventContext,\n\ttype InputVariant,\n\tSEVERITY_ORDER,\n\ttype ThreatContext,\n\ttype ThreatFinding,\n\ttype ThreatPolicy,\n\ttype ThreatSeverity,\n\ttype ThreatType,\n\ttype ThreatVerdict,\n} from \"./types.js\";\nimport { buildInputVariants } from \"./variants.js\";\n\n//#region Constants\n\n/**\n * Characters scanned before truncation.\n *\n * Bounds worst-case regex cost per call. Individual rules are bounded too — no\n * unbounded nested quantifiers — so this is a second line of defense rather than the\n * only one, but a 50 MB paste should not become a 50 MB × 89-rule scan regardless.\n */\nexport const MAX_SCAN_LENGTH = 100_000;\n\n/** Matched substrings are truncated to this many characters before entering a finding. */\nconst MAX_MATCH_EXCERPT = 50;\n\n/**\n * Run length at which a repeated single character is reported as resource abuse.\n * Set well above anything that occurs in prose or formatted text.\n */\nconst REPETITION_THRESHOLD = 200;\n\n//#endregion\n\n//#region Options and result\n\n/** Controls what {@link scanForThreats} evaluates and how it grades the outcome. */\nexport interface ThreatScanOptions {\n\t/**\n\t * Correlation metadata, echoed onto the result untouched.\n\t *\n\t * Affects nothing about detection. See {@link EventContext} for why it exists.\n\t */\n\treadonly event?: EventContext;\n\t/**\n\t * Sinks the value is destined for.\n\t *\n\t * Defaults to `[\"general_text\"]`, which enables only the universal rules — bidi\n\t * overrides, invisible characters, control characters. Any caller that wants\n\t * injection detection must name the sink it is protecting; that requirement is\n\t * the package's primary false-positive control, not an inconvenience.\n\t */\n\treadonly contexts?: readonly ThreatContext[];\n\t/** Truncation bound. Defaults to {@link MAX_SCAN_LENGTH}. */\n\treadonly maxLength?: number;\n\t/**\n\t * Scan encoded representations in addition to the raw string. Defaults to `true`.\n\t * Pass `false` when the caller has already decoded the value and re-decoding\n\t * would misrepresent it.\n\t */\n\treadonly scanVariants?: boolean;\n\t/** Drop findings below this severity before scoring. Defaults to `\"low\"`. */\n\treadonly minSeverity?: ThreatSeverity;\n\t/**\n\t * Rule IDs to skip. The tuning knob to reach for first: silencing one noisy rule\n\t * on one route beats disabling a whole category.\n\t */\n\treadonly excludeRuleIds?: readonly string[];\n\t/** Score thresholds. Defaults to {@link DEFAULT_THREAT_POLICY}. */\n\treadonly policy?: ThreatPolicy;\n}\n\n/** Outcome of a scan. */\nexport interface ThreatScanResult {\n\t/**\n\t * The {@link EventContext} the caller supplied, echoed verbatim.\n\t *\n\t * Present only when one was passed. The scanner neither resolves nor validates it.\n\t */\n\treadonly event?: EventContext;\n\t/** `true` when nothing fired at all — strictly stronger than `verdict === \"allow\"`. */\n\treadonly isSafe: boolean;\n\t/** Anomaly score. See {@link calculateThreatScore}. */\n\treadonly score: number;\n\t/** Policy band derived from {@link ThreatScanResult.score}. */\n\treadonly verdict: ThreatVerdict;\n\t/** Every finding, in catalog order then variant order. */\n\treadonly findings: readonly ThreatFinding[];\n\t/** Distinct weakness categories present, in first-seen order. */\n\treadonly types: readonly ThreatType[];\n\t/** `true` when the input exceeded `maxLength` and the tail went unscanned. */\n\treadonly truncated: boolean;\n}\n\n/** Result returned for empty or non-string input. */\nconst SAFE_RESULT: ThreatScanResult = {\n\tisSafe: true,\n\tscore: 0,\n\tverdict: \"allow\",\n\tfindings: [],\n\ttypes: [],\n\ttruncated: false,\n};\n\n//#endregion\n\n//#region Scanning\n\n/**\n * Structural check that is cheaper and more informative computed than matched.\n *\n * A backreference like `/(.)\\1{200,}/` expresses \"long repeated run\" but costs far\n * more than the single linear pass below, and the pass can report the actual run\n * length in the finding.\n *\n * @param value - Bounded input.\n * @returns At most one finding, for the first over-threshold run.\n */\nfunction detectResourceAbuse(value: string): ThreatFinding[] {\n\tlet runStart = 0;\n\n\tfor (let i = 1; i <= value.length; i++) {\n\t\tif (i < value.length && value[i] === value[runStart]) continue;\n\n\t\tconst runLength = i - runStart;\n\t\tif (runLength >= REPETITION_THRESHOLD) {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\truleId: \"RESOURCE-REPETITION-001\",\n\t\t\t\t\ttype: \"resource_abuse\",\n\t\t\t\t\tseverity: \"medium\",\n\t\t\t\t\tconfidence: \"medium\",\n\t\t\t\t\tdescription: `Single character repeated ${runLength} times`,\n\t\t\t\t\tcwe: 1333,\n\t\t\t\t\tprimaryControl:\n\t\t\t\t\t\t\"Bound input length at the boundary and use linear-time matching (RE2) for untrusted patterns\",\n\t\t\t\t\tvariant: \"raw\",\n\t\t\t\t\tmatchedPattern: value.slice(runStart, runStart + MAX_MATCH_EXCERPT),\n\t\t\t\t\tstart: runStart,\n\t\t\t\t\tend: i,\n\t\t\t\t},\n\t\t\t];\n\t\t}\n\t\trunStart = i;\n\t}\n\n\treturn [];\n}\n\n/**\n * Scan a value against the rules for its declared sinks.\n *\n * Every applicable rule is evaluated against every applicable representation, so the\n * finding list may contain one rule more than once with different `variant` values.\n * Scoring collapses those — see {@link calculateThreatScore} — so an encoded payload\n * is not penalized twice merely for being encoded.\n *\n * Non-string input (`null`, `undefined`, a number) is reported safe rather than\n * throwing: this is a detector, and rejecting the wrong *type* belongs to the\n * caller's schema layer.\n *\n * @param input - The candidate value.\n * @param options - Contexts and tuning. See {@link ThreatScanOptions}.\n * @returns A {@link ThreatScanResult}. Never throws.\n *\n * @example Scoping to the actual sink\n * ```ts\n * const bio = \"I maintain build scripts under C:\\\\Windows\\\\System32\";\n *\n * scanForThreats(bio, { contexts: [\"general_text\"] }).verdict; // \"allow\"\n * scanForThreats(bio, { contexts: [\"filesystem\"] }).verdict; // \"review\"\n * ```\n */\nexport function scanForThreats(input: string, options: ThreatScanOptions = {}): ThreatScanResult {\n\tif (typeof input !== \"string\" || input.length === 0) {\n\t\treturn SAFE_RESULT;\n\t}\n\n\tconst {\n\t\tcontexts = [\"general_text\"],\n\t\tmaxLength = MAX_SCAN_LENGTH,\n\t\tscanVariants = true,\n\t\tminSeverity = \"low\",\n\t\texcludeRuleIds,\n\t\tpolicy = DEFAULT_THREAT_POLICY,\n\t} = options;\n\n\tconst truncated = input.length > maxLength;\n\tconst bounded = truncated ? input.slice(0, maxLength) : input;\n\n\tconst excluded = excludeRuleIds && excludeRuleIds.length > 0 ? new Set(excludeRuleIds) : null;\n\tconst severityFloor = SEVERITY_ORDER[minSeverity];\n\n\tconst rules = getRulesForContexts(contexts);\n\tconst rawOnly: readonly InputVariant[] = [{ kind: \"raw\", value: bounded }];\n\tconst variants = scanVariants ? buildInputVariants(bounded) : rawOnly;\n\n\tconst findings: ThreatFinding[] = [];\n\n\tfor (const rule of rules) {\n\t\tif (excluded?.has(rule.id)) continue;\n\t\tif (SEVERITY_ORDER[rule.severity] < severityFloor) continue;\n\n\t\tfor (const variant of variants) {\n\t\t\tif (rule.variants && !rule.variants.includes(variant.kind)) continue;\n\n\t\t\t// Patterns are validated non-global at catalog load, so `exec` carries no\n\t\t\t// lastIndex state between calls and always matches from position 0.\n\t\t\tconst match = rule.pattern.exec(variant.value);\n\t\t\tif (match === null) continue;\n\n\t\t\tfindings.push({\n\t\t\t\truleId: rule.id,\n\t\t\t\ttype: rule.type,\n\t\t\t\tseverity: rule.severity,\n\t\t\t\tconfidence: rule.confidence,\n\t\t\t\tdescription: rule.description,\n\t\t\t\t...(rule.cwe === undefined ? {} : { cwe: rule.cwe }),\n\t\t\t\tprimaryControl: rule.primaryControl,\n\t\t\t\tvariant: variant.kind,\n\t\t\t\tmatchedPattern: match[0].slice(0, MAX_MATCH_EXCERPT),\n\t\t\t\tstart: match.index,\n\t\t\t\tend: match.index + match[0].length,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (SEVERITY_ORDER.medium >= severityFloor && !excluded?.has(\"RESOURCE-REPETITION-001\")) {\n\t\tfindings.push(...detectResourceAbuse(bounded));\n\t}\n\n\tconst score = calculateThreatScore(findings);\n\n\tconst types: ThreatType[] = [];\n\tfor (const finding of findings) {\n\t\tif (!types.includes(finding.type)) types.push(finding.type);\n\t}\n\n\treturn {\n\t\tisSafe: findings.length === 0,\n\t\tscore,\n\t\tverdict: verdictForScore(score, policy),\n\t\tfindings,\n\t\ttypes,\n\t\ttruncated,\n\t\t...(options.event === undefined ? {} : { event: options.event }),\n\t};\n}\n\n//#endregion\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,kBAAkB;;AAG/B,MAAM,oBAAoB;;;;;AAM1B,MAAM,uBAAuB;;AAiE7B,MAAM,cAAgC;CACrC,QAAQ;CACR,OAAO;CACP,SAAS;CACT,UAAU,CAAC;CACX,OAAO,CAAC;CACR,WAAW;AACZ;;;;;;;;;;;AAgBA,SAAS,oBAAoB,OAAgC;CAC5D,IAAI,WAAW;CAEf,KAAK,IAAI,IAAI,GAAG,KAAK,MAAM,QAAQ,KAAK;EACvC,IAAI,IAAI,MAAM,UAAU,MAAM,OAAO,MAAM,WAAW;EAEtD,MAAM,YAAY,IAAI;EACtB,IAAI,aAAa,sBAChB,OAAO,CACN;GACC,QAAQ;GACR,MAAM;GACN,UAAU;GACV,YAAY;GACZ,aAAa,6BAA6B,UAAU;GACpD,KAAK;GACL,gBACC;GACD,SAAS;GACT,gBAAgB,MAAM,MAAM,UAAU,WAAW,iBAAiB;GAClE,OAAO;GACP,KAAK;EACN,CACD;EAED,WAAW;CACZ;CAEA,OAAO,CAAC;AACT;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,eAAe,OAAe,UAA6B,CAAC,GAAqB;CAChG,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAGR,MAAM,EACL,WAAW,CAAC,cAAc,GAC1B,YAAY,iBACZ,eAAe,MACf,cAAc,OACd,gBACA,SAAS,0BACN;CAEJ,MAAM,YAAY,MAAM,SAAS;CACjC,MAAM,UAAU,YAAY,MAAM,MAAM,GAAG,SAAS,IAAI;CAExD,MAAM,WAAW,kBAAkB,eAAe,SAAS,IAAI,IAAI,IAAI,cAAc,IAAI;CACzF,MAAM,gBAAgB,eAAe;CAErC,MAAM,QAAQ,oBAAoB,QAAQ;CAE1C,MAAM,WAAW,eAAe,mBAAmB,OAAO,IAAI,CADpB;EAAE,MAAM;EAAO,OAAO;CAAQ,CACJ;CAEpE,MAAM,WAA4B,CAAC;CAEnC,KAAK,MAAM,QAAQ,OAAO;EACzB,IAAI,UAAU,IAAI,KAAK,EAAE,GAAG;EAC5B,IAAI,eAAe,KAAK,YAAY,eAAe;EAEnD,KAAK,MAAM,WAAW,UAAU;GAC/B,IAAI,KAAK,YAAY,CAAC,KAAK,SAAS,SAAS,QAAQ,IAAI,GAAG;GAI5D,MAAM,QAAQ,KAAK,QAAQ,KAAK,QAAQ,KAAK;GAC7C,IAAI,UAAU,MAAM;GAEpB,SAAS,KAAK;IACb,QAAQ,KAAK;IACb,MAAM,KAAK;IACX,UAAU,KAAK;IACf,YAAY,KAAK;IACjB,aAAa,KAAK;IAClB,GAAI,KAAK,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI;IAClD,gBAAgB,KAAK;IACrB,SAAS,QAAQ;IACjB,gBAAgB,MAAM,EAAE,CAAC,MAAM,GAAG,iBAAiB;IACnD,OAAO,MAAM;IACb,KAAK,MAAM,QAAQ,MAAM,EAAE,CAAC;GAC7B,CAAC;EACF;CACD;CAEA,IAAI,eAAe,UAAU,iBAAiB,CAAC,UAAU,IAAI,yBAAyB,GACrF,SAAS,KAAK,GAAG,oBAAoB,OAAO,CAAC;CAG9C,MAAM,QAAQ,qBAAqB,QAAQ;CAE3C,MAAM,QAAsB,CAAC;CAC7B,KAAK,MAAM,WAAW,UACrB,IAAI,CAAC,MAAM,SAAS,QAAQ,IAAI,GAAG,MAAM,KAAK,QAAQ,IAAI;CAG3D,OAAO;EACN,QAAQ,SAAS,WAAW;EAC5B;EACA,SAAS,gBAAgB,OAAO,MAAM;EACtC;EACA;EACA;EACA,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;CAC/D;AACD"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ALL_THREAT_CONTEXTS, CONFIDENCE_MULTIPLIERS, DEFAULT_THREAT_POLICY, EventContext, InputSource, InputVariant, InputVariantKind, SEVERITY_ORDER, SEVERITY_WEIGHTS, ThreatConfidence, ThreatContext, ThreatFinding, ThreatPolicy, ThreatRule, ThreatSeverity, ThreatType, ThreatVerdict } from "./types.mjs";
|
|
2
|
+
import { MAX_SCAN_LENGTH, ThreatScanOptions, ThreatScanResult, scanForThreats } from "./engine.mjs";
|
|
3
|
+
import { LDAP_INJECTION_RULES, NOSQL_INJECTION_RULES, SQL_INJECTION_RULES, XPATH_INJECTION_RULES } from "./rules/datastore.mjs";
|
|
4
|
+
import { PROMPT_INJECTION_RULES, TEMPLATE_INJECTION_RULES, UNIVERSAL_RULES, XML_INJECTION_RULES } from "./rules/markup.mjs";
|
|
5
|
+
import { COMMAND_INJECTION_RULES, FILE_INCLUSION_RULES, PATH_TRAVERSAL_RULES, SSRF_RULES } from "./rules/system.mjs";
|
|
6
|
+
import { FORMULA_INJECTION_RULES, HEADER_INJECTION_RULES, LOG_INJECTION_RULES, PROTOTYPE_POLLUTION_RULES, XSS_RULES } from "./rules/web.mjs";
|
|
7
|
+
import { THREAT_RULES, assertRuleCatalogIsValid, getRulesForContexts } from "./rules/index.mjs";
|
|
8
|
+
import { ThreatTypeSummary, calculateThreatScore, scoreForFinding, summarizeByType, verdictForScore } from "./scoring.mjs";
|
|
9
|
+
import { buildInputVariants, decodeHtmlEntities, tryPercentDecode } from "./variants.mjs";
|
|
10
|
+
import { ATTACK_PATTERNS, AttackPattern, attackPatternsForCwe } from "./capec.generated.mjs";
|
|
11
|
+
export { ALL_THREAT_CONTEXTS, ATTACK_PATTERNS, type AttackPattern, COMMAND_INJECTION_RULES, CONFIDENCE_MULTIPLIERS, DEFAULT_THREAT_POLICY, type EventContext, FILE_INCLUSION_RULES, FORMULA_INJECTION_RULES, HEADER_INJECTION_RULES, type InputSource, type InputVariant, type InputVariantKind, LDAP_INJECTION_RULES, LOG_INJECTION_RULES, MAX_SCAN_LENGTH, NOSQL_INJECTION_RULES, PATH_TRAVERSAL_RULES, PROMPT_INJECTION_RULES, PROTOTYPE_POLLUTION_RULES, SEVERITY_ORDER, SEVERITY_WEIGHTS, SQL_INJECTION_RULES, SSRF_RULES, TEMPLATE_INJECTION_RULES, THREAT_RULES, type ThreatConfidence, type ThreatContext, type ThreatFinding, type ThreatPolicy, type ThreatRule, type ThreatScanOptions, type ThreatScanResult, type ThreatSeverity, type ThreatType, type ThreatTypeSummary, type ThreatVerdict, UNIVERSAL_RULES, XML_INJECTION_RULES, XPATH_INJECTION_RULES, XSS_RULES, assertRuleCatalogIsValid, attackPatternsForCwe, buildInputVariants, calculateThreatScore, decodeHtmlEntities, getRulesForContexts, scanForThreats, scoreForFinding, summarizeByType, tryPercentDecode, verdictForScore };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LDAP_INJECTION_RULES, NOSQL_INJECTION_RULES, SQL_INJECTION_RULES, XPATH_INJECTION_RULES } from "./rules/datastore.mjs";
|
|
2
|
+
import { ALL_THREAT_CONTEXTS, CONFIDENCE_MULTIPLIERS, DEFAULT_THREAT_POLICY, SEVERITY_ORDER, SEVERITY_WEIGHTS } from "./types.mjs";
|
|
3
|
+
import { PROMPT_INJECTION_RULES, TEMPLATE_INJECTION_RULES, UNIVERSAL_RULES, XML_INJECTION_RULES } from "./rules/markup.mjs";
|
|
4
|
+
import { COMMAND_INJECTION_RULES, FILE_INCLUSION_RULES, PATH_TRAVERSAL_RULES, SSRF_RULES } from "./rules/system.mjs";
|
|
5
|
+
import { FORMULA_INJECTION_RULES, HEADER_INJECTION_RULES, LOG_INJECTION_RULES, PROTOTYPE_POLLUTION_RULES, XSS_RULES } from "./rules/web.mjs";
|
|
6
|
+
import { THREAT_RULES, assertRuleCatalogIsValid, getRulesForContexts } from "./rules/index.mjs";
|
|
7
|
+
import { calculateThreatScore, scoreForFinding, summarizeByType, verdictForScore } from "./scoring.mjs";
|
|
8
|
+
import { buildInputVariants, decodeHtmlEntities, tryPercentDecode } from "./variants.mjs";
|
|
9
|
+
import { MAX_SCAN_LENGTH, scanForThreats } from "./engine.mjs";
|
|
10
|
+
import { ATTACK_PATTERNS, attackPatternsForCwe } from "./capec.generated.mjs";
|
|
11
|
+
export { ALL_THREAT_CONTEXTS, ATTACK_PATTERNS, COMMAND_INJECTION_RULES, CONFIDENCE_MULTIPLIERS, DEFAULT_THREAT_POLICY, FILE_INCLUSION_RULES, FORMULA_INJECTION_RULES, HEADER_INJECTION_RULES, LDAP_INJECTION_RULES, LOG_INJECTION_RULES, MAX_SCAN_LENGTH, NOSQL_INJECTION_RULES, PATH_TRAVERSAL_RULES, PROMPT_INJECTION_RULES, PROTOTYPE_POLLUTION_RULES, SEVERITY_ORDER, SEVERITY_WEIGHTS, SQL_INJECTION_RULES, SSRF_RULES, TEMPLATE_INJECTION_RULES, THREAT_RULES, UNIVERSAL_RULES, XML_INJECTION_RULES, XPATH_INJECTION_RULES, XSS_RULES, assertRuleCatalogIsValid, attackPatternsForCwe, buildInputVariants, calculateThreatScore, decodeHtmlEntities, getRulesForContexts, scanForThreats, scoreForFinding, summarizeByType, tryPercentDecode, verdictForScore };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ThreatRule } from "../types.mjs";
|
|
2
|
+
//#region src/threats/rules/datastore.d.ts
|
|
3
|
+
/** SQL-injection signatures. Scoped to the `sql` context. */
|
|
4
|
+
declare const SQL_INJECTION_RULES: readonly ThreatRule[];
|
|
5
|
+
/** Document-store operator injection (MongoDB and compatible drivers). */
|
|
6
|
+
declare const NOSQL_INJECTION_RULES: readonly ThreatRule[];
|
|
7
|
+
/** LDAP filter and DN injection. */
|
|
8
|
+
declare const LDAP_INJECTION_RULES: readonly ThreatRule[];
|
|
9
|
+
/** XPath/XQuery injection. */
|
|
10
|
+
declare const XPATH_INJECTION_RULES: readonly ThreatRule[];
|
|
11
|
+
//#endregion
|
|
12
|
+
export { LDAP_INJECTION_RULES, NOSQL_INJECTION_RULES, SQL_INJECTION_RULES, XPATH_INJECTION_RULES };
|
|
13
|
+
//# sourceMappingURL=datastore.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"datastore.d.mts","names":[],"sources":["../../../src/threats/rules/datastore.ts"],"mappings":";;;cA4Ca,8BAA8B;;cA+O9B,gCAAgC;;cAyDhC,+BAA+B;;cA6E/B,gCAAgC"}
|