@tuent/sentinel 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -26
- package/SECURITY_MODEL.md +231 -0
- package/dist/Sentinel-QHMQ67W3.js +10 -0
- package/dist/chunk-B5QKJHSV.js +32 -0
- package/dist/{chunk-Z3PWIJKT.js → chunk-IYC5E7RL.js} +99 -422
- package/dist/{chunk-CUJKNIKT.js → chunk-LATQNIRW.js} +33 -1
- package/dist/{chunk-QFRDEISP.js → chunk-NS6ZLMDK.js} +6 -6
- package/dist/{chunk-6MHWJATS.js → chunk-QHE56MEO.js} +510 -18
- package/dist/{chunk-3U3PKD4N.js → chunk-WPTJBRX5.js} +2 -2
- package/dist/cli.js +30 -30
- package/dist/gateway/index.d.ts +14 -0
- package/dist/gateway/index.js +3 -2
- package/dist/gatewayDaemon.js +3 -2
- package/dist/index.js +4 -4
- package/dist/pidManager-DOGVN6ZT.js +23 -0
- package/package.json +3 -2
- package/dist/Sentinel-JLQL3YRD.js +0 -10
- package/dist/pidManager-ZYC7SICM.js +0 -15
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
saveMap,
|
|
18
18
|
saveOverlay,
|
|
19
19
|
walkForbiddenInodeRoots
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-QHE56MEO.js";
|
|
21
21
|
import {
|
|
22
22
|
loadPolicy,
|
|
23
23
|
policyToConfig,
|
|
@@ -7318,13 +7318,13 @@ var Sentinel = class _Sentinel {
|
|
|
7318
7318
|
type: "agent_quarantined",
|
|
7319
7319
|
agentId,
|
|
7320
7320
|
agentName: event.agentName ?? agentId,
|
|
7321
|
-
description: `Agent ${agentId} is quarantined. All actions blocked.`,
|
|
7321
|
+
description: `Agent ${agentId} is quarantined. All actions blocked. Run 'sentinel release' to restore access.`,
|
|
7322
7322
|
evidence: {
|
|
7323
7323
|
action: event.action,
|
|
7324
7324
|
target: event.primaryTarget,
|
|
7325
7325
|
timestamp: event.timestamp
|
|
7326
7326
|
},
|
|
7327
|
-
recommendation:
|
|
7327
|
+
recommendation: `Agent must be manually released before it can perform any actions. Run 'sentinel release' in this workspace (or 'sentinel release --agent=${agentId}') to restore access.`,
|
|
7328
7328
|
timestamp: event.timestamp
|
|
7329
7329
|
}
|
|
7330
7330
|
};
|
|
@@ -7339,13 +7339,13 @@ var Sentinel = class _Sentinel {
|
|
|
7339
7339
|
type: "agent_restricted",
|
|
7340
7340
|
agentId,
|
|
7341
7341
|
agentName: event.agentName ?? agentId,
|
|
7342
|
-
description: `Agent ${agentId} is restricted. Action '${event.action}' is not permitted in restricted mode.`,
|
|
7342
|
+
description: `Agent ${agentId} is restricted. Action '${event.action}' is not permitted in restricted mode. Run 'sentinel release' to restore full access.`,
|
|
7343
7343
|
evidence: {
|
|
7344
7344
|
action: event.action,
|
|
7345
7345
|
target: event.primaryTarget,
|
|
7346
7346
|
timestamp: event.timestamp
|
|
7347
7347
|
},
|
|
7348
|
-
recommendation:
|
|
7348
|
+
recommendation: `Only file_read and tool_invocation are allowed in restricted mode. Run 'sentinel release' in this workspace (or 'sentinel release --agent=${agentId}') to restore full access.`,
|
|
7349
7349
|
timestamp: event.timestamp
|
|
7350
7350
|
}
|
|
7351
7351
|
};
|
|
@@ -7426,4 +7426,4 @@ export {
|
|
|
7426
7426
|
createCliApproval,
|
|
7427
7427
|
Sentinel
|
|
7428
7428
|
};
|
|
7429
|
-
//# sourceMappingURL=chunk-
|
|
7429
|
+
//# sourceMappingURL=chunk-NS6ZLMDK.js.map
|
|
@@ -258,23 +258,508 @@ var DEFAULT_NETWORK_DENYLIST_CIDRS = [
|
|
|
258
258
|
var DEFAULT_DANGEROUS_SCHEMES = ["file:", "data:", "javascript:", "vbscript:"];
|
|
259
259
|
|
|
260
260
|
// src/roleValidator.ts
|
|
261
|
-
import { normalize, basename, dirname as
|
|
262
|
-
import { lstatSync, readdirSync, realpathSync } from "fs";
|
|
261
|
+
import { normalize as normalize2, basename as basename2, dirname as dirname4, join as join4 } from "path";
|
|
262
|
+
import { lstatSync, readdirSync, realpathSync as realpathSync2 } from "fs";
|
|
263
263
|
import { homedir as homedir3 } from "os";
|
|
264
|
+
|
|
265
|
+
// src/gateway/bashScanner.ts
|
|
266
|
+
import { parse as shellParse } from "shell-quote";
|
|
267
|
+
import { realpathSync } from "fs";
|
|
268
|
+
import { dirname as dirname3, join as join3, basename, normalize } from "path";
|
|
269
|
+
var BRACE_PATTERN_RE = /\{[^}]*,[^}]*\}/;
|
|
270
|
+
var MAX_BRACE_EXPANSION = 64;
|
|
271
|
+
function fnmatchBasename(pattern, candidate) {
|
|
272
|
+
if (pattern.length !== candidate.length) return false;
|
|
273
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
274
|
+
const p = pattern[i].toLowerCase();
|
|
275
|
+
const c = candidate[i].toLowerCase();
|
|
276
|
+
if (p === "?") continue;
|
|
277
|
+
if (p !== c) return false;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
function bracketTokenMatchesForbidden(token, forbiddenBasenames) {
|
|
282
|
+
const literals = [];
|
|
283
|
+
let current = "";
|
|
284
|
+
let inBracket = false;
|
|
285
|
+
for (let i = 0; i < token.length; i++) {
|
|
286
|
+
if (token[i] === "[" && !inBracket) {
|
|
287
|
+
if (current) literals.push(current);
|
|
288
|
+
current = "";
|
|
289
|
+
inBracket = true;
|
|
290
|
+
} else if (token[i] === "]" && inBracket) {
|
|
291
|
+
inBracket = false;
|
|
292
|
+
} else if (!inBracket) {
|
|
293
|
+
current += token[i];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (current) literals.push(current);
|
|
297
|
+
for (const forbidden of forbiddenBasenames) {
|
|
298
|
+
const fl = forbidden.toLowerCase();
|
|
299
|
+
for (const lit of literals) {
|
|
300
|
+
if (lit.length === 0) continue;
|
|
301
|
+
if (fl.includes(lit.toLowerCase())) return forbidden;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
function resolveBraceExpansion(token) {
|
|
307
|
+
const match = token.match(/^(.*?)\{([^}]*,[^}]*)\}(.*)$/);
|
|
308
|
+
if (!match) return null;
|
|
309
|
+
const [, prefix, alternatives, suffix] = match;
|
|
310
|
+
const parts = alternatives.split(",");
|
|
311
|
+
if (parts.length > MAX_BRACE_EXPANSION) return null;
|
|
312
|
+
return parts.map((p) => prefix + p + suffix);
|
|
313
|
+
}
|
|
314
|
+
function wildcardDispatch(token, forbiddenBasenames, metadataField) {
|
|
315
|
+
const result = {
|
|
316
|
+
resolvedBasenames: [],
|
|
317
|
+
unparseable: false,
|
|
318
|
+
metadata: {}
|
|
319
|
+
};
|
|
320
|
+
if (token === "*" || token === "**" || token === "?") {
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
if (BRACE_PATTERN_RE.test(token)) {
|
|
324
|
+
const expanded = resolveBraceExpansion(token);
|
|
325
|
+
if (expanded === null) {
|
|
326
|
+
result.unparseable = true;
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
for (const alt of expanded) {
|
|
330
|
+
const hasWildcard = /[?*[]/.test(alt);
|
|
331
|
+
if (hasWildcard) {
|
|
332
|
+
const sub = wildcardDispatch(alt, forbiddenBasenames, metadataField);
|
|
333
|
+
if (sub.resolvedBasenames.length > 0) {
|
|
334
|
+
result.resolvedBasenames.push(...sub.resolvedBasenames);
|
|
335
|
+
result.metadata["resolvedFromBrace"] = token;
|
|
336
|
+
Object.assign(result.metadata, sub.metadata);
|
|
337
|
+
}
|
|
338
|
+
if (sub.unparseable) result.unparseable = true;
|
|
339
|
+
} else {
|
|
340
|
+
const altLower = alt.toLowerCase();
|
|
341
|
+
for (const forbidden of forbiddenBasenames) {
|
|
342
|
+
if (altLower === forbidden.toLowerCase()) {
|
|
343
|
+
result.resolvedBasenames.push(forbidden);
|
|
344
|
+
result.metadata["resolvedFromBrace"] = token;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
const hasStar = token.includes("*");
|
|
353
|
+
const hasQuestion = token.includes("?");
|
|
354
|
+
const hasBracket = token.includes("[");
|
|
355
|
+
if (hasBracket) {
|
|
356
|
+
const matched = bracketTokenMatchesForbidden(token, forbiddenBasenames);
|
|
357
|
+
if (matched) {
|
|
358
|
+
result.resolvedBasenames.push(matched);
|
|
359
|
+
result.metadata["resolvedFromBracket"] = token;
|
|
360
|
+
} else {
|
|
361
|
+
result.unparseable = true;
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
if (hasStar && !hasQuestion) {
|
|
366
|
+
const matched = starLiteralSubstringCheck(token, forbiddenBasenames);
|
|
367
|
+
if (matched) {
|
|
368
|
+
result.resolvedBasenames.push(matched);
|
|
369
|
+
result.metadata[metadataField] = token;
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
if (hasQuestion && !hasStar) {
|
|
374
|
+
for (const forbidden of forbiddenBasenames) {
|
|
375
|
+
if (fnmatchBasename(token, forbidden)) {
|
|
376
|
+
result.resolvedBasenames.push(forbidden);
|
|
377
|
+
result.metadata[metadataField] = token;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
if (hasStar && hasQuestion) {
|
|
384
|
+
const starMatch = starLiteralSubstringCheck(token, forbiddenBasenames);
|
|
385
|
+
if (starMatch) {
|
|
386
|
+
result.resolvedBasenames.push(starMatch);
|
|
387
|
+
result.metadata[metadataField] = token;
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
const segments = token.split("*").filter((s) => s.includes("?"));
|
|
391
|
+
for (const seg of segments) {
|
|
392
|
+
for (const forbidden of forbiddenBasenames) {
|
|
393
|
+
if (fnmatchBasename(seg, forbidden)) {
|
|
394
|
+
result.resolvedBasenames.push(forbidden);
|
|
395
|
+
result.metadata[metadataField] = token;
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
function starLiteralSubstringCheck(token, forbiddenBasenames) {
|
|
405
|
+
const literals = token.split("*").filter((s) => s.length > 0);
|
|
406
|
+
for (const forbidden of forbiddenBasenames) {
|
|
407
|
+
const fl = forbidden.toLowerCase();
|
|
408
|
+
for (const lit of literals) {
|
|
409
|
+
if (fl.includes(lit.toLowerCase())) return forbidden;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
function shouldDispatchWildcard(token) {
|
|
415
|
+
const hasMetachar = /[?*[{]/.test(token);
|
|
416
|
+
if (!hasMetachar) return false;
|
|
417
|
+
if (isPathShaped(token)) return true;
|
|
418
|
+
if (token.includes("[")) return true;
|
|
419
|
+
if (BRACE_PATTERN_RE.test(token)) return true;
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
var SENSITIVE_BASENAME_RE = /(?:\.env|\.ssh|secrets|credentials|id_rsa|id_dsa|id_ecdsa|id_ed25519|\.pem|\.key)/i;
|
|
423
|
+
var DANGEROUS_COMMAND_TOKENS = /* @__PURE__ */ new Set(["eval"]);
|
|
424
|
+
var COMMAND_SUBSTITUTION_RE = /\$\(|`/;
|
|
425
|
+
var DANGEROUS_RAW_RE = /<<<|<\(|>\(/;
|
|
426
|
+
function isVarMarker(token) {
|
|
427
|
+
return typeof token === "object" && token !== null && "__sentinel_var" in token && typeof token.__sentinel_var === "string";
|
|
428
|
+
}
|
|
429
|
+
function tokenizePaths(command) {
|
|
430
|
+
const result = {
|
|
431
|
+
paths: [],
|
|
432
|
+
unparseable: false,
|
|
433
|
+
hasDangerousConstruct: false
|
|
434
|
+
};
|
|
435
|
+
if (DANGEROUS_RAW_RE.test(command)) {
|
|
436
|
+
result.hasDangerousConstruct = true;
|
|
437
|
+
}
|
|
438
|
+
if (COMMAND_SUBSTITUTION_RE.test(command)) {
|
|
439
|
+
result.hasDangerousConstruct = true;
|
|
440
|
+
}
|
|
441
|
+
let tokens;
|
|
442
|
+
try {
|
|
443
|
+
tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
|
|
444
|
+
} catch {
|
|
445
|
+
result.unparseable = true;
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
if (!Array.isArray(tokens)) {
|
|
449
|
+
result.unparseable = true;
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
let prevToken = null;
|
|
453
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
454
|
+
const token = tokens[i];
|
|
455
|
+
if (isVarMarker(token)) {
|
|
456
|
+
const nextToken = tokens[i + 1];
|
|
457
|
+
const nextIsPathRelevant = nextToken === void 0 || // end of tokens — var is complete argument
|
|
458
|
+
typeof nextToken === "object" && nextToken !== null && "op" in nextToken || // followed by operator — var is complete argument
|
|
459
|
+
typeof nextToken === "string" && isPathShaped(nextToken);
|
|
460
|
+
const prevIsPathRelevant = prevToken !== null && isPathShaped(prevToken);
|
|
461
|
+
if (nextIsPathRelevant || prevIsPathRelevant) {
|
|
462
|
+
result.unparseable = true;
|
|
463
|
+
}
|
|
464
|
+
prevToken = null;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (typeof token === "object" && token !== null) {
|
|
468
|
+
if ("pattern" in token) {
|
|
469
|
+
const globPattern = token.pattern;
|
|
470
|
+
const lastSlash = globPattern.lastIndexOf("/");
|
|
471
|
+
const dispatchTarget = lastSlash >= 0 ? globPattern.slice(lastSlash + 1) : globPattern;
|
|
472
|
+
const dispatch = wildcardDispatch(dispatchTarget, FORBIDDEN_BASENAMES, "resolvedFromGlob");
|
|
473
|
+
if (dispatch.resolvedBasenames.length > 0) {
|
|
474
|
+
for (const resolved of dispatch.resolvedBasenames) {
|
|
475
|
+
result.paths.push(resolved);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (dispatch.unparseable) {
|
|
479
|
+
result.unparseable = true;
|
|
480
|
+
}
|
|
481
|
+
if (SENSITIVE_BASENAME_RE.test(globPattern)) {
|
|
482
|
+
result.unparseable = true;
|
|
483
|
+
}
|
|
484
|
+
prevToken = null;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if ("op" in token) {
|
|
488
|
+
if (token.op === "<(") {
|
|
489
|
+
result.hasDangerousConstruct = true;
|
|
490
|
+
}
|
|
491
|
+
prevToken = null;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
prevToken = null;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (typeof token !== "string") {
|
|
498
|
+
prevToken = null;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (DANGEROUS_COMMAND_TOKENS.has(token.toLowerCase())) {
|
|
502
|
+
result.hasDangerousConstruct = true;
|
|
503
|
+
}
|
|
504
|
+
if ((prevToken === "sh" || prevToken === "bash" || prevToken === "/bin/sh" || prevToken === "/bin/bash") && token === "-c") {
|
|
505
|
+
result.hasDangerousConstruct = true;
|
|
506
|
+
}
|
|
507
|
+
if (shouldDispatchWildcard(token)) {
|
|
508
|
+
const metaField = "resolvedFromQuotedGlob";
|
|
509
|
+
const dispatch = wildcardDispatch(token, FORBIDDEN_BASENAMES, metaField);
|
|
510
|
+
if (dispatch.resolvedBasenames.length > 0) {
|
|
511
|
+
for (const resolved of dispatch.resolvedBasenames) {
|
|
512
|
+
result.paths.push(resolved);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (dispatch.unparseable) {
|
|
516
|
+
result.unparseable = true;
|
|
517
|
+
}
|
|
518
|
+
} else if (isPathShaped(token)) {
|
|
519
|
+
const resolved = resolvePathToken(token);
|
|
520
|
+
result.paths.push(resolved);
|
|
521
|
+
}
|
|
522
|
+
prevToken = token;
|
|
523
|
+
}
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
function isPathShaped(token) {
|
|
527
|
+
if (token.includes("/")) return true;
|
|
528
|
+
if (token.startsWith(".")) return true;
|
|
529
|
+
if (SENSITIVE_BASENAME_RE.test(token)) return true;
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
function resolvePathToken(token) {
|
|
533
|
+
const normalized = normalize(token);
|
|
534
|
+
try {
|
|
535
|
+
return realpathSync(normalized);
|
|
536
|
+
} catch (err) {
|
|
537
|
+
const code = err.code;
|
|
538
|
+
if (code === "ENOENT") {
|
|
539
|
+
return resolveNonexistentPathToken(normalized);
|
|
540
|
+
}
|
|
541
|
+
return normalized;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function resolveNonexistentPathToken(normalizedPath) {
|
|
545
|
+
let current = normalizedPath;
|
|
546
|
+
let suffix = "";
|
|
547
|
+
for (let i = 0; i < 50; i++) {
|
|
548
|
+
const parent = dirname3(current);
|
|
549
|
+
if (parent === current) {
|
|
550
|
+
return normalizedPath;
|
|
551
|
+
}
|
|
552
|
+
if (parent === ".") {
|
|
553
|
+
return normalizedPath;
|
|
554
|
+
}
|
|
555
|
+
suffix = suffix ? join3(basename(current), suffix) : basename(current);
|
|
556
|
+
current = parent;
|
|
557
|
+
try {
|
|
558
|
+
const resolved = realpathSync(current);
|
|
559
|
+
if (resolved !== current) {
|
|
560
|
+
return join3(resolved, suffix);
|
|
561
|
+
}
|
|
562
|
+
return join3(resolved, suffix);
|
|
563
|
+
} catch {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return normalizedPath;
|
|
568
|
+
}
|
|
569
|
+
var FORBIDDEN_BASENAMES = [
|
|
570
|
+
".env",
|
|
571
|
+
".ssh",
|
|
572
|
+
".aws",
|
|
573
|
+
"secrets",
|
|
574
|
+
"credentials",
|
|
575
|
+
"id_rsa",
|
|
576
|
+
"id_dsa",
|
|
577
|
+
"id_ecdsa",
|
|
578
|
+
"id_ed25519",
|
|
579
|
+
".pem",
|
|
580
|
+
".key"
|
|
581
|
+
];
|
|
582
|
+
function scanBashCommand(command, forbiddenBasenames) {
|
|
583
|
+
const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
|
|
584
|
+
const hits = [];
|
|
585
|
+
for (const basename3 of basenames) {
|
|
586
|
+
const pattern = buildPattern(basename3);
|
|
587
|
+
if (pattern.test(command)) {
|
|
588
|
+
hits.push(basename3);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { matched: hits.length > 0, hits };
|
|
592
|
+
}
|
|
593
|
+
function buildPattern(basename3) {
|
|
594
|
+
const escaped = escapeRegex(basename3);
|
|
595
|
+
if (basename3.startsWith(".") && basename3.length <= 4 && !isAlphaAfterDot(basename3)) {
|
|
596
|
+
return new RegExp(`\\w${escaped}(?=$|[\\s;&|<>()'"=\\/])`, "i");
|
|
597
|
+
}
|
|
598
|
+
if (basename3.startsWith(".")) {
|
|
599
|
+
return new RegExp(`(?:^|[\\s;&|<>()\\/'"=])${escaped}(?=$|[\\s;&|<>()\\/'"=.])`, "i");
|
|
600
|
+
}
|
|
601
|
+
return new RegExp(`\\b${escaped}\\b`, "i");
|
|
602
|
+
}
|
|
603
|
+
function scanContentForForbiddenBasenames(content, forbiddenBasenames) {
|
|
604
|
+
const hits = [];
|
|
605
|
+
for (const basename3 of forbiddenBasenames) {
|
|
606
|
+
const pattern = buildContentPattern(basename3);
|
|
607
|
+
if (pattern.test(content)) {
|
|
608
|
+
hits.push(basename3);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return { matched: hits.length > 0, hits };
|
|
612
|
+
}
|
|
613
|
+
function buildContentPattern(basename3) {
|
|
614
|
+
const escaped = escapeRegex(basename3);
|
|
615
|
+
if (basename3.startsWith(".") && basename3.length <= 4 && !isAlphaAfterDot(basename3)) {
|
|
616
|
+
return new RegExp(`\\w${escaped}(?=$|[\\s;&|<>()'"=\\/])`, "i");
|
|
617
|
+
}
|
|
618
|
+
if (basename3.startsWith(".")) {
|
|
619
|
+
return new RegExp(`(?:^|[\\s;&|<>()\\/'"=])${escaped}(?=$|[\\s;&|<>()\\/'"=.])`, "i");
|
|
620
|
+
}
|
|
621
|
+
return new RegExp(`(?<=[/\\\\]\\.?)${escaped}\\b`, "i");
|
|
622
|
+
}
|
|
623
|
+
function isAlphaAfterDot(s) {
|
|
624
|
+
return /^\.[a-zA-Z]+$/.test(s);
|
|
625
|
+
}
|
|
626
|
+
function escapeRegex(s) {
|
|
627
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
628
|
+
}
|
|
629
|
+
function scanGlobPattern(pattern, forbiddenBasenames) {
|
|
630
|
+
const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
|
|
631
|
+
const hits = [];
|
|
632
|
+
for (const basename3 of basenames) {
|
|
633
|
+
const re = buildGlobContextPattern(basename3);
|
|
634
|
+
if (re.test(pattern)) {
|
|
635
|
+
hits.push(basename3);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return { matched: hits.length > 0, hits };
|
|
639
|
+
}
|
|
640
|
+
function buildGlobContextPattern(basename3) {
|
|
641
|
+
const escaped = escapeRegex(basename3);
|
|
642
|
+
const GLOB_DELIM = String.raw`\s;&|<>()\\/'"=.*?{}\[\]`;
|
|
643
|
+
if (basename3.startsWith(".") && basename3.length <= 4 && !isAlphaAfterDot(basename3)) {
|
|
644
|
+
return new RegExp(`[\\w*]${escaped}(?=$|[${GLOB_DELIM}])`, "i");
|
|
645
|
+
}
|
|
646
|
+
if (basename3.startsWith(".")) {
|
|
647
|
+
return new RegExp(`(?:^|[${GLOB_DELIM}])${escaped}(?=$|[${GLOB_DELIM}])`, "i");
|
|
648
|
+
}
|
|
649
|
+
return new RegExp(`\\b${escaped}\\b`, "i");
|
|
650
|
+
}
|
|
651
|
+
var SAFE_VERBS = /* @__PURE__ */ new Set(["echo", "printf", ":", "true"]);
|
|
652
|
+
var SEGMENT_OPS = /* @__PURE__ */ new Set([";", "&&", "||", "|", "&", "\n"]);
|
|
653
|
+
var REDIRECT_OPS = /* @__PURE__ */ new Set([">", ">>", "<", "&>", ">&", "<&"]);
|
|
654
|
+
function positionallySafeBasenames(command) {
|
|
655
|
+
const safe = /* @__PURE__ */ new Set();
|
|
656
|
+
if (typeof command !== "string" || command.length === 0) return safe;
|
|
657
|
+
if (COMMAND_SUBSTITUTION_RE.test(command) || DANGEROUS_RAW_RE.test(command) || command.includes("<<")) {
|
|
658
|
+
return safe;
|
|
659
|
+
}
|
|
660
|
+
let tokens;
|
|
661
|
+
try {
|
|
662
|
+
tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
|
|
663
|
+
} catch {
|
|
664
|
+
return safe;
|
|
665
|
+
}
|
|
666
|
+
if (!Array.isArray(tokens)) return safe;
|
|
667
|
+
const tally = /* @__PURE__ */ new Map();
|
|
668
|
+
const mark = (hits, isSafe) => {
|
|
669
|
+
for (const b of hits) {
|
|
670
|
+
const e = tally.get(b) ?? { safe: 0, unsafe: 0 };
|
|
671
|
+
if (isSafe) e.safe++;
|
|
672
|
+
else e.unsafe++;
|
|
673
|
+
tally.set(b, e);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
let verb = null;
|
|
677
|
+
let expectVerb = true;
|
|
678
|
+
let prevRedirect = false;
|
|
679
|
+
for (const tok of tokens) {
|
|
680
|
+
if (tok && typeof tok === "object") {
|
|
681
|
+
if (isVarMarker(tok)) {
|
|
682
|
+
if (expectVerb) {
|
|
683
|
+
verb = null;
|
|
684
|
+
expectVerb = false;
|
|
685
|
+
}
|
|
686
|
+
prevRedirect = false;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if ("comment" in tok) {
|
|
690
|
+
const hits2 = scanBashCommand(
|
|
691
|
+
String(tok.comment),
|
|
692
|
+
FORBIDDEN_BASENAMES
|
|
693
|
+
).hits;
|
|
694
|
+
if (hits2.length) mark(hits2, true);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if ("pattern" in tok) {
|
|
698
|
+
const hits2 = scanGlobPattern(
|
|
699
|
+
String(tok.pattern),
|
|
700
|
+
FORBIDDEN_BASENAMES
|
|
701
|
+
).hits;
|
|
702
|
+
if (hits2.length) mark(hits2, false);
|
|
703
|
+
prevRedirect = false;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
if ("op" in tok) {
|
|
707
|
+
const op = String(tok.op);
|
|
708
|
+
prevRedirect = REDIRECT_OPS.has(op);
|
|
709
|
+
if (SEGMENT_OPS.has(op)) {
|
|
710
|
+
expectVerb = true;
|
|
711
|
+
verb = null;
|
|
712
|
+
}
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
prevRedirect = false;
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (typeof tok !== "string") {
|
|
719
|
+
prevRedirect = false;
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const isRedirectTarget = prevRedirect;
|
|
723
|
+
prevRedirect = false;
|
|
724
|
+
const hits = scanBashCommand(tok, FORBIDDEN_BASENAMES).hits;
|
|
725
|
+
if (expectVerb) {
|
|
726
|
+
verb = tok;
|
|
727
|
+
expectVerb = false;
|
|
728
|
+
if (hits.length) mark(hits, false);
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (hits.length) {
|
|
732
|
+
const safeVerb = verb !== null && SAFE_VERBS.has(verb);
|
|
733
|
+
mark(hits, safeVerb && !isRedirectTarget);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
for (const [b, e] of tally) {
|
|
737
|
+
if (e.unsafe === 0 && e.safe > 0) safe.add(b);
|
|
738
|
+
}
|
|
739
|
+
return safe;
|
|
740
|
+
}
|
|
741
|
+
function isPositionallySafeMention(command) {
|
|
742
|
+
const hits = scanBashCommand(command, FORBIDDEN_BASENAMES).hits;
|
|
743
|
+
if (hits.length === 0) return false;
|
|
744
|
+
const safe = positionallySafeBasenames(command);
|
|
745
|
+
return hits.every((h) => safe.has(h));
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/roleValidator.ts
|
|
264
749
|
var SUSPICIOUS_BASENAME_RE = /^\.|(\.env|secret|credential|key|config|token)/i;
|
|
265
750
|
function resolveSymlinks(normalizedPath) {
|
|
266
751
|
if (!normalizedPath || normalizedPath.includes("://")) return normalizedPath;
|
|
267
752
|
if (normalizedPath.includes("node_modules/") || normalizedPath.includes("node_modules\\")) {
|
|
268
753
|
return normalizedPath;
|
|
269
754
|
}
|
|
270
|
-
const base =
|
|
755
|
+
const base = basename2(normalizedPath);
|
|
271
756
|
const needsRealpath = SUSPICIOUS_BASENAME_RE.test(base);
|
|
272
757
|
if (!needsRealpath) {
|
|
273
758
|
try {
|
|
274
759
|
const stat = lstatSync(normalizedPath);
|
|
275
760
|
if (!stat.isSymbolicLink()) {
|
|
276
761
|
try {
|
|
277
|
-
const resolved =
|
|
762
|
+
const resolved = realpathSync2(normalizedPath);
|
|
278
763
|
return resolved === normalizedPath ? normalizedPath : resolved;
|
|
279
764
|
} catch {
|
|
280
765
|
return normalizedPath;
|
|
@@ -289,7 +774,7 @@ function resolveSymlinks(normalizedPath) {
|
|
|
289
774
|
}
|
|
290
775
|
}
|
|
291
776
|
try {
|
|
292
|
-
return
|
|
777
|
+
return realpathSync2(normalizedPath);
|
|
293
778
|
} catch (err) {
|
|
294
779
|
const code = err.code;
|
|
295
780
|
if (code === "ENOENT") {
|
|
@@ -302,19 +787,19 @@ function resolveNonexistentPath(normalizedPath) {
|
|
|
302
787
|
let current = normalizedPath;
|
|
303
788
|
let suffix = "";
|
|
304
789
|
for (let i = 0; i < 50; i++) {
|
|
305
|
-
const parent =
|
|
790
|
+
const parent = dirname4(current);
|
|
306
791
|
if (parent === current) {
|
|
307
792
|
return normalizedPath;
|
|
308
793
|
}
|
|
309
794
|
if (parent === ".") {
|
|
310
795
|
return normalizedPath;
|
|
311
796
|
}
|
|
312
|
-
suffix = suffix ?
|
|
797
|
+
suffix = suffix ? join4(basename2(current), suffix) : basename2(current);
|
|
313
798
|
current = parent;
|
|
314
799
|
try {
|
|
315
|
-
const resolved =
|
|
800
|
+
const resolved = realpathSync2(current);
|
|
316
801
|
if (resolved !== current) {
|
|
317
|
-
return
|
|
802
|
+
return join4(resolved, suffix);
|
|
318
803
|
}
|
|
319
804
|
return normalizedPath;
|
|
320
805
|
} catch {
|
|
@@ -347,7 +832,7 @@ function normalizeForbiddenPattern(pattern) {
|
|
|
347
832
|
if (pattern.startsWith("**/") || pattern.startsWith("/")) return pattern;
|
|
348
833
|
return "**/" + pattern;
|
|
349
834
|
}
|
|
350
|
-
function
|
|
835
|
+
function isPathShaped2(value) {
|
|
351
836
|
if (value.length === 0 || value.length > 4096) return false;
|
|
352
837
|
if (/\s/.test(value)) return false;
|
|
353
838
|
if (value.includes("/") || value.includes("\\")) return true;
|
|
@@ -465,7 +950,7 @@ function walkForbiddenInodeRoots(forbiddenPatterns, cwd) {
|
|
|
465
950
|
collectForbiddenInodes(home, deepPatterns, inodes, false);
|
|
466
951
|
const sensitiveDirs = [".ssh", ".aws", ".gnupg", ".config"];
|
|
467
952
|
for (const dir of sensitiveDirs) {
|
|
468
|
-
collectForbiddenInodes(
|
|
953
|
+
collectForbiddenInodes(join4(home, dir), deepPatterns, inodes, true);
|
|
469
954
|
}
|
|
470
955
|
collectForbiddenInodes(cwd, deepPatterns, inodes, true, true);
|
|
471
956
|
return inodes;
|
|
@@ -479,7 +964,7 @@ function collectForbiddenInodes(dirPath, forbiddenPatterns, inodes, recursive, s
|
|
|
479
964
|
}
|
|
480
965
|
for (const entry of entries) {
|
|
481
966
|
if (skipNodeModules && entry === "node_modules") continue;
|
|
482
|
-
const fullPath =
|
|
967
|
+
const fullPath = join4(dirPath, entry);
|
|
483
968
|
let stat;
|
|
484
969
|
try {
|
|
485
970
|
stat = lstatSync(fullPath);
|
|
@@ -547,7 +1032,7 @@ var RoleValidator = class {
|
|
|
547
1032
|
}
|
|
548
1033
|
validateEvent(event, activeTask) {
|
|
549
1034
|
const eventTarget = event.primaryTarget;
|
|
550
|
-
const pathNormalized = isUrlShaped(eventTarget) ? eventTarget :
|
|
1035
|
+
const pathNormalized = isUrlShaped(eventTarget) ? eventTarget : normalize2(eventTarget);
|
|
551
1036
|
const resolvedTarget = resolveSymlinks(pathNormalized);
|
|
552
1037
|
const normalizedPrimaryTarget = resolvedTarget;
|
|
553
1038
|
const primaryTargetPaths = pathNormalized !== resolvedTarget ? [resolvedTarget, pathNormalized] : [resolvedTarget];
|
|
@@ -570,7 +1055,7 @@ var RoleValidator = class {
|
|
|
570
1055
|
for (let i = 1; i < event.targets.length; i++) {
|
|
571
1056
|
const st = event.targets[i];
|
|
572
1057
|
if (!isUrlShaped(st)) {
|
|
573
|
-
const stNorm =
|
|
1058
|
+
const stNorm = normalize2(st);
|
|
574
1059
|
inodeTargets.push(resolveSymlinks(stNorm));
|
|
575
1060
|
}
|
|
576
1061
|
}
|
|
@@ -588,9 +1073,10 @@ var RoleValidator = class {
|
|
|
588
1073
|
}
|
|
589
1074
|
}
|
|
590
1075
|
}
|
|
1076
|
+
const safeCommandMention = event.action === "command_exec" && isPositionallySafeMention(event.primaryTarget);
|
|
591
1077
|
for (const pattern of this.role.forbiddenTargetPatterns) {
|
|
592
1078
|
let matchedTargetValue = null;
|
|
593
|
-
if (primaryTargetPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
|
|
1079
|
+
if (!safeCommandMention && primaryTargetPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
|
|
594
1080
|
matchedTargetValue = normalizedPrimaryTarget;
|
|
595
1081
|
}
|
|
596
1082
|
if (!matchedTargetValue) {
|
|
@@ -598,7 +1084,7 @@ var RoleValidator = class {
|
|
|
598
1084
|
for (let i = 1; i < event.targets.length; i++) {
|
|
599
1085
|
const st = event.targets[i];
|
|
600
1086
|
if (mcpTool && /\s/.test(st)) continue;
|
|
601
|
-
const stNorm = isUrlShaped(st) ? st :
|
|
1087
|
+
const stNorm = isUrlShaped(st) ? st : normalize2(st);
|
|
602
1088
|
const stResolved = resolveSymlinks(stNorm);
|
|
603
1089
|
const stPaths = stNorm !== stResolved ? [stResolved, stNorm] : [stResolved];
|
|
604
1090
|
if (stPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
|
|
@@ -664,7 +1150,7 @@ var RoleValidator = class {
|
|
|
664
1150
|
if (this.sensitivityScorer && event.metadata?._mcpTool === "true") {
|
|
665
1151
|
for (let i = 1; i < event.targets.length; i++) {
|
|
666
1152
|
const val = event.targets[i];
|
|
667
|
-
if (!(isUrlShaped(val) ||
|
|
1153
|
+
if (!(isUrlShaped(val) || isPathShaped2(val))) continue;
|
|
668
1154
|
const sens = this.sensitivityScorer.scoreTarget(val, event.action);
|
|
669
1155
|
if (sens.effectiveScore < 0.6) continue;
|
|
670
1156
|
return this.makeFinding(event, {
|
|
@@ -1210,6 +1696,12 @@ export {
|
|
|
1210
1696
|
DEFAULT_FORBIDDEN_PATTERNS,
|
|
1211
1697
|
DEFAULT_MEDIUM_DISPOSITION,
|
|
1212
1698
|
TargetSensitivityScorer,
|
|
1699
|
+
tokenizePaths,
|
|
1700
|
+
FORBIDDEN_BASENAMES,
|
|
1701
|
+
scanBashCommand,
|
|
1702
|
+
scanContentForForbiddenBasenames,
|
|
1703
|
+
scanGlobPattern,
|
|
1704
|
+
isPositionallySafeMention,
|
|
1213
1705
|
matchGlob,
|
|
1214
1706
|
normalizeForbiddenPattern,
|
|
1215
1707
|
matchGlobInsensitive,
|
|
@@ -1218,4 +1710,4 @@ export {
|
|
|
1218
1710
|
findMatchingException,
|
|
1219
1711
|
RoleValidator
|
|
1220
1712
|
};
|
|
1221
|
-
//# sourceMappingURL=chunk-
|
|
1713
|
+
//# sourceMappingURL=chunk-QHE56MEO.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
acquireGatewayLock,
|
|
3
3
|
writePidFile
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LATQNIRW.js";
|
|
5
5
|
import {
|
|
6
6
|
discoverPolicy
|
|
7
7
|
} from "./chunk-FMZWHT4M.js";
|
|
@@ -536,4 +536,4 @@ export {
|
|
|
536
536
|
runInitClaudeCode,
|
|
537
537
|
runSessionStart
|
|
538
538
|
};
|
|
539
|
-
//# sourceMappingURL=chunk-
|
|
539
|
+
//# sourceMappingURL=chunk-WPTJBRX5.js.map
|