@tuent/sentinel 0.1.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/LICENSE +201 -0
- package/README.md +96 -0
- package/dist/Sentinel-B_sv8Kiy.d.ts +1785 -0
- package/dist/Sentinel-JLQL3YRD.js +10 -0
- package/dist/auditTrailKeys-GKCW5KUD.js +23 -0
- package/dist/chunk-2FFMYSVC.js +428 -0
- package/dist/chunk-3U3PKD4N.js +539 -0
- package/dist/chunk-6MHWJATS.js +1221 -0
- package/dist/chunk-CUJKNIKT.js +62 -0
- package/dist/chunk-FMZWHT4M.js +20 -0
- package/dist/chunk-NUXSUSYY.js +95 -0
- package/dist/chunk-PDWWRZXF.js +238 -0
- package/dist/chunk-QFRDEISP.js +7429 -0
- package/dist/chunk-Z3PWIJKT.js +2268 -0
- package/dist/cli.js +80 -0
- package/dist/gateway/index.d.ts +241 -0
- package/dist/gateway/index.js +10 -0
- package/dist/gatewayDaemon.js +25 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +28 -0
- package/dist/logAdapter-IB6ZDEV2.js +7 -0
- package/dist/mcpAdapter-R47GX2P3.js +178 -0
- package/dist/pidManager-ZYC7SICM.js +15 -0
- package/dist/policyLoader-6KR5VFVV.js +15 -0
- package/dist/webhookReceiver-NAVMQ6N5.js +203 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
// src/repoSensitivityMap.ts
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
var DEFAULT_MAP_PATH = path.join(os.homedir(), ".dahlia", "repo-sensitivity.json");
|
|
6
|
+
async function saveMap(map, customPath) {
|
|
7
|
+
const target = customPath ?? DEFAULT_MAP_PATH;
|
|
8
|
+
const parent = path.dirname(target);
|
|
9
|
+
const tempPath = target + ".tmp";
|
|
10
|
+
try {
|
|
11
|
+
await fs.mkdir(parent, { recursive: true });
|
|
12
|
+
await fs.writeFile(tempPath, JSON.stringify(map, null, 2), "utf-8");
|
|
13
|
+
await fs.rename(tempPath, target);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
try {
|
|
16
|
+
await fs.unlink(tempPath);
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
20
|
+
throw new Error(`RepoSensitivityMap.saveMap: failed to save to ${target}: ${message}`, {
|
|
21
|
+
cause: err
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function loadMap(customPath) {
|
|
26
|
+
const target = customPath ?? DEFAULT_MAP_PATH;
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = await fs.readFile(target, "utf-8");
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err.code === "ENOENT") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(raw);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
+
throw new Error(`RepoSensitivityMap.loadMap: file at ${target} is malformed: ${message}`, {
|
|
42
|
+
cause: err
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const scannedAt = typeof parsed.scannedAt === "string" ? parsed.scannedAt : "unknown";
|
|
46
|
+
const repoRoot = typeof parsed.repoRoot === "string" ? parsed.repoRoot : "";
|
|
47
|
+
const entries = Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
48
|
+
const summary = parsed.summary && typeof parsed.summary === "object" && !Array.isArray(parsed.summary) ? parsed.summary : computeSummaryFromEntries(entries);
|
|
49
|
+
return { scannedAt, repoRoot, entries, summary };
|
|
50
|
+
}
|
|
51
|
+
function lookupEntry(map, target, repoRoot) {
|
|
52
|
+
if (!target || typeof target !== "string" || target.trim() === "") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const lookupRoot = repoRoot ?? map.repoRoot;
|
|
56
|
+
let canonical;
|
|
57
|
+
if (path.isAbsolute(target)) {
|
|
58
|
+
if (!lookupRoot) return null;
|
|
59
|
+
const rel = path.relative(lookupRoot, target);
|
|
60
|
+
if (rel.startsWith("..")) return null;
|
|
61
|
+
canonical = rel;
|
|
62
|
+
} else {
|
|
63
|
+
canonical = target;
|
|
64
|
+
}
|
|
65
|
+
if (canonical.startsWith("./")) {
|
|
66
|
+
canonical = canonical.slice(2);
|
|
67
|
+
}
|
|
68
|
+
canonical = canonical.replace(/\\/g, "/");
|
|
69
|
+
for (const entry of map.entries) {
|
|
70
|
+
if (entry.path === canonical) return entry;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function computeSummaryFromEntries(entries) {
|
|
75
|
+
const byCategory = {};
|
|
76
|
+
const byConfidence = {
|
|
77
|
+
low: 0,
|
|
78
|
+
medium: 0,
|
|
79
|
+
high: 0
|
|
80
|
+
};
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
byCategory[entry.category] = (byCategory[entry.category] ?? 0) + 1;
|
|
83
|
+
if (entry.confidence === "low" || entry.confidence === "medium" || entry.confidence === "high") {
|
|
84
|
+
byConfidence[entry.confidence]++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
totalFiles: entries.length,
|
|
89
|
+
sensitiveFiles: entries.length,
|
|
90
|
+
byCategory,
|
|
91
|
+
byConfidence
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/repoSensitivityOverlay.ts
|
|
96
|
+
import * as fs2 from "fs/promises";
|
|
97
|
+
import * as path2 from "path";
|
|
98
|
+
import * as os2 from "os";
|
|
99
|
+
var DEFAULT_OVERLAY_PATH = path2.join(
|
|
100
|
+
os2.homedir(),
|
|
101
|
+
".dahlia",
|
|
102
|
+
"repo-sensitivity.review.json"
|
|
103
|
+
);
|
|
104
|
+
async function saveOverlay(overlay, customPath) {
|
|
105
|
+
const target = customPath ?? DEFAULT_OVERLAY_PATH;
|
|
106
|
+
const parent = path2.dirname(target);
|
|
107
|
+
const tempPath = target + ".tmp";
|
|
108
|
+
try {
|
|
109
|
+
await fs2.mkdir(parent, { recursive: true });
|
|
110
|
+
await fs2.writeFile(tempPath, JSON.stringify(overlay, null, 2), "utf-8");
|
|
111
|
+
await fs2.rename(tempPath, target);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
try {
|
|
114
|
+
await fs2.unlink(tempPath);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
118
|
+
throw new Error(`SensitivityOverlay.saveOverlay: failed to save to ${target}: ${message}`, {
|
|
119
|
+
cause: err
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function loadOverlay(customPath) {
|
|
124
|
+
const target = customPath ?? DEFAULT_OVERLAY_PATH;
|
|
125
|
+
let raw;
|
|
126
|
+
try {
|
|
127
|
+
raw = await fs2.readFile(target, "utf-8");
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (err.code === "ENOENT") {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(raw);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
139
|
+
throw new Error(`SensitivityOverlay.loadOverlay: file at ${target} is malformed: ${message}`, {
|
|
140
|
+
cause: err
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const version = typeof parsed.version === "string" ? parsed.version : "1.0";
|
|
144
|
+
const repoRoot = typeof parsed.repoRoot === "string" ? parsed.repoRoot : "";
|
|
145
|
+
const decisions = Array.isArray(parsed.decisions) ? parsed.decisions : [];
|
|
146
|
+
const updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "unknown";
|
|
147
|
+
return { version, repoRoot, decisions, updatedAt };
|
|
148
|
+
}
|
|
149
|
+
function lookupOverlayDecision(overlay, target, repoRoot) {
|
|
150
|
+
if (!target || typeof target !== "string" || target.trim() === "") {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const lookupRoot = repoRoot ?? overlay.repoRoot;
|
|
154
|
+
let canonical;
|
|
155
|
+
if (path2.isAbsolute(target)) {
|
|
156
|
+
if (!lookupRoot) return null;
|
|
157
|
+
const rel = path2.relative(lookupRoot, target);
|
|
158
|
+
if (rel.startsWith("..")) return null;
|
|
159
|
+
canonical = rel;
|
|
160
|
+
} else {
|
|
161
|
+
canonical = target;
|
|
162
|
+
}
|
|
163
|
+
if (canonical.startsWith("./")) {
|
|
164
|
+
canonical = canonical.slice(2);
|
|
165
|
+
}
|
|
166
|
+
canonical = canonical.replace(/\\/g, "/");
|
|
167
|
+
for (const decision of overlay.decisions) {
|
|
168
|
+
if (decision.path === canonical) return decision;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
function applyOverlay(overlay, decisionPath, decisionType, options) {
|
|
173
|
+
let canonical = decisionPath;
|
|
174
|
+
if (canonical.startsWith("./")) {
|
|
175
|
+
canonical = canonical.slice(2);
|
|
176
|
+
}
|
|
177
|
+
canonical = canonical.replace(/\\/g, "/");
|
|
178
|
+
const newDecision = {
|
|
179
|
+
path: canonical,
|
|
180
|
+
decision: decisionType,
|
|
181
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
182
|
+
};
|
|
183
|
+
if (options?.reason !== void 0) newDecision.reason = options.reason;
|
|
184
|
+
if (options?.sensitivity !== void 0) newDecision.sensitivity = options.sensitivity;
|
|
185
|
+
if (options?.category !== void 0) newDecision.category = options.category;
|
|
186
|
+
const existing = overlay.decisions.findIndex((d) => d.path === canonical);
|
|
187
|
+
const decisions = existing >= 0 ? [
|
|
188
|
+
...overlay.decisions.slice(0, existing),
|
|
189
|
+
newDecision,
|
|
190
|
+
...overlay.decisions.slice(existing + 1)
|
|
191
|
+
] : [...overlay.decisions, newDecision];
|
|
192
|
+
return {
|
|
193
|
+
...overlay,
|
|
194
|
+
decisions,
|
|
195
|
+
updatedAt: newDecision.decidedAt
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function removeOverlayDecision(overlay, decisionPath) {
|
|
199
|
+
let canonical = decisionPath;
|
|
200
|
+
if (canonical.startsWith("./")) {
|
|
201
|
+
canonical = canonical.slice(2);
|
|
202
|
+
}
|
|
203
|
+
canonical = canonical.replace(/\\/g, "/");
|
|
204
|
+
const filtered = overlay.decisions.filter((d) => d.path !== canonical);
|
|
205
|
+
if (filtered.length === overlay.decisions.length) {
|
|
206
|
+
return overlay;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
...overlay,
|
|
210
|
+
decisions: filtered,
|
|
211
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function createEmptyOverlay(repoRoot) {
|
|
215
|
+
return {
|
|
216
|
+
version: "1.0",
|
|
217
|
+
repoRoot,
|
|
218
|
+
decisions: [],
|
|
219
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/defaults.ts
|
|
224
|
+
var DEFAULT_FORBIDDEN_PATTERNS = [
|
|
225
|
+
"**/.env",
|
|
226
|
+
"**/.env.*",
|
|
227
|
+
"**/.ssh/**",
|
|
228
|
+
"**/.aws/**",
|
|
229
|
+
"**/secrets/**",
|
|
230
|
+
"**/credentials/**",
|
|
231
|
+
"**/id_rsa*",
|
|
232
|
+
"**/id_dsa*",
|
|
233
|
+
"**/id_ecdsa*",
|
|
234
|
+
"**/id_ed25519*",
|
|
235
|
+
"**/*.pem",
|
|
236
|
+
"**/*.key",
|
|
237
|
+
"/etc/**"
|
|
238
|
+
];
|
|
239
|
+
var DEFAULT_MEDIUM_DISPOSITION = {
|
|
240
|
+
network_request: "deny"
|
|
241
|
+
};
|
|
242
|
+
var DEFAULT_NETWORK_DENYLIST_CIDRS = [
|
|
243
|
+
"10.0.0.0/8",
|
|
244
|
+
// RFC1918 private (Class A)
|
|
245
|
+
"172.16.0.0/12",
|
|
246
|
+
// RFC1918 private (Class B)
|
|
247
|
+
"192.168.0.0/16",
|
|
248
|
+
// RFC1918 private (Class C)
|
|
249
|
+
"127.0.0.0/8",
|
|
250
|
+
// loopback v4
|
|
251
|
+
"169.254.0.0/16",
|
|
252
|
+
// link-local v4 (cloud metadata endpoints)
|
|
253
|
+
"fe80::/10",
|
|
254
|
+
// link-local v6
|
|
255
|
+
"::1/128"
|
|
256
|
+
// loopback v6
|
|
257
|
+
];
|
|
258
|
+
var DEFAULT_DANGEROUS_SCHEMES = ["file:", "data:", "javascript:", "vbscript:"];
|
|
259
|
+
|
|
260
|
+
// src/roleValidator.ts
|
|
261
|
+
import { normalize, basename, dirname as dirname3, join as join3 } from "path";
|
|
262
|
+
import { lstatSync, readdirSync, realpathSync } from "fs";
|
|
263
|
+
import { homedir as homedir3 } from "os";
|
|
264
|
+
var SUSPICIOUS_BASENAME_RE = /^\.|(\.env|secret|credential|key|config|token)/i;
|
|
265
|
+
function resolveSymlinks(normalizedPath) {
|
|
266
|
+
if (!normalizedPath || normalizedPath.includes("://")) return normalizedPath;
|
|
267
|
+
if (normalizedPath.includes("node_modules/") || normalizedPath.includes("node_modules\\")) {
|
|
268
|
+
return normalizedPath;
|
|
269
|
+
}
|
|
270
|
+
const base = basename(normalizedPath);
|
|
271
|
+
const needsRealpath = SUSPICIOUS_BASENAME_RE.test(base);
|
|
272
|
+
if (!needsRealpath) {
|
|
273
|
+
try {
|
|
274
|
+
const stat = lstatSync(normalizedPath);
|
|
275
|
+
if (!stat.isSymbolicLink()) {
|
|
276
|
+
try {
|
|
277
|
+
const resolved = realpathSync(normalizedPath);
|
|
278
|
+
return resolved === normalizedPath ? normalizedPath : resolved;
|
|
279
|
+
} catch {
|
|
280
|
+
return normalizedPath;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
const code = err.code;
|
|
285
|
+
if (code === "ENOENT") {
|
|
286
|
+
return normalizedPath;
|
|
287
|
+
}
|
|
288
|
+
return normalizedPath;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
return realpathSync(normalizedPath);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
const code = err.code;
|
|
295
|
+
if (code === "ENOENT") {
|
|
296
|
+
return resolveNonexistentPath(normalizedPath);
|
|
297
|
+
}
|
|
298
|
+
return normalizedPath;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function resolveNonexistentPath(normalizedPath) {
|
|
302
|
+
let current = normalizedPath;
|
|
303
|
+
let suffix = "";
|
|
304
|
+
for (let i = 0; i < 50; i++) {
|
|
305
|
+
const parent = dirname3(current);
|
|
306
|
+
if (parent === current) {
|
|
307
|
+
return normalizedPath;
|
|
308
|
+
}
|
|
309
|
+
if (parent === ".") {
|
|
310
|
+
return normalizedPath;
|
|
311
|
+
}
|
|
312
|
+
suffix = suffix ? join3(basename(current), suffix) : basename(current);
|
|
313
|
+
current = parent;
|
|
314
|
+
try {
|
|
315
|
+
const resolved = realpathSync(current);
|
|
316
|
+
if (resolved !== current) {
|
|
317
|
+
return join3(resolved, suffix);
|
|
318
|
+
}
|
|
319
|
+
return normalizedPath;
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return normalizedPath;
|
|
324
|
+
}
|
|
325
|
+
function matchGlob(pattern, path3) {
|
|
326
|
+
let regex = "";
|
|
327
|
+
let i = 0;
|
|
328
|
+
while (i < pattern.length) {
|
|
329
|
+
if (pattern[i] === "*" && pattern[i + 1] === "*") {
|
|
330
|
+
regex += ".*";
|
|
331
|
+
i += 2;
|
|
332
|
+
if (pattern[i] === "/") i++;
|
|
333
|
+
} else if (pattern[i] === "*") {
|
|
334
|
+
regex += "[^/]*";
|
|
335
|
+
i++;
|
|
336
|
+
} else if (".+?^${}()|[]\\".includes(pattern[i])) {
|
|
337
|
+
regex += "\\" + pattern[i];
|
|
338
|
+
i++;
|
|
339
|
+
} else {
|
|
340
|
+
regex += pattern[i];
|
|
341
|
+
i++;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return new RegExp("^" + regex + "$").test(path3);
|
|
345
|
+
}
|
|
346
|
+
function normalizeForbiddenPattern(pattern) {
|
|
347
|
+
if (pattern.startsWith("**/") || pattern.startsWith("/")) return pattern;
|
|
348
|
+
return "**/" + pattern;
|
|
349
|
+
}
|
|
350
|
+
function isPathShaped(value) {
|
|
351
|
+
if (value.length === 0 || value.length > 4096) return false;
|
|
352
|
+
if (/\s/.test(value)) return false;
|
|
353
|
+
if (value.includes("/") || value.includes("\\")) return true;
|
|
354
|
+
if (value.startsWith(".") || value.startsWith("~")) return true;
|
|
355
|
+
if (/\.[A-Za-z0-9]{1,8}$/.test(value)) return true;
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
var PATH_TARGET_ACTIONS = /* @__PURE__ */ new Set([
|
|
359
|
+
"file_read",
|
|
360
|
+
"file_write",
|
|
361
|
+
"network_request"
|
|
362
|
+
]);
|
|
363
|
+
function anchorAllowedPattern(pattern, workspaceRoot) {
|
|
364
|
+
if (!workspaceRoot) return pattern;
|
|
365
|
+
if (pattern.startsWith("**") || pattern.startsWith("/")) return pattern;
|
|
366
|
+
const root = workspaceRoot.endsWith("/") ? workspaceRoot.slice(0, -1) : workspaceRoot;
|
|
367
|
+
return `${root}/${pattern}`;
|
|
368
|
+
}
|
|
369
|
+
function matchGlobInsensitive(pattern, path3) {
|
|
370
|
+
return matchGlob(pattern.toLowerCase(), path3.toLowerCase());
|
|
371
|
+
}
|
|
372
|
+
var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
373
|
+
function actionVerb(action) {
|
|
374
|
+
switch (action) {
|
|
375
|
+
case "api_call":
|
|
376
|
+
return "called API";
|
|
377
|
+
case "tool_invocation":
|
|
378
|
+
return "invoked tool";
|
|
379
|
+
case "file_read":
|
|
380
|
+
return "read";
|
|
381
|
+
case "file_write":
|
|
382
|
+
return "wrote to";
|
|
383
|
+
case "command_exec":
|
|
384
|
+
return "executed command";
|
|
385
|
+
default:
|
|
386
|
+
return "accessed";
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function formatPatterns(patterns, max = 3) {
|
|
390
|
+
if (patterns.length === 0) return "none defined";
|
|
391
|
+
if (patterns.length <= max) return patterns.join(", ");
|
|
392
|
+
return patterns.slice(0, max).join(", ") + ` +${patterns.length - max} more`;
|
|
393
|
+
}
|
|
394
|
+
function getTargetRecommendation(findingType, target, action) {
|
|
395
|
+
const t = target.toLowerCase();
|
|
396
|
+
if (findingType === "role_violation") {
|
|
397
|
+
if (action === "command_exec") {
|
|
398
|
+
if (t.includes("http") || t.startsWith("curl") || t.startsWith("wget")) {
|
|
399
|
+
return "Agent attempted unauthorized command execution targeting an external URL. Treat this as a potential data exfiltration attempt. Restrict the agent's action types and review prompt history for injection.";
|
|
400
|
+
}
|
|
401
|
+
return "Agent attempted unauthorized command execution. Review the specific command for data exfiltration or system modification intent. Restrict the agent's action types and review prompt history for injection.";
|
|
402
|
+
}
|
|
403
|
+
if (action === "database_query") {
|
|
404
|
+
return `Agent attempted unauthorized database query on '${target}'. Database access is not in this agent's allowed actions. If database access is required, add 'database_query' to allowedActions. If unexpected, investigate for data harvesting or SQL injection attempts.`;
|
|
405
|
+
}
|
|
406
|
+
return `Agent performed unauthorized action '${action}'. This action type is not in the agent's role definition. Investigate why it was attempted \u2014 check the agent's prompt chain for injection or misconfiguration. If legitimate, add '${action}' to allowedActions.`;
|
|
407
|
+
}
|
|
408
|
+
if (findingType === "unauthorized_target") {
|
|
409
|
+
if (t.includes(".env")) {
|
|
410
|
+
return "URGENT: Rotate all API keys, database credentials, and secrets stored in .env files. Audit which services consumed these credentials. Review agent prompt history for injection attempts that directed the agent toward environment files.";
|
|
411
|
+
}
|
|
412
|
+
if (t.includes(".ssh/authorized_keys") && action === "file_write") {
|
|
413
|
+
return "CRITICAL: Inspect ~/.ssh/authorized_keys for unauthorized entries added by the agent. This pattern indicates a backdoor installation attempt. Remove any unrecognized keys. Rotate all SSH credentials. Escalate to your security team immediately.";
|
|
414
|
+
}
|
|
415
|
+
if (t.includes(".ssh/")) {
|
|
416
|
+
return "URGENT: Rotate SSH keys immediately. Audit all systems that accepted this key for unauthorized access. Check ~/.ssh/known_hosts for unfamiliar entries. Review agent prompt history for injection.";
|
|
417
|
+
}
|
|
418
|
+
if (t.includes(".aws/")) {
|
|
419
|
+
return "URGENT: Rotate all AWS access keys and secret keys. Check CloudTrail logs for unauthorized API calls made with these credentials. Review IAM policies for unauthorized changes.";
|
|
420
|
+
}
|
|
421
|
+
if (t.includes("/etc/passwd") || t.includes("/etc/shadow")) {
|
|
422
|
+
return "Agent attempted to read system authentication files. This indicates reconnaissance for privilege escalation. Review whether this agent should have any access to system paths. Restrict the agent's filesystem scope.";
|
|
423
|
+
}
|
|
424
|
+
if (t.includes("/etc/")) {
|
|
425
|
+
return "Agent accessed system configuration files. Review whether this access was intentional and restrict the agent's filesystem scope to prevent system-level reconnaissance.";
|
|
426
|
+
}
|
|
427
|
+
if (t.includes("secrets/") || t.includes("credentials/")) {
|
|
428
|
+
return "URGENT: Audit all files in the accessed secrets directory. Rotate any credentials that may have been exposed. Review access logs for the secrets store.";
|
|
429
|
+
}
|
|
430
|
+
if (t.includes("debug") && t.includes(".log")) {
|
|
431
|
+
return action === "file_write" ? `Agent is writing to debug log '${target}'. Debug logs can contain sensitive runtime data including credentials, tokens, and internal state. If debug logging is expected, remove the matching forbidden pattern. If unexpected, inspect the log contents for exfiltrated data and investigate what triggered verbose logging.` : `Agent accessed debug log '${target}'. Debug logs may contain sensitive runtime information including API keys, tokens, and stack traces. Review the log contents for leaked credentials and restrict the agent's access to log files.`;
|
|
432
|
+
}
|
|
433
|
+
if (t.endsWith(".log") || t.includes("/log/") || t.includes("/logs/")) {
|
|
434
|
+
return `Agent ${actionVerb(action)} log file '${target}' which is explicitly forbidden. Log files can contain credentials, session tokens, and PII. If this agent needs log access, update forbiddenTargetPatterns. Otherwise, investigate what prompted the log access.`;
|
|
435
|
+
}
|
|
436
|
+
return "Immediately review agent activity. This target is explicitly forbidden. Check for prompt injection or misconfiguration.";
|
|
437
|
+
}
|
|
438
|
+
if (findingType === "scope_violation") {
|
|
439
|
+
if (action === "tool_invocation") {
|
|
440
|
+
if (t === "agent_stop" || t === "agent_shutdown" || t.includes("stop") || t.includes("shutdown") || t.includes("kill")) {
|
|
441
|
+
return `Agent invoked lifecycle tool '${target}'. This may indicate the agent is attempting to control its own execution or evade monitoring. If self-management is expected, add '${target}' to allowedTargetPatterns. If unexpected, review the agent's prompt history for self-termination or detection evasion instructions.`;
|
|
442
|
+
}
|
|
443
|
+
return `Agent invoked tool '${target}' outside its defined scope. If this tool is expected behavior, add '${target}' to allowedTargetPatterns. If unexpected, investigate what triggered the tool call and review the agent's prompt chain for injection.`;
|
|
444
|
+
}
|
|
445
|
+
if (action === "api_call") {
|
|
446
|
+
if (t.includes("anthropic") || t.includes("openai") || t.includes("claude") || t.includes("gpt") || t.includes("gemini")) {
|
|
447
|
+
return `Agent made an API call to AI service '${target}' outside its allowed scope. This could indicate legitimate AI integration not yet configured, or the agent attempting to invoke another model to bypass its restrictions. If expected, add '${target}' to allowedTargetPatterns. If unexpected, review what prompted this external AI call.`;
|
|
448
|
+
}
|
|
449
|
+
return `Agent made an API call to '${target}' outside its allowed scope. If this API is expected, add the target pattern to allowedTargetPatterns. If unexpected, investigate whether the agent was directed to make unauthorized external requests \u2014 this could indicate data exfiltration or prompt injection.`;
|
|
450
|
+
}
|
|
451
|
+
if (action === "file_write") {
|
|
452
|
+
return `Agent wrote to '${target}' which is outside its allowed scope. Inspect the written content for data exfiltration, unauthorized configuration changes, or persistence mechanisms. If this write target is legitimate, expand allowedTargetPatterns to include it.`;
|
|
453
|
+
}
|
|
454
|
+
if (action === "file_read") {
|
|
455
|
+
return `Agent read '${target}' outside its allowed scope. Review whether this file access is intentional. If legitimate, expand allowedTargetPatterns. If unexpected, investigate what prompted the out-of-scope read \u2014 the agent may be probing for sensitive data.`;
|
|
456
|
+
}
|
|
457
|
+
return `Agent accessed '${target}' outside its allowed scope. Review the agent's role definition and either expand allowedTargetPatterns if legitimate, or investigate what prompted the out-of-scope access.`;
|
|
458
|
+
}
|
|
459
|
+
return "Investigate the unauthorized access. Review agent role definition and consider tightening target patterns.";
|
|
460
|
+
}
|
|
461
|
+
function walkForbiddenInodeRoots(forbiddenPatterns, cwd) {
|
|
462
|
+
const inodes = /* @__PURE__ */ new Set();
|
|
463
|
+
const home = homedir3();
|
|
464
|
+
const deepPatterns = forbiddenPatterns.map(normalizeForbiddenPattern);
|
|
465
|
+
collectForbiddenInodes(home, deepPatterns, inodes, false);
|
|
466
|
+
const sensitiveDirs = [".ssh", ".aws", ".gnupg", ".config"];
|
|
467
|
+
for (const dir of sensitiveDirs) {
|
|
468
|
+
collectForbiddenInodes(join3(home, dir), deepPatterns, inodes, true);
|
|
469
|
+
}
|
|
470
|
+
collectForbiddenInodes(cwd, deepPatterns, inodes, true, true);
|
|
471
|
+
return inodes;
|
|
472
|
+
}
|
|
473
|
+
function collectForbiddenInodes(dirPath, forbiddenPatterns, inodes, recursive, skipNodeModules = false) {
|
|
474
|
+
let entries;
|
|
475
|
+
try {
|
|
476
|
+
entries = readdirSync(dirPath);
|
|
477
|
+
} catch {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
for (const entry of entries) {
|
|
481
|
+
if (skipNodeModules && entry === "node_modules") continue;
|
|
482
|
+
const fullPath = join3(dirPath, entry);
|
|
483
|
+
let stat;
|
|
484
|
+
try {
|
|
485
|
+
stat = lstatSync(fullPath);
|
|
486
|
+
} catch {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (stat.isFile()) {
|
|
490
|
+
for (const pattern of forbiddenPatterns) {
|
|
491
|
+
if (matchGlobInsensitive(pattern, fullPath)) {
|
|
492
|
+
inodes.add(BigInt(stat.ino));
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} else if (stat.isDirectory() && recursive) {
|
|
497
|
+
collectForbiddenInodes(fullPath, forbiddenPatterns, inodes, true, skipNodeModules);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function checkForbiddenInode(targetPath, forbiddenInodes) {
|
|
502
|
+
if (forbiddenInodes.size === 0) return false;
|
|
503
|
+
try {
|
|
504
|
+
const stat = lstatSync(targetPath);
|
|
505
|
+
if (stat.nlink <= 1) return false;
|
|
506
|
+
return forbiddenInodes.has(BigInt(stat.ino));
|
|
507
|
+
} catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function findMatchingException(exceptions, event, activeTask) {
|
|
512
|
+
if (!exceptions || exceptions.length === 0) return null;
|
|
513
|
+
for (const exception of exceptions) {
|
|
514
|
+
if (!matchGlob(exception.target, event.primaryTarget)) continue;
|
|
515
|
+
if (!exception.allowedActions.includes(event.action)) continue;
|
|
516
|
+
if (exception.requiresTask) {
|
|
517
|
+
if (!activeTask) continue;
|
|
518
|
+
if (!activeTask.description.toLowerCase().includes(exception.requiresTask.toLowerCase()))
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (exception.expiresAfter !== void 0) {
|
|
522
|
+
if (!activeTask) continue;
|
|
523
|
+
const elapsed = Date.now() - new Date(activeTask.startedAt).getTime();
|
|
524
|
+
if (elapsed > exception.expiresAfter) continue;
|
|
525
|
+
}
|
|
526
|
+
return exception;
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
var RoleValidator = class {
|
|
531
|
+
role;
|
|
532
|
+
sensitivityScorer;
|
|
533
|
+
approvalFn;
|
|
534
|
+
onAuditEntry;
|
|
535
|
+
forbiddenInodeCache;
|
|
536
|
+
workspaceRoot;
|
|
537
|
+
constructor(role, sensitivityScorer, options) {
|
|
538
|
+
this.role = {
|
|
539
|
+
...role,
|
|
540
|
+
forbiddenTargetPatterns: (role.forbiddenTargetPatterns ?? []).map(normalizeForbiddenPattern)
|
|
541
|
+
};
|
|
542
|
+
this.sensitivityScorer = sensitivityScorer ?? null;
|
|
543
|
+
this.approvalFn = options?.approvalFn ?? null;
|
|
544
|
+
this.onAuditEntry = options?.onAuditEntry ?? null;
|
|
545
|
+
this.forbiddenInodeCache = options?.forbiddenInodeCache ?? null;
|
|
546
|
+
this.workspaceRoot = options?.workspaceRoot ?? "";
|
|
547
|
+
}
|
|
548
|
+
validateEvent(event, activeTask) {
|
|
549
|
+
const eventTarget = event.primaryTarget;
|
|
550
|
+
const pathNormalized = isUrlShaped(eventTarget) ? eventTarget : normalize(eventTarget);
|
|
551
|
+
const resolvedTarget = resolveSymlinks(pathNormalized);
|
|
552
|
+
const normalizedPrimaryTarget = resolvedTarget;
|
|
553
|
+
const primaryTargetPaths = pathNormalized !== resolvedTarget ? [resolvedTarget, pathNormalized] : [resolvedTarget];
|
|
554
|
+
if (!this.role.allowedActions.includes(event.action)) {
|
|
555
|
+
return this.makeFinding(event, {
|
|
556
|
+
severity: "HIGH",
|
|
557
|
+
type: "role_violation",
|
|
558
|
+
description: `Agent performed action '${event.action}' which is not in its allowed actions list (allowed: ${this.role.allowedActions.join(", ")})`,
|
|
559
|
+
recommendation: getTargetRecommendation(
|
|
560
|
+
"role_violation",
|
|
561
|
+
normalizedPrimaryTarget,
|
|
562
|
+
event.action
|
|
563
|
+
)
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
if (this.forbiddenInodeCache) {
|
|
567
|
+
const forbiddenInodes = this.forbiddenInodeCache.getOrBuild();
|
|
568
|
+
if (forbiddenInodes.size > 0) {
|
|
569
|
+
const inodeTargets = [resolvedTarget];
|
|
570
|
+
for (let i = 1; i < event.targets.length; i++) {
|
|
571
|
+
const st = event.targets[i];
|
|
572
|
+
if (!isUrlShaped(st)) {
|
|
573
|
+
const stNorm = normalize(st);
|
|
574
|
+
inodeTargets.push(resolveSymlinks(stNorm));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
for (const target of inodeTargets) {
|
|
578
|
+
if (checkForbiddenInode(target, forbiddenInodes)) {
|
|
579
|
+
const finding = this.makeFinding(event, {
|
|
580
|
+
severity: "HIGH",
|
|
581
|
+
type: "unauthorized_target",
|
|
582
|
+
description: `Agent accessed '${target}' which is a hardlink to a forbidden file (inode match detected)`,
|
|
583
|
+
recommendation: "The target file shares an inode with a file matching a forbidden pattern. This is likely a hardlink bypass attempt. Investigate and remove the hardlink.",
|
|
584
|
+
matchedTarget: target
|
|
585
|
+
});
|
|
586
|
+
return this.enhanceWithSensitivity(finding, event);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
for (const pattern of this.role.forbiddenTargetPatterns) {
|
|
592
|
+
let matchedTargetValue = null;
|
|
593
|
+
if (primaryTargetPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
|
|
594
|
+
matchedTargetValue = normalizedPrimaryTarget;
|
|
595
|
+
}
|
|
596
|
+
if (!matchedTargetValue) {
|
|
597
|
+
const mcpTool = event.metadata?._mcpTool === "true";
|
|
598
|
+
for (let i = 1; i < event.targets.length; i++) {
|
|
599
|
+
const st = event.targets[i];
|
|
600
|
+
if (mcpTool && /\s/.test(st)) continue;
|
|
601
|
+
const stNorm = isUrlShaped(st) ? st : normalize(st);
|
|
602
|
+
const stResolved = resolveSymlinks(stNorm);
|
|
603
|
+
const stPaths = stNorm !== stResolved ? [stResolved, stNorm] : [stResolved];
|
|
604
|
+
if (stPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
|
|
605
|
+
matchedTargetValue = st;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (matchedTargetValue) {
|
|
611
|
+
const sensitivity = this.sensitivityScorer?.scoreTarget(matchedTargetValue, event.action);
|
|
612
|
+
const isCritical = sensitivity ? sensitivity.effectiveScore >= 0.9 : false;
|
|
613
|
+
const finding = this.makeFinding(event, {
|
|
614
|
+
severity: "HIGH",
|
|
615
|
+
type: "unauthorized_target",
|
|
616
|
+
description: `Agent accessed '${matchedTargetValue}' which matches forbidden pattern '${pattern}'`,
|
|
617
|
+
recommendation: getTargetRecommendation(
|
|
618
|
+
"unauthorized_target",
|
|
619
|
+
matchedTargetValue,
|
|
620
|
+
event.action
|
|
621
|
+
),
|
|
622
|
+
matchedTarget: matchedTargetValue
|
|
623
|
+
});
|
|
624
|
+
if (!isCritical) {
|
|
625
|
+
const exception = findMatchingException(this.role.exceptions, event, activeTask ?? null);
|
|
626
|
+
if (exception) {
|
|
627
|
+
this.onAuditEntry?.({
|
|
628
|
+
type: "exception_applied",
|
|
629
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
630
|
+
agentId: event.agentId,
|
|
631
|
+
target: matchedTargetValue,
|
|
632
|
+
action: event.action,
|
|
633
|
+
exceptionTarget: exception.target,
|
|
634
|
+
taskId: activeTask?.taskId ?? null
|
|
635
|
+
});
|
|
636
|
+
if (exception.requiresApproval) {
|
|
637
|
+
const ctx = {
|
|
638
|
+
finding,
|
|
639
|
+
exception,
|
|
640
|
+
activeTask: activeTask ?? null,
|
|
641
|
+
expiresAt: exception.expiresAfter != null && activeTask ? new Date(new Date(activeTask.startedAt).getTime() + exception.expiresAfter) : null
|
|
642
|
+
};
|
|
643
|
+
const approved = this.approvalFn?.(ctx) === true;
|
|
644
|
+
if (approved) {
|
|
645
|
+
this.onAuditEntry?.({
|
|
646
|
+
type: "exception_approved",
|
|
647
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
648
|
+
agentId: event.agentId,
|
|
649
|
+
target: matchedTargetValue,
|
|
650
|
+
action: event.action,
|
|
651
|
+
exceptionTarget: exception.target,
|
|
652
|
+
taskId: activeTask?.taskId ?? null
|
|
653
|
+
});
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return this.enhanceWithSensitivity(finding, event);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (this.sensitivityScorer && event.metadata?._mcpTool === "true") {
|
|
665
|
+
for (let i = 1; i < event.targets.length; i++) {
|
|
666
|
+
const val = event.targets[i];
|
|
667
|
+
if (!(isUrlShaped(val) || isPathShaped(val))) continue;
|
|
668
|
+
const sens = this.sensitivityScorer.scoreTarget(val, event.action);
|
|
669
|
+
if (sens.effectiveScore < 0.6) continue;
|
|
670
|
+
return this.makeFinding(event, {
|
|
671
|
+
severity: "MEDIUM",
|
|
672
|
+
type: "unauthorized_target",
|
|
673
|
+
kind: "actionable",
|
|
674
|
+
description: `MCP tool '${event.primaryTarget}' referenced sensitive argument '${val}' (sensitivity: ${sens.effectiveScore.toFixed(2)}, category: ${sens.category}). Not a forbidden path, but path-shaped and sensitive \u2014 escalated for review.`,
|
|
675
|
+
recommendation: getTargetRecommendation("unauthorized_target", val, event.action),
|
|
676
|
+
matchedTarget: val
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (this.role.allowedTargetPatterns.length > 0) {
|
|
681
|
+
const anchor = this.workspaceRoot !== "" && PATH_TARGET_ACTIONS.has(event.action);
|
|
682
|
+
const matched = this.role.allowedTargetPatterns.some((pattern) => {
|
|
683
|
+
const candidates = anchor ? [pattern, anchorAllowedPattern(pattern, this.workspaceRoot)] : [pattern];
|
|
684
|
+
return candidates.some((pat) => primaryTargetPaths.some((tp) => matchGlob(pat, tp)));
|
|
685
|
+
});
|
|
686
|
+
if (!matched) {
|
|
687
|
+
if (event.action === "network_request" && isUrlShaped(normalizedPrimaryTarget) && this.role.networkHosts && this.role.networkHosts.length > 0) {
|
|
688
|
+
const sensitivity = this.sensitivityScorer?.scoreTarget(
|
|
689
|
+
normalizedPrimaryTarget,
|
|
690
|
+
event.action
|
|
691
|
+
);
|
|
692
|
+
const isCritical = sensitivity ? sensitivity.effectiveScore >= 0.9 : false;
|
|
693
|
+
if (!isCritical) {
|
|
694
|
+
try {
|
|
695
|
+
const parsed = new URL(normalizedPrimaryTarget);
|
|
696
|
+
const host = parsed.hostname.toLowerCase().replace(/\.+$/, "");
|
|
697
|
+
if (this.role.networkHosts.includes(host)) {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
} catch {
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const finding = this.makeFinding(event, {
|
|
705
|
+
severity: "MEDIUM",
|
|
706
|
+
type: "scope_violation",
|
|
707
|
+
description: `Agent ${actionVerb(event.action)} '${normalizedPrimaryTarget}' which is outside its allowed target patterns (allowed: ${formatPatterns(this.role.allowedTargetPatterns)})`,
|
|
708
|
+
recommendation: getTargetRecommendation(
|
|
709
|
+
"scope_violation",
|
|
710
|
+
normalizedPrimaryTarget,
|
|
711
|
+
event.action
|
|
712
|
+
)
|
|
713
|
+
});
|
|
714
|
+
return this.enhanceWithSensitivity(finding, event);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const scopePatterns = activeTask?.scopePatterns;
|
|
718
|
+
if (scopePatterns && scopePatterns.length > 0) {
|
|
719
|
+
const anchor = this.workspaceRoot !== "" && PATH_TARGET_ACTIONS.has(event.action);
|
|
720
|
+
const inScope = scopePatterns.some((pattern) => {
|
|
721
|
+
const candidates = anchor ? [pattern, anchorAllowedPattern(pattern, this.workspaceRoot)] : [pattern];
|
|
722
|
+
return candidates.some((pat) => primaryTargetPaths.some((tp) => matchGlob(pat, tp)));
|
|
723
|
+
});
|
|
724
|
+
if (!inScope) {
|
|
725
|
+
const finding = this.makeFinding(event, {
|
|
726
|
+
severity: "HIGH",
|
|
727
|
+
type: "role_violation",
|
|
728
|
+
description: `Agent ${actionVerb(event.action)} '${normalizedPrimaryTarget}' which is outside the session-declared SCOPE (scope: ${formatPatterns(scopePatterns)})`,
|
|
729
|
+
recommendation: `This session narrowed its allowed targets via a SCOPE: declaration (${formatPatterns(scopePatterns)}); '${normalizedPrimaryTarget}' is outside it. If this target is legitimately in scope, broaden or drop the SCOPE: line. If unexpected, the agent is acting outside the per-session boundary the operator set \u2014 review the prompt chain for drift or injection.`
|
|
730
|
+
});
|
|
731
|
+
return this.enhanceWithSensitivity(finding, event);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (this.role.expectedSchedule) {
|
|
735
|
+
const eventDate = new Date(event.timestamp);
|
|
736
|
+
const schedule = this.role.expectedSchedule;
|
|
737
|
+
if (schedule.activeDays) {
|
|
738
|
+
const dayName = DAY_NAMES[eventDate.getUTCDay()];
|
|
739
|
+
const activeDaysLower = schedule.activeDays.map((d) => d.toLowerCase());
|
|
740
|
+
if (!activeDaysLower.includes(dayName)) {
|
|
741
|
+
const scheduleDesc = `days: ${schedule.activeDays.join(", ")}`;
|
|
742
|
+
return this.makeFinding(event, {
|
|
743
|
+
severity: "MEDIUM",
|
|
744
|
+
type: "temporal_anomaly",
|
|
745
|
+
kind: "informational",
|
|
746
|
+
description: `Agent was active at ${eventDate.toISOString()} on ${dayName}, outside its expected schedule of ${scheduleDesc}`,
|
|
747
|
+
recommendation: "Review whether this activity was triggered by a legitimate automated process or represents unauthorized usage."
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (schedule.activeHours) {
|
|
752
|
+
const hour = eventDate.getUTCHours();
|
|
753
|
+
const [startHour, endHour] = schedule.activeHours;
|
|
754
|
+
const outsideHours = startHour <= endHour ? hour < startHour || hour > endHour : hour < startHour && hour > endHour;
|
|
755
|
+
if (outsideHours) {
|
|
756
|
+
const dayName = DAY_NAMES[eventDate.getUTCDay()];
|
|
757
|
+
const scheduleDesc = `hours: ${startHour}-${endHour}`;
|
|
758
|
+
return this.makeFinding(event, {
|
|
759
|
+
severity: "MEDIUM",
|
|
760
|
+
type: "temporal_anomaly",
|
|
761
|
+
kind: "informational",
|
|
762
|
+
description: `Agent was active at ${eventDate.toISOString()} on ${dayName}, outside its expected schedule of ${scheduleDesc}`,
|
|
763
|
+
recommendation: "Review whether this activity was triggered by a legitimate automated process or represents unauthorized usage."
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
enhanceWithSensitivity(finding, event) {
|
|
771
|
+
if (!this.sensitivityScorer) return finding;
|
|
772
|
+
const result = this.sensitivityScorer.scoreTarget(event.primaryTarget, event.action);
|
|
773
|
+
finding.description += ` (sensitivity: ${result.effectiveScore.toFixed(2)}, category: ${result.category})`;
|
|
774
|
+
if (result.effectiveScore >= 0.9) {
|
|
775
|
+
finding.severity = "CRITICAL";
|
|
776
|
+
}
|
|
777
|
+
if (result.effectiveScore >= 0.8) {
|
|
778
|
+
finding.kind = "actionable";
|
|
779
|
+
}
|
|
780
|
+
return finding;
|
|
781
|
+
}
|
|
782
|
+
makeFinding(event, details) {
|
|
783
|
+
return {
|
|
784
|
+
severity: details.severity,
|
|
785
|
+
kind: details.kind ?? "actionable",
|
|
786
|
+
type: details.type,
|
|
787
|
+
agentId: event.agentId,
|
|
788
|
+
agentName: event.agentName,
|
|
789
|
+
description: details.description,
|
|
790
|
+
evidence: {
|
|
791
|
+
action: event.action,
|
|
792
|
+
target: details.matchedTarget ?? event.primaryTarget,
|
|
793
|
+
timestamp: event.timestamp
|
|
794
|
+
},
|
|
795
|
+
recommendation: details.recommendation,
|
|
796
|
+
timestamp: event.timestamp
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// src/networkPolicy.ts
|
|
802
|
+
function parseIpv4(s) {
|
|
803
|
+
const parts = s.split(".");
|
|
804
|
+
if (parts.length !== 4) return null;
|
|
805
|
+
const bytes = new Uint8Array(4);
|
|
806
|
+
for (let i = 0; i < 4; i++) {
|
|
807
|
+
const n = Number(parts[i]);
|
|
808
|
+
if (!Number.isInteger(n) || n < 0 || n > 255 || parts[i] !== String(n)) return null;
|
|
809
|
+
bytes[i] = n;
|
|
810
|
+
}
|
|
811
|
+
return bytes;
|
|
812
|
+
}
|
|
813
|
+
function parseIpv6(s) {
|
|
814
|
+
const halves = s.split("::");
|
|
815
|
+
if (halves.length > 2) return null;
|
|
816
|
+
const left = halves[0] ? halves[0].split(":") : [];
|
|
817
|
+
const right = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
|
|
818
|
+
if (halves.length === 1 && left.length !== 8) return null;
|
|
819
|
+
if (left.length + right.length > 8) return null;
|
|
820
|
+
const groups = [];
|
|
821
|
+
for (const g of left) {
|
|
822
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null;
|
|
823
|
+
groups.push(parseInt(g, 16));
|
|
824
|
+
}
|
|
825
|
+
if (halves.length === 2) {
|
|
826
|
+
const fill = 8 - left.length - right.length;
|
|
827
|
+
for (let i = 0; i < fill; i++) groups.push(0);
|
|
828
|
+
}
|
|
829
|
+
for (const g of right) {
|
|
830
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null;
|
|
831
|
+
groups.push(parseInt(g, 16));
|
|
832
|
+
}
|
|
833
|
+
if (groups.length !== 8) return null;
|
|
834
|
+
const bytes = new Uint8Array(16);
|
|
835
|
+
for (let i = 0; i < 8; i++) {
|
|
836
|
+
bytes[i * 2] = groups[i] >> 8 & 255;
|
|
837
|
+
bytes[i * 2 + 1] = groups[i] & 255;
|
|
838
|
+
}
|
|
839
|
+
return bytes;
|
|
840
|
+
}
|
|
841
|
+
function parseIpLiteral(host) {
|
|
842
|
+
let h = host;
|
|
843
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
844
|
+
const v4 = parseIpv4(h);
|
|
845
|
+
if (v4) return { family: "v4", bytes: v4 };
|
|
846
|
+
const v6 = parseIpv6(h);
|
|
847
|
+
if (v6) return { family: "v6", bytes: v6 };
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
function matchesCidr(ipBytes, cidr) {
|
|
851
|
+
const slash = cidr.lastIndexOf("/");
|
|
852
|
+
const addr = cidr.slice(0, slash);
|
|
853
|
+
const prefix = Number(cidr.slice(slash + 1));
|
|
854
|
+
const parsed = parseIpLiteral(addr);
|
|
855
|
+
if (!parsed || parsed.bytes.length !== ipBytes.length) return false;
|
|
856
|
+
const fullBytes = Math.floor(prefix / 8);
|
|
857
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
858
|
+
if (ipBytes[i] !== parsed.bytes[i]) return false;
|
|
859
|
+
}
|
|
860
|
+
const rem = prefix % 8;
|
|
861
|
+
if (rem > 0) {
|
|
862
|
+
const mask = 255 << 8 - rem;
|
|
863
|
+
if ((ipBytes[fullBytes] & mask) !== (parsed.bytes[fullBytes] & mask)) return false;
|
|
864
|
+
}
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
function isHostInDenylist(host, cidrs) {
|
|
868
|
+
const parsed = parseIpLiteral(host);
|
|
869
|
+
if (!parsed) return false;
|
|
870
|
+
return cidrs.some((cidr) => matchesCidr(parsed.bytes, cidr));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/targetSensitivity.ts
|
|
874
|
+
var ACTION_MULTIPLIERS = {
|
|
875
|
+
file_read: 1,
|
|
876
|
+
file_write: 1.3,
|
|
877
|
+
command_exec: 1.5,
|
|
878
|
+
database_query: 1.2,
|
|
879
|
+
api_call: 1.1,
|
|
880
|
+
file_delete: 1.4
|
|
881
|
+
};
|
|
882
|
+
var BUILT_IN_RULES = [
|
|
883
|
+
// Critical (0.9-1.0)
|
|
884
|
+
{
|
|
885
|
+
pattern: "**/.ssh/**",
|
|
886
|
+
sensitivity: 1,
|
|
887
|
+
category: "credentials",
|
|
888
|
+
description: "SSH keys and config"
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
pattern: "**/.env",
|
|
892
|
+
sensitivity: 1,
|
|
893
|
+
category: "credentials",
|
|
894
|
+
description: "Environment variables with secrets"
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
pattern: "**/.env.*",
|
|
898
|
+
sensitivity: 1,
|
|
899
|
+
category: "credentials",
|
|
900
|
+
description: "Environment variables with secrets (dotenv variants)"
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
pattern: "**/secrets/**",
|
|
904
|
+
sensitivity: 1,
|
|
905
|
+
category: "credentials",
|
|
906
|
+
description: "Secrets directory"
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
pattern: "**/.aws/**",
|
|
910
|
+
sensitivity: 1,
|
|
911
|
+
category: "credentials",
|
|
912
|
+
description: "AWS credentials"
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
pattern: "**/.kube/**",
|
|
916
|
+
sensitivity: 0.95,
|
|
917
|
+
category: "credentials",
|
|
918
|
+
description: "Kubernetes config"
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
pattern: "**/id_rsa*",
|
|
922
|
+
sensitivity: 1,
|
|
923
|
+
category: "credentials",
|
|
924
|
+
description: "Private SSH keys"
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
pattern: "**/*.pem",
|
|
928
|
+
sensitivity: 0.95,
|
|
929
|
+
category: "credentials",
|
|
930
|
+
description: "Certificate private keys"
|
|
931
|
+
},
|
|
932
|
+
{ pattern: "**/shadow", sensitivity: 1, category: "system", description: "Unix password file" },
|
|
933
|
+
{ pattern: "**/passwd", sensitivity: 0.9, category: "system", description: "Unix user database" },
|
|
934
|
+
// High (0.7-0.89)
|
|
935
|
+
{
|
|
936
|
+
pattern: "**/database/**",
|
|
937
|
+
sensitivity: 0.8,
|
|
938
|
+
category: "database",
|
|
939
|
+
description: "Database directory"
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
pattern: "**/*.sqlite",
|
|
943
|
+
sensitivity: 0.75,
|
|
944
|
+
category: "database",
|
|
945
|
+
description: "SQLite database file"
|
|
946
|
+
},
|
|
947
|
+
{ pattern: "**/*.db", sensitivity: 0.75, category: "database", description: "Database file" },
|
|
948
|
+
{ pattern: "**/users/**", sensitivity: 0.8, category: "pii", description: "User data directory" },
|
|
949
|
+
{ pattern: "**/customers/**", sensitivity: 0.8, category: "pii", description: "Customer data" },
|
|
950
|
+
{
|
|
951
|
+
pattern: "**/payments/**",
|
|
952
|
+
sensitivity: 0.85,
|
|
953
|
+
category: "pii",
|
|
954
|
+
description: "Payment information"
|
|
955
|
+
},
|
|
956
|
+
{ pattern: "/etc/**", sensitivity: 0.7, category: "system", description: "System configuration" },
|
|
957
|
+
{
|
|
958
|
+
pattern: "**/config/production*",
|
|
959
|
+
sensitivity: 0.75,
|
|
960
|
+
category: "config",
|
|
961
|
+
description: "Production configuration"
|
|
962
|
+
},
|
|
963
|
+
// Medium (0.4-0.69)
|
|
964
|
+
{
|
|
965
|
+
pattern: "**/config/**",
|
|
966
|
+
sensitivity: 0.5,
|
|
967
|
+
category: "config",
|
|
968
|
+
description: "Configuration files"
|
|
969
|
+
},
|
|
970
|
+
{ pattern: "**/logs/**", sensitivity: 0.4, category: "general", description: "Log directory" },
|
|
971
|
+
{ pattern: "**/.git/**", sensitivity: 0.45, category: "source", description: "Git internals" },
|
|
972
|
+
// Low (0.0-0.39)
|
|
973
|
+
{ pattern: "**/src/**", sensitivity: 0.2, category: "source", description: "Source code" },
|
|
974
|
+
{ pattern: "**/tests/**", sensitivity: 0.1, category: "source", description: "Test files" },
|
|
975
|
+
{ pattern: "**/docs/**", sensitivity: 0.1, category: "general", description: "Documentation" },
|
|
976
|
+
{
|
|
977
|
+
pattern: "**/node_modules/**",
|
|
978
|
+
sensitivity: 0.05,
|
|
979
|
+
category: "general",
|
|
980
|
+
description: "Dependencies"
|
|
981
|
+
}
|
|
982
|
+
];
|
|
983
|
+
var DEFAULT_RESULT = {
|
|
984
|
+
sensitivity: 0.15,
|
|
985
|
+
category: "general",
|
|
986
|
+
description: "No specific sensitivity rule matched",
|
|
987
|
+
matchedRule: ""
|
|
988
|
+
};
|
|
989
|
+
var URL_SCHEME_PREFIXES = [
|
|
990
|
+
"http://",
|
|
991
|
+
"https://",
|
|
992
|
+
"ws://",
|
|
993
|
+
"wss://",
|
|
994
|
+
"file://",
|
|
995
|
+
"data:",
|
|
996
|
+
"javascript:",
|
|
997
|
+
"vbscript:"
|
|
998
|
+
];
|
|
999
|
+
function isUrlShaped(target) {
|
|
1000
|
+
const lower = target.toLowerCase();
|
|
1001
|
+
return URL_SCHEME_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
|
1002
|
+
}
|
|
1003
|
+
function safeDecodeURIComponent(component) {
|
|
1004
|
+
try {
|
|
1005
|
+
return decodeURIComponent(component);
|
|
1006
|
+
} catch {
|
|
1007
|
+
return component;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
var TargetSensitivityScorer = class {
|
|
1011
|
+
rules;
|
|
1012
|
+
repoMap;
|
|
1013
|
+
repoRoot;
|
|
1014
|
+
overlay;
|
|
1015
|
+
constructor(customRules, repoMap, repoRoot, overlay) {
|
|
1016
|
+
if (customRules && customRules.length > 0) {
|
|
1017
|
+
const overriddenPatterns = new Set(customRules.map((r) => r.pattern));
|
|
1018
|
+
const kept = BUILT_IN_RULES.filter((r) => !overriddenPatterns.has(r.pattern));
|
|
1019
|
+
this.rules = [...kept, ...customRules];
|
|
1020
|
+
} else {
|
|
1021
|
+
this.rules = [...BUILT_IN_RULES];
|
|
1022
|
+
}
|
|
1023
|
+
this.rules.sort((a, b) => b.sensitivity - a.sensitivity);
|
|
1024
|
+
this.repoMap = repoMap ?? null;
|
|
1025
|
+
this.repoRoot = repoRoot ?? repoMap?.repoRoot ?? "";
|
|
1026
|
+
this.overlay = overlay ?? null;
|
|
1027
|
+
}
|
|
1028
|
+
scoreTarget(target, action) {
|
|
1029
|
+
const multiplier = ACTION_MULTIPLIERS[action] ?? 1;
|
|
1030
|
+
if (action === "network_request" && isUrlShaped(target)) {
|
|
1031
|
+
return this._scoreUrlTarget(target, multiplier);
|
|
1032
|
+
}
|
|
1033
|
+
if (this.overlay) {
|
|
1034
|
+
const decision = lookupOverlayDecision(this.overlay, target, this.repoRoot);
|
|
1035
|
+
if (decision) {
|
|
1036
|
+
if (decision.decision === "reject") {
|
|
1037
|
+
return this._scoreByPatterns(target, multiplier);
|
|
1038
|
+
}
|
|
1039
|
+
if (decision.decision === "override") {
|
|
1040
|
+
const sens = decision.sensitivity ?? 0.5;
|
|
1041
|
+
const cat = decision.category ?? "general";
|
|
1042
|
+
return {
|
|
1043
|
+
sensitivity: sens,
|
|
1044
|
+
category: cat,
|
|
1045
|
+
description: `Overlay override: ${decision.path}`,
|
|
1046
|
+
matchedRule: `overlay:${decision.path}`,
|
|
1047
|
+
actionMultiplier: multiplier,
|
|
1048
|
+
effectiveScore: Math.min(sens * multiplier, 1),
|
|
1049
|
+
groundedInRepoMap: true
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (this.repoMap) {
|
|
1055
|
+
const entry = lookupEntry(this.repoMap, target, this.repoRoot);
|
|
1056
|
+
if (entry) {
|
|
1057
|
+
return {
|
|
1058
|
+
sensitivity: entry.sensitivity,
|
|
1059
|
+
category: entry.category,
|
|
1060
|
+
description: `Repo map entry: ${entry.path}`,
|
|
1061
|
+
matchedRule: `repo-map:${entry.path}`,
|
|
1062
|
+
actionMultiplier: multiplier,
|
|
1063
|
+
effectiveScore: Math.min(entry.sensitivity * multiplier, 1),
|
|
1064
|
+
groundedInRepoMap: true
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return this._scoreByPatterns(target, multiplier);
|
|
1069
|
+
}
|
|
1070
|
+
/** Pattern-only scoring (Tier 2). Used directly by reject decisions. */
|
|
1071
|
+
_scoreByPatterns(target, multiplier) {
|
|
1072
|
+
for (const rule of this.rules) {
|
|
1073
|
+
if (matchGlob(rule.pattern, target)) {
|
|
1074
|
+
return {
|
|
1075
|
+
sensitivity: rule.sensitivity,
|
|
1076
|
+
category: rule.category,
|
|
1077
|
+
description: rule.description,
|
|
1078
|
+
matchedRule: rule.pattern,
|
|
1079
|
+
actionMultiplier: multiplier,
|
|
1080
|
+
effectiveScore: Math.min(rule.sensitivity * multiplier, 1),
|
|
1081
|
+
groundedInRepoMap: false
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
...DEFAULT_RESULT,
|
|
1087
|
+
actionMultiplier: multiplier,
|
|
1088
|
+
effectiveScore: Math.min(DEFAULT_RESULT.sensitivity * multiplier, 1),
|
|
1089
|
+
groundedInRepoMap: false
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* URL-aware component scoring (Sprint 6b W1 fix).
|
|
1094
|
+
* Parses the URL, extracts path/query-values/fragment, decodes each once,
|
|
1095
|
+
* scores each against forbidden-pattern rules, takes MAX.
|
|
1096
|
+
*/
|
|
1097
|
+
_scoreUrlTarget(target, multiplier) {
|
|
1098
|
+
const lowerTarget = target.toLowerCase();
|
|
1099
|
+
for (const scheme of DEFAULT_DANGEROUS_SCHEMES) {
|
|
1100
|
+
if (lowerTarget.startsWith(scheme)) {
|
|
1101
|
+
return {
|
|
1102
|
+
sensitivity: 0.95,
|
|
1103
|
+
category: "system",
|
|
1104
|
+
description: `Dangerous URL scheme '${scheme}' denied by default`,
|
|
1105
|
+
matchedRule: "scheme-denylist",
|
|
1106
|
+
actionMultiplier: multiplier,
|
|
1107
|
+
effectiveScore: Math.min(0.95 * multiplier, 1),
|
|
1108
|
+
groundedInRepoMap: false
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
let parsed;
|
|
1113
|
+
try {
|
|
1114
|
+
parsed = new URL(target);
|
|
1115
|
+
} catch {
|
|
1116
|
+
return {
|
|
1117
|
+
sensitivity: 0.3,
|
|
1118
|
+
category: "malformed_url",
|
|
1119
|
+
description: "URL could not be parsed (malformed)",
|
|
1120
|
+
matchedRule: "",
|
|
1121
|
+
actionMultiplier: multiplier,
|
|
1122
|
+
effectiveScore: Math.min(0.3 * multiplier, 1),
|
|
1123
|
+
groundedInRepoMap: false
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
if (isHostInDenylist(parsed.hostname, DEFAULT_NETWORK_DENYLIST_CIDRS)) {
|
|
1127
|
+
return {
|
|
1128
|
+
sensitivity: 0.95,
|
|
1129
|
+
category: "system",
|
|
1130
|
+
description: `Host '${parsed.hostname}' is in default network denylist (RFC1918/link-local/loopback)`,
|
|
1131
|
+
matchedRule: "cidr-denylist",
|
|
1132
|
+
actionMultiplier: multiplier,
|
|
1133
|
+
effectiveScore: Math.min(0.95 * multiplier, 1),
|
|
1134
|
+
groundedInRepoMap: false
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
const components = [];
|
|
1138
|
+
components.push(safeDecodeURIComponent(parsed.pathname));
|
|
1139
|
+
for (const value of parsed.searchParams.values()) {
|
|
1140
|
+
components.push(safeDecodeURIComponent(value));
|
|
1141
|
+
}
|
|
1142
|
+
const fragment = parsed.hash.replace(/^#/, "");
|
|
1143
|
+
if (fragment) {
|
|
1144
|
+
components.push(safeDecodeURIComponent(fragment));
|
|
1145
|
+
}
|
|
1146
|
+
let best = {
|
|
1147
|
+
...DEFAULT_RESULT,
|
|
1148
|
+
actionMultiplier: multiplier,
|
|
1149
|
+
effectiveScore: Math.min(DEFAULT_RESULT.sensitivity * multiplier, 1),
|
|
1150
|
+
groundedInRepoMap: false
|
|
1151
|
+
};
|
|
1152
|
+
for (const component of components) {
|
|
1153
|
+
const result = this._scoreByPatterns(component, multiplier);
|
|
1154
|
+
if (result.effectiveScore > best.effectiveScore) {
|
|
1155
|
+
best = result;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return best;
|
|
1159
|
+
}
|
|
1160
|
+
scoreEvent(event) {
|
|
1161
|
+
let best = this.scoreTarget(event.primaryTarget, event.action);
|
|
1162
|
+
for (let i = 1; i < event.targets.length; i++) {
|
|
1163
|
+
const result = this.scoreTarget(event.targets[i], event.action);
|
|
1164
|
+
if (result.effectiveScore > best.effectiveScore) {
|
|
1165
|
+
best = result;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return best;
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Replaces the active repo sensitivity map. Pass null to clear.
|
|
1172
|
+
* If newRepoRoot is provided, it overrides the map's repoRoot.
|
|
1173
|
+
*/
|
|
1174
|
+
updateRepoMap(newMap, newRepoRoot) {
|
|
1175
|
+
this.repoMap = newMap;
|
|
1176
|
+
if (newRepoRoot !== void 0) {
|
|
1177
|
+
this.repoRoot = newRepoRoot;
|
|
1178
|
+
} else if (newMap) {
|
|
1179
|
+
this.repoRoot = newMap.repoRoot;
|
|
1180
|
+
} else {
|
|
1181
|
+
this.repoRoot = "";
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Replaces the active sensitivity overlay. Pass null to clear.
|
|
1186
|
+
*/
|
|
1187
|
+
updateOverlay(newOverlay) {
|
|
1188
|
+
this.overlay = newOverlay;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Returns all pattern-based rules that match the target. Does NOT
|
|
1192
|
+
* consult the repo map. Used by RepoSensitivityScanner during
|
|
1193
|
+
* scan-time enumeration.
|
|
1194
|
+
*/
|
|
1195
|
+
getAllMatchingRules(target) {
|
|
1196
|
+
return this.rules.filter((rule) => matchGlob(rule.pattern, target));
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
export {
|
|
1201
|
+
DEFAULT_MAP_PATH,
|
|
1202
|
+
saveMap,
|
|
1203
|
+
loadMap,
|
|
1204
|
+
DEFAULT_OVERLAY_PATH,
|
|
1205
|
+
saveOverlay,
|
|
1206
|
+
loadOverlay,
|
|
1207
|
+
applyOverlay,
|
|
1208
|
+
removeOverlayDecision,
|
|
1209
|
+
createEmptyOverlay,
|
|
1210
|
+
DEFAULT_FORBIDDEN_PATTERNS,
|
|
1211
|
+
DEFAULT_MEDIUM_DISPOSITION,
|
|
1212
|
+
TargetSensitivityScorer,
|
|
1213
|
+
matchGlob,
|
|
1214
|
+
normalizeForbiddenPattern,
|
|
1215
|
+
matchGlobInsensitive,
|
|
1216
|
+
getTargetRecommendation,
|
|
1217
|
+
walkForbiddenInodeRoots,
|
|
1218
|
+
findMatchingException,
|
|
1219
|
+
RoleValidator
|
|
1220
|
+
};
|
|
1221
|
+
//# sourceMappingURL=chunk-6MHWJATS.js.map
|