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
package/dist/runtime/express.js
CHANGED
|
@@ -1,3 +1,315 @@
|
|
|
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
|
+
|
|
1
313
|
// src/runtime/signatures.ts
|
|
2
314
|
var SQLI = /('\s*or\s+'?1'?\s*='?1)|union\s+select|;\s*drop\s+table|;\s*delete\s+from/i;
|
|
3
315
|
var XSS = /<\s*script\b|javascript:\s*|onerror\s*=/i;
|
|
@@ -227,14 +539,39 @@ function createInspector(options = {}) {
|
|
|
227
539
|
}
|
|
228
540
|
|
|
229
541
|
// src/runtime/express.ts
|
|
542
|
+
function attachExpressDlp(req, res, dlp) {
|
|
543
|
+
const path = req.path ?? req.url?.split("?")[0] ?? "/";
|
|
544
|
+
const ip = req.ip ?? "unknown";
|
|
545
|
+
const origJson = res.json?.bind(res);
|
|
546
|
+
const origSend = res.send?.bind(res);
|
|
547
|
+
let busy = false;
|
|
548
|
+
const run = (body, write) => {
|
|
549
|
+
if (busy) return write(body);
|
|
550
|
+
busy = true;
|
|
551
|
+
try {
|
|
552
|
+
const ct = res.getHeader?.("content-type");
|
|
553
|
+
const out = applyDlpWrite(dlp, body, { path, ip, contentType: typeof ct === "string" ? ct : void 0 });
|
|
554
|
+
if (out.block) {
|
|
555
|
+
return res.status(500).json({ error: "internal error" });
|
|
556
|
+
}
|
|
557
|
+
return write(out.body);
|
|
558
|
+
} finally {
|
|
559
|
+
busy = false;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
if (origJson) res.json = (body) => run(body, origJson);
|
|
563
|
+
if (origSend) res.send = (body) => run(body, origSend);
|
|
564
|
+
}
|
|
230
565
|
function shieldGuard(options = {}) {
|
|
231
566
|
const inspect = createInspector(options);
|
|
567
|
+
const dlp = createDlpInspector(options);
|
|
232
568
|
return function fullshieldGuard(req, res, next) {
|
|
233
569
|
const result = inspect(req);
|
|
234
570
|
if (result.block) {
|
|
235
571
|
res.status(403).json({ error: "blocked", ruleId: result.event?.ruleId });
|
|
236
572
|
return;
|
|
237
573
|
}
|
|
574
|
+
if (dlp !== void 0) attachExpressDlp(req, res, dlp);
|
|
238
575
|
next();
|
|
239
576
|
};
|
|
240
577
|
}
|
package/dist/runtime/fastify.js
CHANGED
|
@@ -1,3 +1,315 @@
|
|
|
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
|
+
|
|
1
313
|
// src/runtime/signatures.ts
|
|
2
314
|
var SQLI = /('\s*or\s+'?1'?\s*='?1)|union\s+select|;\s*drop\s+table|;\s*delete\s+from/i;
|
|
3
315
|
var XSS = /<\s*script\b|javascript:\s*|onerror\s*=/i;
|
|
@@ -229,6 +541,7 @@ function createInspector(options = {}) {
|
|
|
229
541
|
// src/runtime/fastify.ts
|
|
230
542
|
function shieldGuard(options = {}) {
|
|
231
543
|
const inspect = createInspector(options);
|
|
544
|
+
const dlp = createDlpInspector(options);
|
|
232
545
|
return function fullshieldFastify(instance, _opts, done) {
|
|
233
546
|
instance.addHook("preHandler", (req, reply, next) => {
|
|
234
547
|
const result = inspect({
|
|
@@ -247,6 +560,18 @@ function shieldGuard(options = {}) {
|
|
|
247
560
|
}
|
|
248
561
|
next();
|
|
249
562
|
});
|
|
563
|
+
if (dlp !== void 0) {
|
|
564
|
+
instance.addHook("onSend", (req, reply, payload, sendDone) => {
|
|
565
|
+
const path = req.url?.split("?")[0] ?? "/";
|
|
566
|
+
const out = applyDlpWrite(dlp, payload, { path, ip: req.ip });
|
|
567
|
+
if (out.block) {
|
|
568
|
+
reply.code(500);
|
|
569
|
+
sendDone?.(null, JSON.stringify({ error: "internal error" }));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
sendDone?.(null, typeof out.body === "string" ? out.body : JSON.stringify(out.body));
|
|
573
|
+
});
|
|
574
|
+
}
|
|
250
575
|
done();
|
|
251
576
|
};
|
|
252
577
|
}
|