fullshield 0.1.1 → 0.1.3
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/CHANGELOG.md +96 -0
- package/README.md +43 -5
- package/dist/agent.js +3408 -607
- package/dist/bench/score.js +118 -0
- package/dist/cli/index.js +7137 -3385
- package/dist/compliance.js +31 -2
- package/dist/dashboard.js +20 -1
- package/dist/index.d.ts +152 -2
- package/dist/index.js +3773 -670
- package/dist/mcp.js +3430 -629
- package/dist/runtime/auto.js +100 -3
- package/dist/runtime/dlp.js +313 -0
- package/dist/runtime/express.js +337 -0
- package/dist/runtime/fastify.js +325 -0
- package/dist/runtime/hono.js +570 -0
- package/dist/runtime/koa.js +573 -0
- package/dist/runtime/rasp.js +100 -3
- package/dist/runtime/register.js +100 -3
- package/dist/supply.js +87 -1
- package/dist/vex.js +178 -0
- package/package.json +12 -1
- package/rules/CONFIG-DIRLIST-001.md +9 -0
- package/rules/CONFIG-TRUSTPROXY-001.md +11 -0
- package/rules/DLP-CARD-001.md +9 -0
- package/rules/DLP-INTERNAL-001.md +9 -0
- package/rules/DLP-PII-001.md +9 -0
- package/rules/DLP-SECRET-001.md +9 -0
- package/rules/DLP-STACK-001.md +9 -0
- package/rules/HOST-SSH-001.md +13 -0
- package/rules/HOST-SSH-002.md +11 -0
- package/rules/HOST-SSH-003.md +11 -0
- package/rules/MCP-001.md +7 -0
- package/rules/MCP-002.md +7 -0
- package/rules/MCP-003.md +7 -0
- package/rules/MCP-004.md +7 -0
- package/rules/MCP-005.md +7 -0
- package/rules/README.md +47 -1
- package/rules/SAST-COOKIE-001.md +11 -0
- package/rules/SAST-CSRF-001.md +11 -0
- package/rules/SAST-DESER-001.md +11 -0
- package/rules/SAST-GQL-001.md +11 -0
- package/rules/SAST-HASH-001.md +11 -0
- package/rules/SAST-MASSASSIGN-001.md +11 -0
- package/rules/SAST-NOSQLI-001.md +6 -2
- package/rules/SAST-REDOS-001.md +11 -0
- package/rules/SAST-TIMING-001.md +11 -0
- package/rules/SAST-UPLOAD-001.md +11 -0
- package/rules/SAST-XXE-001.md +11 -0
- package/rules/SECRET-BUNDLE-001.md +11 -0
- package/rules/SECRET-SESSION-001.md +9 -0
- package/rules/SUPPLY-WORKFLOW-001.md +9 -0
- package/rules/SUPPLY-WORM-001.md +9 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
// src/runtime/dlp/index.ts
|
|
2
|
+
var DLP_FLAG = "FULLSHIELD_EXPERIMENTAL_DLP";
|
|
3
|
+
var DLP_MAX_BYTES = 64 * 1024;
|
|
4
|
+
var DLP_GENERIC = { error: "internal error" };
|
|
5
|
+
var SECRET_RES = [
|
|
6
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
7
|
+
/\bsk_(?:live|test)_[A-Za-z0-9]{16,}\b/g,
|
|
8
|
+
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36}\b/g,
|
|
9
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
|
|
10
|
+
/\bAIza[0-9A-Za-z_-]{35}\b/g,
|
|
11
|
+
/\bsk-ant-[A-Za-z0-9_-]{16,}\b|\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
12
|
+
/-----BEGIN (?:[A-Z]+ )*PRIVATE KEY-----/g,
|
|
13
|
+
/\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s'"@/:]+:[^\s'"@]+@/g
|
|
14
|
+
];
|
|
15
|
+
var STACK_RE = /(?:Error|Exception|TypeError|ReferenceError|SyntaxError|RangeError)[^\n]{0,240}(?:\n|\r\n|\\n)[ \t]*at[ \t]+\S+[^\n]{0,320}(?:\n|\r\n|\\n)[ \t]*at[ \t]+/g;
|
|
16
|
+
var INTERNAL_IP_RE = /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3})\b/g;
|
|
17
|
+
var HOST_TLDS = [".internal", ".corp", ".lan", ".local"];
|
|
18
|
+
var BULK_EMAILS = 5;
|
|
19
|
+
var PII_LEARN = 2;
|
|
20
|
+
var REDACT = "[REDACTED]";
|
|
21
|
+
var EMPTY_HITS = [];
|
|
22
|
+
function isDlpEnabled() {
|
|
23
|
+
return process.env[DLP_FLAG] === "1";
|
|
24
|
+
}
|
|
25
|
+
function allMatches(re, text) {
|
|
26
|
+
re.lastIndex = 0;
|
|
27
|
+
return [...text.matchAll(re)];
|
|
28
|
+
}
|
|
29
|
+
function maskSample(value) {
|
|
30
|
+
if (value.length <= 4) return "****";
|
|
31
|
+
return `${value.slice(0, 4)}****`.slice(0, 40);
|
|
32
|
+
}
|
|
33
|
+
function luhnOk(digits) {
|
|
34
|
+
if (digits.length < 13 || digits.length > 19) return false;
|
|
35
|
+
const first = digits.charCodeAt(0) - 48;
|
|
36
|
+
if (first < 3 || first > 6) return false;
|
|
37
|
+
if (digits.split("").every((c) => c === digits[0])) return false;
|
|
38
|
+
let sum = 0;
|
|
39
|
+
let alt = false;
|
|
40
|
+
for (let i = digits.length - 1; i >= 0; i -= 1) {
|
|
41
|
+
let n = digits.charCodeAt(i) - 48;
|
|
42
|
+
if (alt) {
|
|
43
|
+
n *= 2;
|
|
44
|
+
if (n > 9) n -= 9;
|
|
45
|
+
}
|
|
46
|
+
sum += n;
|
|
47
|
+
alt = !alt;
|
|
48
|
+
}
|
|
49
|
+
return sum % 10 === 0;
|
|
50
|
+
}
|
|
51
|
+
var SECRET_MARKERS = [
|
|
52
|
+
"AKIA",
|
|
53
|
+
"sk_",
|
|
54
|
+
"ghp_",
|
|
55
|
+
"gho_",
|
|
56
|
+
"ghu_",
|
|
57
|
+
"ghs_",
|
|
58
|
+
"ghr_",
|
|
59
|
+
"xox",
|
|
60
|
+
"AIza",
|
|
61
|
+
"sk-",
|
|
62
|
+
"BEGIN ",
|
|
63
|
+
"postgres",
|
|
64
|
+
"mysql",
|
|
65
|
+
"mariadb",
|
|
66
|
+
"mongodb",
|
|
67
|
+
"redis",
|
|
68
|
+
"amqp"
|
|
69
|
+
];
|
|
70
|
+
function hasAny(text, markers) {
|
|
71
|
+
for (const m of markers) {
|
|
72
|
+
if (text.includes(m)) return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function collectSpans(text) {
|
|
77
|
+
const spans = [];
|
|
78
|
+
if (hasAny(text, SECRET_MARKERS)) {
|
|
79
|
+
for (const re of SECRET_RES) {
|
|
80
|
+
for (const m of allMatches(re, text)) {
|
|
81
|
+
if (m[0] === void 0 || m.index === void 0) continue;
|
|
82
|
+
if (re.source.includes("postgres") && /\$\{|^<[^>]+>$|^\{[^}]+\}$/.test(m[0])) continue;
|
|
83
|
+
spans.push({ start: m.index, end: m.index + m[0].length, ruleId: "DLP-SECRET-001", text: m[0] });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (text.includes(" at ") || text.includes("Error") || text.includes("Exception")) {
|
|
88
|
+
for (const m of allMatches(STACK_RE, text)) {
|
|
89
|
+
if (m[0] === void 0 || m.index === void 0) continue;
|
|
90
|
+
spans.push({ start: m.index, end: m.index + m[0].length, ruleId: "DLP-STACK-001", text: m[0] });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (/\d/.test(text)) {
|
|
94
|
+
for (const m of allMatches(INTERNAL_IP_RE, text)) {
|
|
95
|
+
if (m[0] === void 0 || m.index === void 0) continue;
|
|
96
|
+
spans.push({ start: m.index, end: m.index + m[0].length, ruleId: "DLP-INTERNAL-001", text: m[0] });
|
|
97
|
+
}
|
|
98
|
+
spans.push(...findCards(text));
|
|
99
|
+
}
|
|
100
|
+
if (text.includes("localhost") || text.includes("LOCALHOST")) {
|
|
101
|
+
spans.push(...findLocalhost(text));
|
|
102
|
+
}
|
|
103
|
+
if (text.includes(".internal") || text.includes(".corp") || text.includes(".lan") || text.includes(".local") || text.includes(".INTERNAL") || text.includes(".CORP") || text.includes(".LAN") || text.includes(".LOCAL")) {
|
|
104
|
+
spans.push(...findInternalHosts(text));
|
|
105
|
+
}
|
|
106
|
+
return spans;
|
|
107
|
+
}
|
|
108
|
+
function isWord(c) {
|
|
109
|
+
return c !== void 0 && /[A-Za-z0-9-]/.test(c);
|
|
110
|
+
}
|
|
111
|
+
function findIgnoreCase(hay, needle, from) {
|
|
112
|
+
const n = needle.length;
|
|
113
|
+
outer: for (let i = from; i <= hay.length - n; i += 1) {
|
|
114
|
+
for (let j = 0; j < n; j += 1) {
|
|
115
|
+
const a = hay.charCodeAt(i + j);
|
|
116
|
+
const b = needle.charCodeAt(j);
|
|
117
|
+
if (a !== b && a !== b - 32 && a !== b + 32) continue outer;
|
|
118
|
+
}
|
|
119
|
+
return i;
|
|
120
|
+
}
|
|
121
|
+
return -1;
|
|
122
|
+
}
|
|
123
|
+
function findLocalhost(text) {
|
|
124
|
+
const spans = [];
|
|
125
|
+
let from = 0;
|
|
126
|
+
while (from < text.length) {
|
|
127
|
+
const idx = findIgnoreCase(text, "localhost", from);
|
|
128
|
+
if (idx < 0) break;
|
|
129
|
+
if (!isWord(text[idx - 1]) && !isWord(text[idx + 9])) {
|
|
130
|
+
spans.push({ start: idx, end: idx + 9, ruleId: "DLP-INTERNAL-001", text: text.slice(idx, idx + 9) });
|
|
131
|
+
}
|
|
132
|
+
from = idx + 9;
|
|
133
|
+
}
|
|
134
|
+
return spans;
|
|
135
|
+
}
|
|
136
|
+
function findInternalHosts(text) {
|
|
137
|
+
const spans = [];
|
|
138
|
+
for (const tld of HOST_TLDS) {
|
|
139
|
+
let from = 0;
|
|
140
|
+
while (from < text.length) {
|
|
141
|
+
const idx = findIgnoreCase(text, tld, from);
|
|
142
|
+
if (idx < 0) break;
|
|
143
|
+
let start = idx - 1;
|
|
144
|
+
while (start >= 0 && /[A-Za-z0-9.-]/.test(text[start] ?? "")) start -= 1;
|
|
145
|
+
start += 1;
|
|
146
|
+
const label = text.slice(start, idx).toLowerCase();
|
|
147
|
+
const end = idx + tld.length;
|
|
148
|
+
if (label.length > 0 && /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/.test(label) && !isWord(text[end])) {
|
|
149
|
+
spans.push({ start, end, ruleId: "DLP-INTERNAL-001", text: text.slice(start, end) });
|
|
150
|
+
}
|
|
151
|
+
from = idx + tld.length;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return spans;
|
|
155
|
+
}
|
|
156
|
+
function findCards(text) {
|
|
157
|
+
const spans = [];
|
|
158
|
+
let i = 0;
|
|
159
|
+
while (i < text.length) {
|
|
160
|
+
const code = text.charCodeAt(i);
|
|
161
|
+
if (code < 48 || code > 57) {
|
|
162
|
+
i += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
let j = i;
|
|
166
|
+
let digits = "";
|
|
167
|
+
while (j < text.length) {
|
|
168
|
+
const c = text.charCodeAt(j);
|
|
169
|
+
if (c >= 48 && c <= 57) {
|
|
170
|
+
digits += text[j] ?? "";
|
|
171
|
+
j += 1;
|
|
172
|
+
} else if (c === 32 || c === 9 || c === 45) {
|
|
173
|
+
j += 1;
|
|
174
|
+
} else {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (luhnOk(digits)) {
|
|
179
|
+
spans.push({ start: i, end: j, ruleId: "DLP-CARD-001", text: text.slice(i, j) });
|
|
180
|
+
}
|
|
181
|
+
i = j > i ? j : i + 1;
|
|
182
|
+
}
|
|
183
|
+
return spans;
|
|
184
|
+
}
|
|
185
|
+
function findEmails(text) {
|
|
186
|
+
const spans = [];
|
|
187
|
+
let from = 0;
|
|
188
|
+
while (from < text.length) {
|
|
189
|
+
const at = text.indexOf("@", from);
|
|
190
|
+
if (at < 0) break;
|
|
191
|
+
let start = at - 1;
|
|
192
|
+
while (start >= 0 && /[A-Za-z0-9._%+-]/.test(text[start] ?? "")) start -= 1;
|
|
193
|
+
start += 1;
|
|
194
|
+
let end = at + 1;
|
|
195
|
+
while (end < text.length && /[A-Za-z0-9.-]/.test(text[end] ?? "")) end += 1;
|
|
196
|
+
const local = text.slice(start, at);
|
|
197
|
+
const domain = text.slice(at + 1, end);
|
|
198
|
+
const dot = domain.lastIndexOf(".");
|
|
199
|
+
const tld = dot > 0 ? domain.slice(dot + 1) : "";
|
|
200
|
+
if (local.length >= 1 && local.length <= 64 && domain.length >= 3 && domain.length <= 255 && /^[A-Za-z]{2,24}$/.test(tld)) {
|
|
201
|
+
spans.push({ start, end, ruleId: "DLP-PII-001", text: text.slice(start, end) });
|
|
202
|
+
}
|
|
203
|
+
from = at + 1;
|
|
204
|
+
}
|
|
205
|
+
return spans;
|
|
206
|
+
}
|
|
207
|
+
function countEmails(text) {
|
|
208
|
+
if (!text.includes("@")) return { count: 0, spans: [] };
|
|
209
|
+
const spans = findEmails(text);
|
|
210
|
+
return { count: spans.length, spans };
|
|
211
|
+
}
|
|
212
|
+
function redact(text, spans) {
|
|
213
|
+
const ordered = [...spans].sort((a, b) => b.start - a.start);
|
|
214
|
+
let out = text;
|
|
215
|
+
const taken = [];
|
|
216
|
+
for (const s of ordered) {
|
|
217
|
+
if (taken.some(([a, b]) => s.start < b && s.end > a)) continue;
|
|
218
|
+
taken.push([s.start, s.end]);
|
|
219
|
+
out = `${out.slice(0, s.start)}${REDACT}${out.slice(s.end)}`;
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
function uniqueHits(spans) {
|
|
224
|
+
const seen = /* @__PURE__ */ new Set();
|
|
225
|
+
const hits = [];
|
|
226
|
+
for (const s of spans) {
|
|
227
|
+
if (seen.has(s.ruleId)) continue;
|
|
228
|
+
seen.add(s.ruleId);
|
|
229
|
+
hits.push({ ruleId: s.ruleId, sample: maskSample(s.text) });
|
|
230
|
+
}
|
|
231
|
+
return hits;
|
|
232
|
+
}
|
|
233
|
+
function toText(body) {
|
|
234
|
+
if (body === null || body === void 0) return void 0;
|
|
235
|
+
if (typeof body === "string") return { text: body, kind: "string" };
|
|
236
|
+
if (typeof body === "number" || typeof body === "boolean") return { text: String(body), kind: "string" };
|
|
237
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(body)) return void 0;
|
|
238
|
+
try {
|
|
239
|
+
return { text: JSON.stringify(body), kind: "json" };
|
|
240
|
+
} catch {
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function createDlpInspector(options = {}) {
|
|
245
|
+
if (!isDlpEnabled() || options.dlp === void 0) return void 0;
|
|
246
|
+
const mode = options.dlp.mode;
|
|
247
|
+
const maxBytes = options.dlp.maxBytes ?? DLP_MAX_BYTES;
|
|
248
|
+
const routes = /* @__PURE__ */ new Map();
|
|
249
|
+
let skipped = 0;
|
|
250
|
+
const inspect = (body, ctx) => {
|
|
251
|
+
const ct = ctx.contentType ?? "";
|
|
252
|
+
if (ct.length > 0 && !/json|text|javascript|xml/i.test(ct)) {
|
|
253
|
+
return { action: "allow", body, hits: [], skippedOverCap: false };
|
|
254
|
+
}
|
|
255
|
+
const serialized = toText(body);
|
|
256
|
+
if (serialized === void 0) {
|
|
257
|
+
return { action: "allow", body, hits: [], skippedOverCap: false };
|
|
258
|
+
}
|
|
259
|
+
if (serialized.text.length > maxBytes) {
|
|
260
|
+
skipped += 1;
|
|
261
|
+
return { action: "skip", body, hits: [], skippedOverCap: true };
|
|
262
|
+
}
|
|
263
|
+
const spans = collectSpans(serialized.text);
|
|
264
|
+
const emails = countEmails(serialized.text);
|
|
265
|
+
let pii = routes.get(ctx.path);
|
|
266
|
+
if (pii === void 0) {
|
|
267
|
+
pii = { samples: 0, seenLow: false, seenHigh: false };
|
|
268
|
+
routes.set(ctx.path, pii);
|
|
269
|
+
}
|
|
270
|
+
if (pii.samples < PII_LEARN) {
|
|
271
|
+
pii.samples += 1;
|
|
272
|
+
if (emails.count <= 1) pii.seenLow = true;
|
|
273
|
+
if (emails.count >= BULK_EMAILS) pii.seenHigh = true;
|
|
274
|
+
} else if (pii.seenLow && !pii.seenHigh && emails.count >= BULK_EMAILS) {
|
|
275
|
+
spans.push(...emails.spans);
|
|
276
|
+
}
|
|
277
|
+
const hits = uniqueHits(spans);
|
|
278
|
+
if (hits.length === 0) {
|
|
279
|
+
return { action: "allow", body, hits: EMPTY_HITS, skippedOverCap: false };
|
|
280
|
+
}
|
|
281
|
+
const ip = ctx.ip ?? "unknown";
|
|
282
|
+
for (const hit of hits) {
|
|
283
|
+
const event = { ruleId: hit.ruleId, path: ctx.path, ip, sample: hit.sample };
|
|
284
|
+
options.onEvent?.(event);
|
|
285
|
+
}
|
|
286
|
+
if (mode === "monitor") {
|
|
287
|
+
return { action: "allow", body, hits, skippedOverCap: false };
|
|
288
|
+
}
|
|
289
|
+
if (mode === "block") {
|
|
290
|
+
return { action: "block", body: { ...DLP_GENERIC }, hits, skippedOverCap: false };
|
|
291
|
+
}
|
|
292
|
+
const maskedText = redact(serialized.text, spans);
|
|
293
|
+
let masked = maskedText;
|
|
294
|
+
if (serialized.kind === "json") {
|
|
295
|
+
try {
|
|
296
|
+
masked = JSON.parse(maskedText);
|
|
297
|
+
} catch {
|
|
298
|
+
masked = maskedText;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return { action: "mask", body: masked, hits, skippedOverCap: false };
|
|
302
|
+
};
|
|
303
|
+
const api = inspect;
|
|
304
|
+
Object.defineProperty(api, "inspect", { value: inspect });
|
|
305
|
+
Object.defineProperty(api, "skippedOverCap", { get: () => skipped });
|
|
306
|
+
return api;
|
|
307
|
+
}
|
|
308
|
+
function applyDlpWrite(dlp, body, ctx) {
|
|
309
|
+
const result = dlp.inspect(body, ctx);
|
|
310
|
+
return { body: result.body, block: result.action === "block" };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/runtime/signatures.ts
|
|
314
|
+
var SQLI = /('\s*or\s+'?1'?\s*='?1)|union\s+select|;\s*drop\s+table|;\s*delete\s+from/i;
|
|
315
|
+
var XSS = /<\s*script\b|javascript:\s*|onerror\s*=/i;
|
|
316
|
+
var PATH = /(\.\.\/|\.\.\\|%2e%2e%2f|%2e%2e\\)/i;
|
|
317
|
+
function flattenValues(value, out = [], depth = 0) {
|
|
318
|
+
if (depth > 8 || out.length > 200) return out;
|
|
319
|
+
if (typeof value === "string") {
|
|
320
|
+
out.push(value);
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
if (Array.isArray(value)) {
|
|
324
|
+
for (const item of value) flattenValues(item, out, depth + 1);
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
if (value !== null && typeof value === "object") {
|
|
328
|
+
for (const item of Object.values(value)) {
|
|
329
|
+
flattenValues(item, out, depth + 1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
function detectAttack(text) {
|
|
335
|
+
if (PATH.test(text)) return "path";
|
|
336
|
+
if (XSS.test(text)) return "xss";
|
|
337
|
+
if (SQLI.test(text)) return "sqli";
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
function inspectValues(values) {
|
|
341
|
+
for (const value of values) {
|
|
342
|
+
const hit = detectAttack(value);
|
|
343
|
+
if (hit !== void 0) return hit;
|
|
344
|
+
}
|
|
345
|
+
return void 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/runtime/lru.ts
|
|
349
|
+
var LruMap = class {
|
|
350
|
+
constructor(max) {
|
|
351
|
+
this.max = max;
|
|
352
|
+
if (max < 1) throw new Error("LruMap max must be >= 1");
|
|
353
|
+
}
|
|
354
|
+
max;
|
|
355
|
+
map = /* @__PURE__ */ new Map();
|
|
356
|
+
get size() {
|
|
357
|
+
return this.map.size;
|
|
358
|
+
}
|
|
359
|
+
has(key) {
|
|
360
|
+
return this.map.has(key);
|
|
361
|
+
}
|
|
362
|
+
get(key) {
|
|
363
|
+
if (!this.map.has(key)) return void 0;
|
|
364
|
+
const value = this.map.get(key);
|
|
365
|
+
this.map.delete(key);
|
|
366
|
+
this.map.set(key, value);
|
|
367
|
+
return value;
|
|
368
|
+
}
|
|
369
|
+
set(key, value) {
|
|
370
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
371
|
+
this.map.set(key, value);
|
|
372
|
+
while (this.map.size > this.max) {
|
|
373
|
+
const oldest = this.map.keys().next().value;
|
|
374
|
+
if (oldest === void 0) break;
|
|
375
|
+
this.map.delete(oldest);
|
|
376
|
+
}
|
|
377
|
+
return this;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/runtime/rateLimit.ts
|
|
382
|
+
var DEFAULT_MAX_KEYS = 1e4;
|
|
383
|
+
function createRateLimiter(options) {
|
|
384
|
+
const maxKeys = options.maxKeys ?? DEFAULT_MAX_KEYS;
|
|
385
|
+
const hits = new LruMap(maxKeys);
|
|
386
|
+
return {
|
|
387
|
+
allow(key, now = Date.now()) {
|
|
388
|
+
const windowStart = now - options.windowMs;
|
|
389
|
+
const prev = hits.get(key);
|
|
390
|
+
if (prev === void 0) {
|
|
391
|
+
hits.set(key, [now]);
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
let keep = 0;
|
|
395
|
+
for (let i = 0; i < prev.length; i += 1) {
|
|
396
|
+
const t = prev[i];
|
|
397
|
+
if (t !== void 0 && t > windowStart) {
|
|
398
|
+
prev[keep] = t;
|
|
399
|
+
keep += 1;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
prev.length = keep;
|
|
403
|
+
if (prev.length >= options.max) {
|
|
404
|
+
hits.set(key, prev);
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
prev.push(now);
|
|
408
|
+
hits.set(key, prev);
|
|
409
|
+
return true;
|
|
410
|
+
},
|
|
411
|
+
get size() {
|
|
412
|
+
return hits.size;
|
|
413
|
+
},
|
|
414
|
+
get maxKeys() {
|
|
415
|
+
return maxKeys;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/runtime/inspect.ts
|
|
421
|
+
var RULE = {
|
|
422
|
+
sqli: "RUNTIME-SQLI-001",
|
|
423
|
+
xss: "RUNTIME-XSS-001",
|
|
424
|
+
path: "RUNTIME-PATH-001"
|
|
425
|
+
};
|
|
426
|
+
function bodyStats(value, depth = 0) {
|
|
427
|
+
if (value === null || value === void 0) return { bytes: 0, depth, keys: 0 };
|
|
428
|
+
if (typeof value === "string") return { bytes: value.length, depth, keys: 0 };
|
|
429
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
430
|
+
return { bytes: String(value).length, depth, keys: 0 };
|
|
431
|
+
}
|
|
432
|
+
if (typeof value !== "object") return { bytes: 0, depth, keys: 0 };
|
|
433
|
+
const entries = Object.entries(value);
|
|
434
|
+
let bytes = 2;
|
|
435
|
+
let keys = entries.length;
|
|
436
|
+
let maxDepth = depth;
|
|
437
|
+
for (const [k, v] of entries) {
|
|
438
|
+
bytes += k.length;
|
|
439
|
+
const inner = bodyStats(v, depth + 1);
|
|
440
|
+
bytes += inner.bytes;
|
|
441
|
+
keys += inner.keys;
|
|
442
|
+
if (inner.depth > maxDepth) maxDepth = inner.depth;
|
|
443
|
+
}
|
|
444
|
+
return { bytes, depth: maxDepth, keys };
|
|
445
|
+
}
|
|
446
|
+
var DEFAULT_MAX_ROUTES = 1e3;
|
|
447
|
+
var DEFAULT_MAX_PARAM_NAMES = 200;
|
|
448
|
+
function keysOf(value) {
|
|
449
|
+
if (value === null || value === void 0 || typeof value !== "object") return [];
|
|
450
|
+
return Object.keys(value);
|
|
451
|
+
}
|
|
452
|
+
function createInspector(options = {}) {
|
|
453
|
+
const mode = options.mode ?? "monitor";
|
|
454
|
+
const authRoutes = new Set(options.authRoutes ?? ["/login"]);
|
|
455
|
+
const limiter = createRateLimiter(options.rateLimit ?? { windowMs: 6e4, max: 20 });
|
|
456
|
+
const maxBodyBytes = options.maxBodyBytes ?? 1e6;
|
|
457
|
+
const maxBodyDepth = options.maxBodyDepth ?? 8;
|
|
458
|
+
const maxBodyKeys = options.maxBodyKeys ?? 200;
|
|
459
|
+
const anomaly = options.anomaly;
|
|
460
|
+
const maxRoutes = anomaly?.maxRoutes ?? DEFAULT_MAX_ROUTES;
|
|
461
|
+
const maxParamNames = anomaly?.maxParamNames ?? DEFAULT_MAX_PARAM_NAMES;
|
|
462
|
+
const routes = new LruMap(maxRoutes);
|
|
463
|
+
let windowStart;
|
|
464
|
+
const inspect = ((req) => {
|
|
465
|
+
const path = req.path ?? req.url?.split("?")[0] ?? "/";
|
|
466
|
+
const ip = req.ip ?? "unknown";
|
|
467
|
+
const emit = (ruleId, sample, shouldBlock) => {
|
|
468
|
+
const event = { ruleId, path, ip, sample: sample.slice(0, 40) };
|
|
469
|
+
options.onEvent?.(event);
|
|
470
|
+
if (shouldBlock) options.onBlock?.(event);
|
|
471
|
+
return { block: shouldBlock, event };
|
|
472
|
+
};
|
|
473
|
+
if (authRoutes.has(path) && !limiter.allow(`${ip}:${path}`)) {
|
|
474
|
+
return emit("RUNTIME-RATELIMIT-001", path, mode === "block");
|
|
475
|
+
}
|
|
476
|
+
const stats = bodyStats(req.body);
|
|
477
|
+
if (stats.bytes > maxBodyBytes || stats.depth > maxBodyDepth || stats.keys > maxBodyKeys) {
|
|
478
|
+
return emit("RUNTIME-BODY-001", "oversized-body", mode === "block");
|
|
479
|
+
}
|
|
480
|
+
const values = [
|
|
481
|
+
...flattenValues(req.query),
|
|
482
|
+
...flattenValues(req.body),
|
|
483
|
+
...flattenValues(req.params)
|
|
484
|
+
];
|
|
485
|
+
const kind = inspectValues(values);
|
|
486
|
+
if (kind !== void 0) {
|
|
487
|
+
return emit(RULE[kind], kind, mode === "block");
|
|
488
|
+
}
|
|
489
|
+
if (anomaly !== void 0) {
|
|
490
|
+
const now = anomaly.now ?? Date.now;
|
|
491
|
+
const t = now();
|
|
492
|
+
if (windowStart === void 0) windowStart = t;
|
|
493
|
+
const learning = t - windowStart < anomaly.learnMinutes * 6e4;
|
|
494
|
+
let baseline = routes.get(path);
|
|
495
|
+
if (baseline === void 0) {
|
|
496
|
+
baseline = {
|
|
497
|
+
count: 0,
|
|
498
|
+
bodySum: 0,
|
|
499
|
+
paramNames: new LruMap(maxParamNames),
|
|
500
|
+
status401: 0,
|
|
501
|
+
status404: 0,
|
|
502
|
+
recent404: 0
|
|
503
|
+
};
|
|
504
|
+
routes.set(path, baseline);
|
|
505
|
+
}
|
|
506
|
+
const names = [...keysOf(req.query), ...keysOf(req.body), ...keysOf(req.params)];
|
|
507
|
+
const status = req.statusCode;
|
|
508
|
+
if (learning) {
|
|
509
|
+
baseline.count += 1;
|
|
510
|
+
baseline.bodySum += stats.bytes;
|
|
511
|
+
for (const n of names) baseline.paramNames.set(n, true);
|
|
512
|
+
if (status === 401) baseline.status401 += 1;
|
|
513
|
+
if (status === 404) baseline.status404 += 1;
|
|
514
|
+
} else {
|
|
515
|
+
const newParam = names.some((n) => !baseline.paramNames.has(n));
|
|
516
|
+
const spike401 = status === 401 && baseline.status401 === 0 && baseline.count > 0;
|
|
517
|
+
if (status === 404) baseline.recent404 += 1;
|
|
518
|
+
else baseline.recent404 = 0;
|
|
519
|
+
const burst404 = baseline.recent404 >= 3 && baseline.status404 === 0 && baseline.count > 0;
|
|
520
|
+
if (newParam || spike401 || burst404) {
|
|
521
|
+
const event = {
|
|
522
|
+
ruleId: "RUNTIME-ANOMALY-001",
|
|
523
|
+
path,
|
|
524
|
+
ip,
|
|
525
|
+
sample: newParam ? "new-param" : spike401 ? "401-spike" : "404-burst"
|
|
526
|
+
};
|
|
527
|
+
anomaly.onAnomaly?.(event);
|
|
528
|
+
options.onEvent?.(event);
|
|
529
|
+
return { block: false, event };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return { block: false };
|
|
534
|
+
});
|
|
535
|
+
Object.defineProperty(inspect, "limiterSize", { get: () => limiter.size });
|
|
536
|
+
Object.defineProperty(inspect, "routeCount", { get: () => routes.size });
|
|
537
|
+
inspect.paramCount = (path) => routes.get(path)?.paramNames.size ?? 0;
|
|
538
|
+
return inspect;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/runtime/koa.ts
|
|
542
|
+
function shieldGuard(options = {}) {
|
|
543
|
+
const inspect = createInspector(options);
|
|
544
|
+
const dlp = createDlpInspector(options);
|
|
545
|
+
return async function fullshieldKoa(ctx, next) {
|
|
546
|
+
const result = inspect({
|
|
547
|
+
method: ctx.method,
|
|
548
|
+
url: ctx.url,
|
|
549
|
+
path: ctx.path ?? ctx.url?.split("?")[0],
|
|
550
|
+
ip: ctx.ip,
|
|
551
|
+
query: ctx.query,
|
|
552
|
+
body: ctx.request?.body,
|
|
553
|
+
params: ctx.params,
|
|
554
|
+
headers: ctx.headers
|
|
555
|
+
});
|
|
556
|
+
if (result.block) {
|
|
557
|
+
ctx.status = 403;
|
|
558
|
+
ctx.body = { error: "blocked", ruleId: result.event?.ruleId };
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
await next();
|
|
562
|
+
if (dlp === void 0) return;
|
|
563
|
+
const out = applyDlpWrite(dlp, ctx.body, { path: ctx.path ?? ctx.url?.split("?")[0] ?? "/", ip: ctx.ip });
|
|
564
|
+
if (out.block) {
|
|
565
|
+
ctx.status = 500;
|
|
566
|
+
ctx.body = { error: "internal error" };
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
ctx.body = out.body;
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export { shieldGuard };
|
package/dist/runtime/rasp.js
CHANGED
|
@@ -526,9 +526,25 @@ function sinkTexts(sink) {
|
|
|
526
526
|
if (typeof sink === "number" || typeof sink === "boolean") return [String(sink)];
|
|
527
527
|
return [];
|
|
528
528
|
}
|
|
529
|
+
function whereJsChangesStructure(sink, ctx) {
|
|
530
|
+
let code = "";
|
|
531
|
+
if (typeof sink === "string") code = sink;
|
|
532
|
+
else if (sink !== null && typeof sink === "object" && !Array.isArray(sink) && "$where" in sink) {
|
|
533
|
+
const where = sink.$where;
|
|
534
|
+
if (typeof where === "string") code = where;
|
|
535
|
+
}
|
|
536
|
+
if (!code) return false;
|
|
537
|
+
for (const input of ctx.strings) {
|
|
538
|
+
if (input && sqlInputChangesStructure(code, input)) return true;
|
|
539
|
+
}
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
529
542
|
function detectStructureChange(kind, sink, ctx, base) {
|
|
530
543
|
if (kind === "proto") return protoPolluted(ctx);
|
|
531
|
-
if (kind === "nosql")
|
|
544
|
+
if (kind === "nosql") {
|
|
545
|
+
if (nosqlUserOperators(sink, ctx)) return true;
|
|
546
|
+
return whereJsChangesStructure(sink, ctx);
|
|
547
|
+
}
|
|
532
548
|
for (const text of sinkTexts(sink)) {
|
|
533
549
|
for (const input of ctx.strings) {
|
|
534
550
|
if (!input) continue;
|
|
@@ -826,11 +842,38 @@ function patchMethod(obj, key, wrapper) {
|
|
|
826
842
|
record[key] = orig;
|
|
827
843
|
});
|
|
828
844
|
}
|
|
845
|
+
function sqlTextFrom(sql) {
|
|
846
|
+
if (typeof sql === "string") return sql;
|
|
847
|
+
if (Array.isArray(sql)) {
|
|
848
|
+
return sql.filter((part) => typeof part === "string").join(" ");
|
|
849
|
+
}
|
|
850
|
+
if (sql !== null && typeof sql === "object") {
|
|
851
|
+
const rec = sql;
|
|
852
|
+
if (typeof rec.text === "string") return rec.text;
|
|
853
|
+
if (typeof rec.sql === "string") return rec.sql;
|
|
854
|
+
if (Array.isArray(rec.strings)) {
|
|
855
|
+
return rec.strings.filter((part) => typeof part === "string").join(" ");
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return "";
|
|
859
|
+
}
|
|
829
860
|
function wrapSqlMethod(obj, key) {
|
|
830
861
|
patchMethod(obj, key, (orig) => {
|
|
831
862
|
return function(sql, ...rest) {
|
|
832
|
-
|
|
833
|
-
|
|
863
|
+
return applySink(
|
|
864
|
+
"sql",
|
|
865
|
+
sqlTextFrom(sql),
|
|
866
|
+
void 0,
|
|
867
|
+
() => orig.call(this, sql, ...rest)
|
|
868
|
+
);
|
|
869
|
+
};
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
function wrapNosqlWhere(obj, key) {
|
|
873
|
+
patchMethod(obj, key, (orig) => {
|
|
874
|
+
return function(expr, ...rest) {
|
|
875
|
+
const sink = typeof expr === "string" ? { $where: expr } : expr ?? {};
|
|
876
|
+
return applySink("nosql", sink, void 0, () => orig.call(this, expr, ...rest));
|
|
834
877
|
};
|
|
835
878
|
});
|
|
836
879
|
}
|
|
@@ -1121,6 +1164,57 @@ function wrapKnownPackage(request, exported) {
|
|
|
1121
1164
|
wrappedPackages.add(obj);
|
|
1122
1165
|
return exported;
|
|
1123
1166
|
}
|
|
1167
|
+
if (name === "@prisma/client" || name === "prisma" || name.endsWith("/@prisma/client")) {
|
|
1168
|
+
const client = exported.PrismaClient?.prototype;
|
|
1169
|
+
if (client) {
|
|
1170
|
+
wrapSqlMethod(client, "$queryRawUnsafe");
|
|
1171
|
+
wrapSqlMethod(client, "$executeRawUnsafe");
|
|
1172
|
+
wrapSqlMethod(client, "$queryRaw");
|
|
1173
|
+
wrapSqlMethod(client, "$executeRaw");
|
|
1174
|
+
}
|
|
1175
|
+
const Prisma = exported.Prisma;
|
|
1176
|
+
if (Prisma) wrapSqlMethod(Prisma, "raw");
|
|
1177
|
+
wrappedPackages.add(obj);
|
|
1178
|
+
return exported;
|
|
1179
|
+
}
|
|
1180
|
+
if (name === "sequelize" || name.endsWith("/sequelize")) {
|
|
1181
|
+
const rec = exported;
|
|
1182
|
+
const proto = rec.Sequelize?.prototype ?? rec.prototype;
|
|
1183
|
+
if (proto) wrapSqlMethod(proto, "query");
|
|
1184
|
+
if (typeof rec.literal === "function") wrapSqlMethod(rec, "literal");
|
|
1185
|
+
if (rec.Sequelize && typeof rec.Sequelize.literal === "function") wrapSqlMethod(rec.Sequelize, "literal");
|
|
1186
|
+
wrappedPackages.add(obj);
|
|
1187
|
+
return exported;
|
|
1188
|
+
}
|
|
1189
|
+
if (name === "knex" || name.endsWith("/knex")) {
|
|
1190
|
+
if (typeof exported.raw === "function") wrapSqlMethod(obj, "raw");
|
|
1191
|
+
const client = exported.Client?.prototype;
|
|
1192
|
+
if (client) {
|
|
1193
|
+
wrapSqlMethod(client, "raw");
|
|
1194
|
+
wrapSqlMethod(client, "whereRaw");
|
|
1195
|
+
wrapSqlMethod(client, "orderByRaw");
|
|
1196
|
+
wrapSqlMethod(client, "query");
|
|
1197
|
+
}
|
|
1198
|
+
wrappedPackages.add(obj);
|
|
1199
|
+
return exported;
|
|
1200
|
+
}
|
|
1201
|
+
if (name === "typeorm" || name.endsWith("/typeorm")) {
|
|
1202
|
+
const rec = exported;
|
|
1203
|
+
for (const cls of ["DataSource", "EntityManager", "QueryRunner", "Connection"]) {
|
|
1204
|
+
const proto = rec[cls]?.prototype;
|
|
1205
|
+
if (proto) wrapSqlMethod(proto, "query");
|
|
1206
|
+
}
|
|
1207
|
+
const qb = rec.SelectQueryBuilder?.prototype ?? rec.QueryBuilder?.prototype;
|
|
1208
|
+
if (qb) wrapSqlMethod(qb, "where");
|
|
1209
|
+
wrappedPackages.add(obj);
|
|
1210
|
+
return exported;
|
|
1211
|
+
}
|
|
1212
|
+
if (name === "drizzle-orm" || name.endsWith("/drizzle-orm")) {
|
|
1213
|
+
const sql = exported.sql;
|
|
1214
|
+
if (sql) wrapSqlMethod(sql, "raw");
|
|
1215
|
+
wrappedPackages.add(obj);
|
|
1216
|
+
return exported;
|
|
1217
|
+
}
|
|
1124
1218
|
if (name === "mongodb" || name.includes("mongodb")) {
|
|
1125
1219
|
const col = exported.Collection?.prototype;
|
|
1126
1220
|
if (col) {
|
|
@@ -1141,7 +1235,10 @@ function wrapKnownPackage(request, exported) {
|
|
|
1141
1235
|
return applySink("nosql", query ?? {}, void 0, () => orig.call(this, query, ...rest));
|
|
1142
1236
|
};
|
|
1143
1237
|
});
|
|
1238
|
+
wrapNosqlWhere(q, "$where");
|
|
1144
1239
|
}
|
|
1240
|
+
const model = exported.Model?.prototype;
|
|
1241
|
+
if (model) wrapNosqlWhere(model, "$where");
|
|
1145
1242
|
wrappedPackages.add(obj);
|
|
1146
1243
|
return exported;
|
|
1147
1244
|
}
|