@tuent/sentinel 0.1.0 → 0.1.2
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 +26 -26
- package/SECURITY_MODEL.md +281 -0
- package/dist/Sentinel-XMSJE4DZ.js +10 -0
- package/dist/{Sentinel-B_sv8Kiy.d.ts → Sentinel-xFCyXH45.d.ts} +31 -1
- package/dist/chunk-B5QKJHSV.js +32 -0
- package/dist/{chunk-3U3PKD4N.js → chunk-FWIISAZZ.js} +119 -8
- package/dist/{chunk-QFRDEISP.js → chunk-GRN5P3H2.js} +67 -23
- package/dist/{chunk-Z3PWIJKT.js → chunk-L4R3LPJS.js} +237 -443
- package/dist/{chunk-CUJKNIKT.js → chunk-LATQNIRW.js} +33 -1
- package/dist/{chunk-6MHWJATS.js → chunk-QIYQWOLO.js} +589 -19
- package/dist/{chunk-2FFMYSVC.js → chunk-WLIDSTS4.js} +18 -2
- package/dist/cli.js +30 -30
- package/dist/gateway/index.d.ts +37 -1
- package/dist/gateway/index.js +4 -3
- package/dist/gatewayDaemon.js +4 -3
- package/dist/index.d.ts +11 -2
- package/dist/index.js +5 -5
- package/dist/pidManager-DOGVN6ZT.js +23 -0
- package/dist/{policyLoader-6KR5VFVV.js → policyLoader-KZL2U4M2.js} +2 -2
- package/package.json +3 -2
- package/dist/Sentinel-JLQL3YRD.js +0 -10
- package/dist/pidManager-ZYC7SICM.js +0 -15
|
@@ -234,8 +234,43 @@ var DEFAULT_FORBIDDEN_PATTERNS = [
|
|
|
234
234
|
"**/id_ed25519*",
|
|
235
235
|
"**/*.pem",
|
|
236
236
|
"**/*.key",
|
|
237
|
-
"/etc/**"
|
|
237
|
+
"/etc/**",
|
|
238
|
+
// Sprint 26 FIX 1 (A) — common credential stores. DRIFT: keep in sync with
|
|
239
|
+
// STARTER_POLICY.forbid.targets in setup/initClaudeCode.ts (template↔defaults
|
|
240
|
+
// unification tracked in a separate ticket — do not refactor here).
|
|
241
|
+
"**/.netrc",
|
|
242
|
+
"**/.npmrc",
|
|
243
|
+
"**/.git-credentials",
|
|
244
|
+
"**/.pgpass",
|
|
245
|
+
"**/.zsh_history",
|
|
246
|
+
"**/.config/gh/**",
|
|
247
|
+
"**/.docker/config.json",
|
|
248
|
+
"**/.gnupg/**",
|
|
249
|
+
"**/.config/gcloud/**",
|
|
250
|
+
"**/.kube/**",
|
|
251
|
+
"**/Library/Keychains/**",
|
|
252
|
+
// Sprint 26 FIX 1 (B) — Sentinel's own state dir (current path only; the
|
|
253
|
+
// ~/.dahlia → ~/.sentinel rename is a separate re-arch).
|
|
254
|
+
"**/.dahlia/**",
|
|
255
|
+
// Sprint 26 FIX 3 — the live policy file and cc's hook-wiring settings files.
|
|
256
|
+
// Denies the agent's own tool-writes (policy rewrite / unhook vectors); reads
|
|
257
|
+
// stay allowed via DEFAULT_POLICY_READ_EXCEPTIONS below. The settings glob
|
|
258
|
+
// covers project AND user-level (~/.claude/settings*.json) in one pattern.
|
|
259
|
+
// DRIFT: keep in sync with STARTER_POLICY.forbid.targets in setup/initClaudeCode.ts.
|
|
260
|
+
"**/.sentinel.yaml",
|
|
261
|
+
"**/.claude/settings*.json"
|
|
238
262
|
];
|
|
263
|
+
var DEFAULT_POLICY_READ_EXCEPTIONS = [
|
|
264
|
+
{ target: "**/.sentinel.yaml", allowedActions: ["file_read"] },
|
|
265
|
+
{ target: "**/.claude/settings*.json", allowedActions: ["file_read"] }
|
|
266
|
+
];
|
|
267
|
+
function withPolicyReadExceptions(existing) {
|
|
268
|
+
const merged = existing ? [...existing] : [];
|
|
269
|
+
for (const exc of DEFAULT_POLICY_READ_EXCEPTIONS) {
|
|
270
|
+
if (!merged.some((e) => e.target === exc.target)) merged.push(exc);
|
|
271
|
+
}
|
|
272
|
+
return merged;
|
|
273
|
+
}
|
|
239
274
|
var DEFAULT_MEDIUM_DISPOSITION = {
|
|
240
275
|
network_request: "deny"
|
|
241
276
|
};
|
|
@@ -258,23 +293,543 @@ var DEFAULT_NETWORK_DENYLIST_CIDRS = [
|
|
|
258
293
|
var DEFAULT_DANGEROUS_SCHEMES = ["file:", "data:", "javascript:", "vbscript:"];
|
|
259
294
|
|
|
260
295
|
// src/roleValidator.ts
|
|
261
|
-
import { normalize, basename, dirname as
|
|
262
|
-
import { lstatSync, readdirSync, realpathSync } from "fs";
|
|
296
|
+
import { normalize as normalize2, basename as basename2, dirname as dirname4, join as join4 } from "path";
|
|
297
|
+
import { lstatSync, readdirSync, realpathSync as realpathSync2 } from "fs";
|
|
263
298
|
import { homedir as homedir3 } from "os";
|
|
299
|
+
|
|
300
|
+
// src/gateway/bashScanner.ts
|
|
301
|
+
import { parse as shellParse } from "shell-quote";
|
|
302
|
+
import { realpathSync } from "fs";
|
|
303
|
+
import { dirname as dirname3, join as join3, basename, normalize } from "path";
|
|
304
|
+
var BRACE_PATTERN_RE = /\{[^}]*,[^}]*\}/;
|
|
305
|
+
var MAX_BRACE_EXPANSION = 64;
|
|
306
|
+
function fnmatchBasename(pattern, candidate) {
|
|
307
|
+
if (pattern.length !== candidate.length) return false;
|
|
308
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
309
|
+
const p = pattern[i].toLowerCase();
|
|
310
|
+
const c = candidate[i].toLowerCase();
|
|
311
|
+
if (p === "?") continue;
|
|
312
|
+
if (p !== c) return false;
|
|
313
|
+
}
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
function bracketTokenMatchesForbidden(token, forbiddenBasenames) {
|
|
317
|
+
const literals = [];
|
|
318
|
+
let current = "";
|
|
319
|
+
let inBracket = false;
|
|
320
|
+
for (let i = 0; i < token.length; i++) {
|
|
321
|
+
if (token[i] === "[" && !inBracket) {
|
|
322
|
+
if (current) literals.push(current);
|
|
323
|
+
current = "";
|
|
324
|
+
inBracket = true;
|
|
325
|
+
} else if (token[i] === "]" && inBracket) {
|
|
326
|
+
inBracket = false;
|
|
327
|
+
} else if (!inBracket) {
|
|
328
|
+
current += token[i];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (current) literals.push(current);
|
|
332
|
+
for (const forbidden of forbiddenBasenames) {
|
|
333
|
+
const fl = forbidden.toLowerCase();
|
|
334
|
+
for (const lit of literals) {
|
|
335
|
+
if (lit.length === 0) continue;
|
|
336
|
+
if (fl.includes(lit.toLowerCase())) return forbidden;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
function resolveBraceExpansion(token) {
|
|
342
|
+
const match = token.match(/^(.*?)\{([^}]*,[^}]*)\}(.*)$/);
|
|
343
|
+
if (!match) return null;
|
|
344
|
+
const [, prefix, alternatives, suffix] = match;
|
|
345
|
+
const parts = alternatives.split(",");
|
|
346
|
+
if (parts.length > MAX_BRACE_EXPANSION) return null;
|
|
347
|
+
return parts.map((p) => prefix + p + suffix);
|
|
348
|
+
}
|
|
349
|
+
function wildcardDispatch(token, forbiddenBasenames, metadataField) {
|
|
350
|
+
const result = {
|
|
351
|
+
resolvedBasenames: [],
|
|
352
|
+
unparseable: false,
|
|
353
|
+
metadata: {}
|
|
354
|
+
};
|
|
355
|
+
if (token === "*" || token === "**" || token === "?") {
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
if (BRACE_PATTERN_RE.test(token)) {
|
|
359
|
+
const expanded = resolveBraceExpansion(token);
|
|
360
|
+
if (expanded === null) {
|
|
361
|
+
result.unparseable = true;
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
for (const alt of expanded) {
|
|
365
|
+
const hasWildcard = /[?*[]/.test(alt);
|
|
366
|
+
if (hasWildcard) {
|
|
367
|
+
const sub = wildcardDispatch(alt, forbiddenBasenames, metadataField);
|
|
368
|
+
if (sub.resolvedBasenames.length > 0) {
|
|
369
|
+
result.resolvedBasenames.push(...sub.resolvedBasenames);
|
|
370
|
+
result.metadata["resolvedFromBrace"] = token;
|
|
371
|
+
Object.assign(result.metadata, sub.metadata);
|
|
372
|
+
}
|
|
373
|
+
if (sub.unparseable) result.unparseable = true;
|
|
374
|
+
} else {
|
|
375
|
+
const altLower = alt.toLowerCase();
|
|
376
|
+
for (const forbidden of forbiddenBasenames) {
|
|
377
|
+
if (altLower === forbidden.toLowerCase()) {
|
|
378
|
+
result.resolvedBasenames.push(forbidden);
|
|
379
|
+
result.metadata["resolvedFromBrace"] = token;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
const hasStar = token.includes("*");
|
|
388
|
+
const hasQuestion = token.includes("?");
|
|
389
|
+
const hasBracket = token.includes("[");
|
|
390
|
+
if (hasBracket) {
|
|
391
|
+
const matched = bracketTokenMatchesForbidden(token, forbiddenBasenames);
|
|
392
|
+
if (matched) {
|
|
393
|
+
result.resolvedBasenames.push(matched);
|
|
394
|
+
result.metadata["resolvedFromBracket"] = token;
|
|
395
|
+
} else {
|
|
396
|
+
result.unparseable = true;
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
if (hasStar && !hasQuestion) {
|
|
401
|
+
const matched = starLiteralSubstringCheck(token, forbiddenBasenames);
|
|
402
|
+
if (matched) {
|
|
403
|
+
result.resolvedBasenames.push(matched);
|
|
404
|
+
result.metadata[metadataField] = token;
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
if (hasQuestion && !hasStar) {
|
|
409
|
+
for (const forbidden of forbiddenBasenames) {
|
|
410
|
+
if (fnmatchBasename(token, forbidden)) {
|
|
411
|
+
result.resolvedBasenames.push(forbidden);
|
|
412
|
+
result.metadata[metadataField] = token;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
if (hasStar && hasQuestion) {
|
|
419
|
+
const starMatch = starLiteralSubstringCheck(token, forbiddenBasenames);
|
|
420
|
+
if (starMatch) {
|
|
421
|
+
result.resolvedBasenames.push(starMatch);
|
|
422
|
+
result.metadata[metadataField] = token;
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
const segments = token.split("*").filter((s) => s.includes("?"));
|
|
426
|
+
for (const seg of segments) {
|
|
427
|
+
for (const forbidden of forbiddenBasenames) {
|
|
428
|
+
if (fnmatchBasename(seg, forbidden)) {
|
|
429
|
+
result.resolvedBasenames.push(forbidden);
|
|
430
|
+
result.metadata[metadataField] = token;
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
function starLiteralSubstringCheck(token, forbiddenBasenames) {
|
|
440
|
+
const literals = token.split("*").filter((s) => s.length > 0);
|
|
441
|
+
for (const forbidden of forbiddenBasenames) {
|
|
442
|
+
const fl = forbidden.toLowerCase();
|
|
443
|
+
for (const lit of literals) {
|
|
444
|
+
if (fl.includes(lit.toLowerCase())) return forbidden;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
function shouldDispatchWildcard(token) {
|
|
450
|
+
const hasMetachar = /[?*[{]/.test(token);
|
|
451
|
+
if (!hasMetachar) return false;
|
|
452
|
+
if (isPathShaped(token)) return true;
|
|
453
|
+
if (token.includes("[")) return true;
|
|
454
|
+
if (BRACE_PATTERN_RE.test(token)) return true;
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
var SENSITIVE_BASENAME_RE = /(?:\.env|\.ssh|secrets|credentials|id_rsa|id_dsa|id_ecdsa|id_ed25519|\.pem|\.key|\.netrc|\.npmrc|\.pgpass|\.zsh_history|\.gnupg|\.kube|\.dahlia)/i;
|
|
458
|
+
var DANGEROUS_COMMAND_TOKENS = /* @__PURE__ */ new Set(["eval"]);
|
|
459
|
+
var COMMAND_SUBSTITUTION_RE = /\$\(|`/;
|
|
460
|
+
var DANGEROUS_RAW_RE = /<<<|<\(|>\(/;
|
|
461
|
+
function isVarMarker(token) {
|
|
462
|
+
return typeof token === "object" && token !== null && "__sentinel_var" in token && typeof token.__sentinel_var === "string";
|
|
463
|
+
}
|
|
464
|
+
function tokenizePaths(command) {
|
|
465
|
+
const result = {
|
|
466
|
+
paths: [],
|
|
467
|
+
unparseable: false,
|
|
468
|
+
hasDangerousConstruct: false
|
|
469
|
+
};
|
|
470
|
+
if (DANGEROUS_RAW_RE.test(command)) {
|
|
471
|
+
result.hasDangerousConstruct = true;
|
|
472
|
+
}
|
|
473
|
+
if (COMMAND_SUBSTITUTION_RE.test(command)) {
|
|
474
|
+
result.hasDangerousConstruct = true;
|
|
475
|
+
}
|
|
476
|
+
let tokens;
|
|
477
|
+
try {
|
|
478
|
+
tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
|
|
479
|
+
} catch {
|
|
480
|
+
result.unparseable = true;
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
if (!Array.isArray(tokens)) {
|
|
484
|
+
result.unparseable = true;
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
487
|
+
let prevToken = null;
|
|
488
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
489
|
+
const token = tokens[i];
|
|
490
|
+
if (isVarMarker(token)) {
|
|
491
|
+
const nextToken = tokens[i + 1];
|
|
492
|
+
const nextIsPathRelevant = nextToken === void 0 || // end of tokens — var is complete argument
|
|
493
|
+
typeof nextToken === "object" && nextToken !== null && "op" in nextToken || // followed by operator — var is complete argument
|
|
494
|
+
typeof nextToken === "string" && isPathShaped(nextToken);
|
|
495
|
+
const prevIsPathRelevant = prevToken !== null && isPathShaped(prevToken);
|
|
496
|
+
if (nextIsPathRelevant || prevIsPathRelevant) {
|
|
497
|
+
result.unparseable = true;
|
|
498
|
+
}
|
|
499
|
+
prevToken = null;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (typeof token === "object" && token !== null) {
|
|
503
|
+
if ("pattern" in token) {
|
|
504
|
+
const globPattern = token.pattern;
|
|
505
|
+
const lastSlash = globPattern.lastIndexOf("/");
|
|
506
|
+
const dispatchTarget = lastSlash >= 0 ? globPattern.slice(lastSlash + 1) : globPattern;
|
|
507
|
+
const dispatch = wildcardDispatch(dispatchTarget, FORBIDDEN_BASENAMES, "resolvedFromGlob");
|
|
508
|
+
if (dispatch.resolvedBasenames.length > 0) {
|
|
509
|
+
for (const resolved of dispatch.resolvedBasenames) {
|
|
510
|
+
result.paths.push(resolved);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (dispatch.unparseable) {
|
|
514
|
+
result.unparseable = true;
|
|
515
|
+
}
|
|
516
|
+
if (SENSITIVE_BASENAME_RE.test(globPattern)) {
|
|
517
|
+
result.unparseable = true;
|
|
518
|
+
}
|
|
519
|
+
prevToken = null;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if ("op" in token) {
|
|
523
|
+
if (token.op === "<(") {
|
|
524
|
+
result.hasDangerousConstruct = true;
|
|
525
|
+
}
|
|
526
|
+
prevToken = null;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
prevToken = null;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (typeof token !== "string") {
|
|
533
|
+
prevToken = null;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (DANGEROUS_COMMAND_TOKENS.has(token.toLowerCase())) {
|
|
537
|
+
result.hasDangerousConstruct = true;
|
|
538
|
+
}
|
|
539
|
+
if ((prevToken === "sh" || prevToken === "bash" || prevToken === "/bin/sh" || prevToken === "/bin/bash") && token === "-c") {
|
|
540
|
+
result.hasDangerousConstruct = true;
|
|
541
|
+
}
|
|
542
|
+
if (shouldDispatchWildcard(token)) {
|
|
543
|
+
const metaField = "resolvedFromQuotedGlob";
|
|
544
|
+
const dispatch = wildcardDispatch(token, FORBIDDEN_BASENAMES, metaField);
|
|
545
|
+
if (dispatch.resolvedBasenames.length > 0) {
|
|
546
|
+
for (const resolved of dispatch.resolvedBasenames) {
|
|
547
|
+
result.paths.push(resolved);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (dispatch.unparseable) {
|
|
551
|
+
result.unparseable = true;
|
|
552
|
+
}
|
|
553
|
+
} else if (isPathShaped(token)) {
|
|
554
|
+
const resolved = resolvePathToken(token);
|
|
555
|
+
result.paths.push(resolved);
|
|
556
|
+
}
|
|
557
|
+
prevToken = token;
|
|
558
|
+
}
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
function isPathShaped(token) {
|
|
562
|
+
if (token.includes("/")) return true;
|
|
563
|
+
if (token.startsWith(".")) return true;
|
|
564
|
+
if (SENSITIVE_BASENAME_RE.test(token)) return true;
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
function resolvePathToken(token) {
|
|
568
|
+
const normalized = normalize(token);
|
|
569
|
+
try {
|
|
570
|
+
return realpathSync(normalized);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
const code = err.code;
|
|
573
|
+
if (code === "ENOENT") {
|
|
574
|
+
return resolveNonexistentPathToken(normalized);
|
|
575
|
+
}
|
|
576
|
+
return normalized;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function resolveNonexistentPathToken(normalizedPath) {
|
|
580
|
+
let current = normalizedPath;
|
|
581
|
+
let suffix = "";
|
|
582
|
+
for (let i = 0; i < 50; i++) {
|
|
583
|
+
const parent = dirname3(current);
|
|
584
|
+
if (parent === current) {
|
|
585
|
+
return normalizedPath;
|
|
586
|
+
}
|
|
587
|
+
if (parent === ".") {
|
|
588
|
+
return normalizedPath;
|
|
589
|
+
}
|
|
590
|
+
suffix = suffix ? join3(basename(current), suffix) : basename(current);
|
|
591
|
+
current = parent;
|
|
592
|
+
try {
|
|
593
|
+
const resolved = realpathSync(current);
|
|
594
|
+
if (resolved !== current) {
|
|
595
|
+
return join3(resolved, suffix);
|
|
596
|
+
}
|
|
597
|
+
return join3(resolved, suffix);
|
|
598
|
+
} catch {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return normalizedPath;
|
|
603
|
+
}
|
|
604
|
+
var FORBIDDEN_BASENAMES = [
|
|
605
|
+
".env",
|
|
606
|
+
".ssh",
|
|
607
|
+
".aws",
|
|
608
|
+
"secrets",
|
|
609
|
+
"credentials",
|
|
610
|
+
"id_rsa",
|
|
611
|
+
"id_dsa",
|
|
612
|
+
"id_ecdsa",
|
|
613
|
+
"id_ed25519",
|
|
614
|
+
".pem",
|
|
615
|
+
".key",
|
|
616
|
+
// Sprint 26 FIX 1 (A) — credential-store file basenames (L2 bash deny).
|
|
617
|
+
// `.git-credentials` is already covered by the "credentials" entry above.
|
|
618
|
+
".netrc",
|
|
619
|
+
".npmrc",
|
|
620
|
+
".pgpass",
|
|
621
|
+
".zsh_history"
|
|
622
|
+
];
|
|
623
|
+
function scanBashCommand(command, forbiddenBasenames) {
|
|
624
|
+
const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
|
|
625
|
+
const hits = [];
|
|
626
|
+
for (const basename3 of basenames) {
|
|
627
|
+
const pattern = buildPattern(basename3);
|
|
628
|
+
if (pattern.test(command)) {
|
|
629
|
+
hits.push(basename3);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return { matched: hits.length > 0, hits };
|
|
633
|
+
}
|
|
634
|
+
function buildPattern(basename3) {
|
|
635
|
+
const escaped = escapeRegex(basename3);
|
|
636
|
+
if (basename3.startsWith(".") && basename3.length <= 4 && !isAlphaAfterDot(basename3)) {
|
|
637
|
+
return new RegExp(`\\w${escaped}(?=$|[\\s;&|<>()'"=\\/])`, "i");
|
|
638
|
+
}
|
|
639
|
+
if (basename3.startsWith(".")) {
|
|
640
|
+
return new RegExp(`(?:^|[\\s;&|<>()\\/'"=])${escaped}(?=$|[\\s;&|<>()\\/'"=.])`, "i");
|
|
641
|
+
}
|
|
642
|
+
return new RegExp(`\\b${escaped}\\b`, "i");
|
|
643
|
+
}
|
|
644
|
+
function scanContentForForbiddenBasenames(content, forbiddenBasenames) {
|
|
645
|
+
const hits = [];
|
|
646
|
+
for (const basename3 of forbiddenBasenames) {
|
|
647
|
+
const pattern = buildContentPattern(basename3);
|
|
648
|
+
if (pattern.test(content)) {
|
|
649
|
+
hits.push(basename3);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return { matched: hits.length > 0, hits };
|
|
653
|
+
}
|
|
654
|
+
function buildContentPattern(basename3) {
|
|
655
|
+
const escaped = escapeRegex(basename3);
|
|
656
|
+
if (basename3.startsWith(".") && basename3.length <= 4 && !isAlphaAfterDot(basename3)) {
|
|
657
|
+
return new RegExp(`\\w${escaped}(?=$|[\\s;&|<>()'"=\\/])`, "i");
|
|
658
|
+
}
|
|
659
|
+
if (basename3.startsWith(".")) {
|
|
660
|
+
return new RegExp(`(?:^|[\\s;&|<>()\\/'"=])${escaped}(?=$|[\\s;&|<>()\\/'"=.])`, "i");
|
|
661
|
+
}
|
|
662
|
+
return new RegExp(`(?<=[/\\\\]\\.?)${escaped}\\b`, "i");
|
|
663
|
+
}
|
|
664
|
+
function isAlphaAfterDot(s) {
|
|
665
|
+
return /^\.[a-zA-Z]+$/.test(s);
|
|
666
|
+
}
|
|
667
|
+
function escapeRegex(s) {
|
|
668
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
669
|
+
}
|
|
670
|
+
function scanGlobPattern(pattern, forbiddenBasenames) {
|
|
671
|
+
const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
|
|
672
|
+
const hits = [];
|
|
673
|
+
for (const basename3 of basenames) {
|
|
674
|
+
const re = buildGlobContextPattern(basename3);
|
|
675
|
+
if (re.test(pattern)) {
|
|
676
|
+
hits.push(basename3);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return { matched: hits.length > 0, hits };
|
|
680
|
+
}
|
|
681
|
+
function buildGlobContextPattern(basename3) {
|
|
682
|
+
const escaped = escapeRegex(basename3);
|
|
683
|
+
const GLOB_DELIM = String.raw`\s;&|<>()\\/'"=.*?{}\[\]`;
|
|
684
|
+
if (basename3.startsWith(".") && basename3.length <= 4 && !isAlphaAfterDot(basename3)) {
|
|
685
|
+
return new RegExp(`[\\w*]${escaped}(?=$|[${GLOB_DELIM}])`, "i");
|
|
686
|
+
}
|
|
687
|
+
if (basename3.startsWith(".")) {
|
|
688
|
+
return new RegExp(`(?:^|[${GLOB_DELIM}])${escaped}(?=$|[${GLOB_DELIM}])`, "i");
|
|
689
|
+
}
|
|
690
|
+
return new RegExp(`\\b${escaped}\\b`, "i");
|
|
691
|
+
}
|
|
692
|
+
var SAFE_VERBS = /* @__PURE__ */ new Set(["echo", "printf", ":", "true"]);
|
|
693
|
+
var SEGMENT_OPS = /* @__PURE__ */ new Set([";", "&&", "||", "|", "&", "\n"]);
|
|
694
|
+
var REDIRECT_OPS = /* @__PURE__ */ new Set([">", ">>", "<", "&>", ">&", "<&"]);
|
|
695
|
+
function positionallySafeBasenames(command) {
|
|
696
|
+
const safe = /* @__PURE__ */ new Set();
|
|
697
|
+
if (typeof command !== "string" || command.length === 0) return safe;
|
|
698
|
+
if (COMMAND_SUBSTITUTION_RE.test(command) || DANGEROUS_RAW_RE.test(command) || command.includes("<<")) {
|
|
699
|
+
return safe;
|
|
700
|
+
}
|
|
701
|
+
let tokens;
|
|
702
|
+
try {
|
|
703
|
+
tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
|
|
704
|
+
} catch {
|
|
705
|
+
return safe;
|
|
706
|
+
}
|
|
707
|
+
if (!Array.isArray(tokens)) return safe;
|
|
708
|
+
const tally = /* @__PURE__ */ new Map();
|
|
709
|
+
const mark = (hits, isSafe) => {
|
|
710
|
+
for (const b of hits) {
|
|
711
|
+
const e = tally.get(b) ?? { safe: 0, unsafe: 0 };
|
|
712
|
+
if (isSafe) e.safe++;
|
|
713
|
+
else e.unsafe++;
|
|
714
|
+
tally.set(b, e);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
let verb = null;
|
|
718
|
+
let expectVerb = true;
|
|
719
|
+
let prevRedirect = false;
|
|
720
|
+
for (const tok of tokens) {
|
|
721
|
+
if (tok && typeof tok === "object") {
|
|
722
|
+
if (isVarMarker(tok)) {
|
|
723
|
+
if (expectVerb) {
|
|
724
|
+
verb = null;
|
|
725
|
+
expectVerb = false;
|
|
726
|
+
}
|
|
727
|
+
prevRedirect = false;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if ("comment" in tok) {
|
|
731
|
+
const hits2 = scanBashCommand(
|
|
732
|
+
String(tok.comment),
|
|
733
|
+
FORBIDDEN_BASENAMES
|
|
734
|
+
).hits;
|
|
735
|
+
if (hits2.length) mark(hits2, true);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if ("pattern" in tok) {
|
|
739
|
+
const hits2 = scanGlobPattern(
|
|
740
|
+
String(tok.pattern),
|
|
741
|
+
FORBIDDEN_BASENAMES
|
|
742
|
+
).hits;
|
|
743
|
+
if (hits2.length) mark(hits2, false);
|
|
744
|
+
prevRedirect = false;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if ("op" in tok) {
|
|
748
|
+
const op = String(tok.op);
|
|
749
|
+
prevRedirect = REDIRECT_OPS.has(op);
|
|
750
|
+
if (SEGMENT_OPS.has(op)) {
|
|
751
|
+
expectVerb = true;
|
|
752
|
+
verb = null;
|
|
753
|
+
}
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
prevRedirect = false;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (typeof tok !== "string") {
|
|
760
|
+
prevRedirect = false;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const isRedirectTarget = prevRedirect;
|
|
764
|
+
prevRedirect = false;
|
|
765
|
+
const hits = scanBashCommand(tok, FORBIDDEN_BASENAMES).hits;
|
|
766
|
+
if (expectVerb) {
|
|
767
|
+
verb = tok;
|
|
768
|
+
expectVerb = false;
|
|
769
|
+
if (hits.length) mark(hits, false);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (hits.length) {
|
|
773
|
+
const safeVerb = verb !== null && SAFE_VERBS.has(verb);
|
|
774
|
+
mark(hits, safeVerb && !isRedirectTarget);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
for (const [b, e] of tally) {
|
|
778
|
+
if (e.unsafe === 0 && e.safe > 0) safe.add(b);
|
|
779
|
+
}
|
|
780
|
+
return safe;
|
|
781
|
+
}
|
|
782
|
+
function isPositionallySafeMention(command) {
|
|
783
|
+
const hits = scanBashCommand(command, FORBIDDEN_BASENAMES).hits;
|
|
784
|
+
if (hits.length === 0) return false;
|
|
785
|
+
const safe = positionallySafeBasenames(command);
|
|
786
|
+
return hits.every((h) => safe.has(h));
|
|
787
|
+
}
|
|
788
|
+
function classifyDeny(command, signals) {
|
|
789
|
+
const { l2Hits, hasL1Hit, unparseable, hasDangerousConstruct } = signals;
|
|
790
|
+
if (hasL1Hit || unparseable || hasDangerousConstruct) return { mentionOnly: false };
|
|
791
|
+
if (l2Hits.length === 0) return { mentionOnly: false };
|
|
792
|
+
let tokens;
|
|
793
|
+
try {
|
|
794
|
+
tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
|
|
795
|
+
} catch {
|
|
796
|
+
return { mentionOnly: false };
|
|
797
|
+
}
|
|
798
|
+
if (!Array.isArray(tokens)) return { mentionOnly: false };
|
|
799
|
+
if (tokens.some((t) => isVarMarker(t))) return { mentionOnly: false };
|
|
800
|
+
const strTokens = tokens.filter((t) => typeof t === "string");
|
|
801
|
+
for (const hit of l2Hits) {
|
|
802
|
+
const h = hit.toLowerCase();
|
|
803
|
+
const confident = strTokens.some((tok) => {
|
|
804
|
+
const low = tok.toLowerCase();
|
|
805
|
+
if (!low.includes(h)) return false;
|
|
806
|
+
if (low.length === h.length) return false;
|
|
807
|
+
const reHits = scanBashCommand(tok, FORBIDDEN_BASENAMES).hits;
|
|
808
|
+
if (reHits.some((rh) => tok.length === rh.length)) return false;
|
|
809
|
+
const reTok = tokenizePaths(tok);
|
|
810
|
+
if (reTok.unparseable || reTok.hasDangerousConstruct) return false;
|
|
811
|
+
return true;
|
|
812
|
+
});
|
|
813
|
+
if (!confident) return { mentionOnly: false };
|
|
814
|
+
}
|
|
815
|
+
return { mentionOnly: true };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/roleValidator.ts
|
|
264
819
|
var SUSPICIOUS_BASENAME_RE = /^\.|(\.env|secret|credential|key|config|token)/i;
|
|
265
820
|
function resolveSymlinks(normalizedPath) {
|
|
266
821
|
if (!normalizedPath || normalizedPath.includes("://")) return normalizedPath;
|
|
267
822
|
if (normalizedPath.includes("node_modules/") || normalizedPath.includes("node_modules\\")) {
|
|
268
823
|
return normalizedPath;
|
|
269
824
|
}
|
|
270
|
-
const base =
|
|
825
|
+
const base = basename2(normalizedPath);
|
|
271
826
|
const needsRealpath = SUSPICIOUS_BASENAME_RE.test(base);
|
|
272
827
|
if (!needsRealpath) {
|
|
273
828
|
try {
|
|
274
829
|
const stat = lstatSync(normalizedPath);
|
|
275
830
|
if (!stat.isSymbolicLink()) {
|
|
276
831
|
try {
|
|
277
|
-
const resolved =
|
|
832
|
+
const resolved = realpathSync2(normalizedPath);
|
|
278
833
|
return resolved === normalizedPath ? normalizedPath : resolved;
|
|
279
834
|
} catch {
|
|
280
835
|
return normalizedPath;
|
|
@@ -289,7 +844,7 @@ function resolveSymlinks(normalizedPath) {
|
|
|
289
844
|
}
|
|
290
845
|
}
|
|
291
846
|
try {
|
|
292
|
-
return
|
|
847
|
+
return realpathSync2(normalizedPath);
|
|
293
848
|
} catch (err) {
|
|
294
849
|
const code = err.code;
|
|
295
850
|
if (code === "ENOENT") {
|
|
@@ -302,19 +857,19 @@ function resolveNonexistentPath(normalizedPath) {
|
|
|
302
857
|
let current = normalizedPath;
|
|
303
858
|
let suffix = "";
|
|
304
859
|
for (let i = 0; i < 50; i++) {
|
|
305
|
-
const parent =
|
|
860
|
+
const parent = dirname4(current);
|
|
306
861
|
if (parent === current) {
|
|
307
862
|
return normalizedPath;
|
|
308
863
|
}
|
|
309
864
|
if (parent === ".") {
|
|
310
865
|
return normalizedPath;
|
|
311
866
|
}
|
|
312
|
-
suffix = suffix ?
|
|
867
|
+
suffix = suffix ? join4(basename2(current), suffix) : basename2(current);
|
|
313
868
|
current = parent;
|
|
314
869
|
try {
|
|
315
|
-
const resolved =
|
|
870
|
+
const resolved = realpathSync2(current);
|
|
316
871
|
if (resolved !== current) {
|
|
317
|
-
return
|
|
872
|
+
return join4(resolved, suffix);
|
|
318
873
|
}
|
|
319
874
|
return normalizedPath;
|
|
320
875
|
} catch {
|
|
@@ -347,7 +902,12 @@ function normalizeForbiddenPattern(pattern) {
|
|
|
347
902
|
if (pattern.startsWith("**/") || pattern.startsWith("/")) return pattern;
|
|
348
903
|
return "**/" + pattern;
|
|
349
904
|
}
|
|
350
|
-
function
|
|
905
|
+
function unionWithDefaultForbiddenPatterns(supplied) {
|
|
906
|
+
return [
|
|
907
|
+
...new Set([...supplied ?? [], ...DEFAULT_FORBIDDEN_PATTERNS].map(normalizeForbiddenPattern))
|
|
908
|
+
];
|
|
909
|
+
}
|
|
910
|
+
function isPathShaped2(value) {
|
|
351
911
|
if (value.length === 0 || value.length > 4096) return false;
|
|
352
912
|
if (/\s/.test(value)) return false;
|
|
353
913
|
if (value.includes("/") || value.includes("\\")) return true;
|
|
@@ -465,7 +1025,7 @@ function walkForbiddenInodeRoots(forbiddenPatterns, cwd) {
|
|
|
465
1025
|
collectForbiddenInodes(home, deepPatterns, inodes, false);
|
|
466
1026
|
const sensitiveDirs = [".ssh", ".aws", ".gnupg", ".config"];
|
|
467
1027
|
for (const dir of sensitiveDirs) {
|
|
468
|
-
collectForbiddenInodes(
|
|
1028
|
+
collectForbiddenInodes(join4(home, dir), deepPatterns, inodes, true);
|
|
469
1029
|
}
|
|
470
1030
|
collectForbiddenInodes(cwd, deepPatterns, inodes, true, true);
|
|
471
1031
|
return inodes;
|
|
@@ -479,7 +1039,7 @@ function collectForbiddenInodes(dirPath, forbiddenPatterns, inodes, recursive, s
|
|
|
479
1039
|
}
|
|
480
1040
|
for (const entry of entries) {
|
|
481
1041
|
if (skipNodeModules && entry === "node_modules") continue;
|
|
482
|
-
const fullPath =
|
|
1042
|
+
const fullPath = join4(dirPath, entry);
|
|
483
1043
|
let stat;
|
|
484
1044
|
try {
|
|
485
1045
|
stat = lstatSync(fullPath);
|
|
@@ -547,7 +1107,7 @@ var RoleValidator = class {
|
|
|
547
1107
|
}
|
|
548
1108
|
validateEvent(event, activeTask) {
|
|
549
1109
|
const eventTarget = event.primaryTarget;
|
|
550
|
-
const pathNormalized = isUrlShaped(eventTarget) ? eventTarget :
|
|
1110
|
+
const pathNormalized = isUrlShaped(eventTarget) ? eventTarget : normalize2(eventTarget);
|
|
551
1111
|
const resolvedTarget = resolveSymlinks(pathNormalized);
|
|
552
1112
|
const normalizedPrimaryTarget = resolvedTarget;
|
|
553
1113
|
const primaryTargetPaths = pathNormalized !== resolvedTarget ? [resolvedTarget, pathNormalized] : [resolvedTarget];
|
|
@@ -570,7 +1130,7 @@ var RoleValidator = class {
|
|
|
570
1130
|
for (let i = 1; i < event.targets.length; i++) {
|
|
571
1131
|
const st = event.targets[i];
|
|
572
1132
|
if (!isUrlShaped(st)) {
|
|
573
|
-
const stNorm =
|
|
1133
|
+
const stNorm = normalize2(st);
|
|
574
1134
|
inodeTargets.push(resolveSymlinks(stNorm));
|
|
575
1135
|
}
|
|
576
1136
|
}
|
|
@@ -588,9 +1148,10 @@ var RoleValidator = class {
|
|
|
588
1148
|
}
|
|
589
1149
|
}
|
|
590
1150
|
}
|
|
1151
|
+
const safeCommandMention = event.action === "command_exec" && isPositionallySafeMention(event.primaryTarget);
|
|
591
1152
|
for (const pattern of this.role.forbiddenTargetPatterns) {
|
|
592
1153
|
let matchedTargetValue = null;
|
|
593
|
-
if (primaryTargetPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
|
|
1154
|
+
if (!safeCommandMention && primaryTargetPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
|
|
594
1155
|
matchedTargetValue = normalizedPrimaryTarget;
|
|
595
1156
|
}
|
|
596
1157
|
if (!matchedTargetValue) {
|
|
@@ -598,7 +1159,7 @@ var RoleValidator = class {
|
|
|
598
1159
|
for (let i = 1; i < event.targets.length; i++) {
|
|
599
1160
|
const st = event.targets[i];
|
|
600
1161
|
if (mcpTool && /\s/.test(st)) continue;
|
|
601
|
-
const stNorm = isUrlShaped(st) ? st :
|
|
1162
|
+
const stNorm = isUrlShaped(st) ? st : normalize2(st);
|
|
602
1163
|
const stResolved = resolveSymlinks(stNorm);
|
|
603
1164
|
const stPaths = stNorm !== stResolved ? [stResolved, stNorm] : [stResolved];
|
|
604
1165
|
if (stPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
|
|
@@ -664,7 +1225,7 @@ var RoleValidator = class {
|
|
|
664
1225
|
if (this.sensitivityScorer && event.metadata?._mcpTool === "true") {
|
|
665
1226
|
for (let i = 1; i < event.targets.length; i++) {
|
|
666
1227
|
const val = event.targets[i];
|
|
667
|
-
if (!(isUrlShaped(val) ||
|
|
1228
|
+
if (!(isUrlShaped(val) || isPathShaped2(val))) continue;
|
|
668
1229
|
const sens = this.sensitivityScorer.scoreTarget(val, event.action);
|
|
669
1230
|
if (sens.effectiveScore < 0.6) continue;
|
|
670
1231
|
return this.makeFinding(event, {
|
|
@@ -1208,14 +1769,23 @@ export {
|
|
|
1208
1769
|
removeOverlayDecision,
|
|
1209
1770
|
createEmptyOverlay,
|
|
1210
1771
|
DEFAULT_FORBIDDEN_PATTERNS,
|
|
1772
|
+
withPolicyReadExceptions,
|
|
1211
1773
|
DEFAULT_MEDIUM_DISPOSITION,
|
|
1212
1774
|
TargetSensitivityScorer,
|
|
1775
|
+
tokenizePaths,
|
|
1776
|
+
FORBIDDEN_BASENAMES,
|
|
1777
|
+
scanBashCommand,
|
|
1778
|
+
scanContentForForbiddenBasenames,
|
|
1779
|
+
scanGlobPattern,
|
|
1780
|
+
isPositionallySafeMention,
|
|
1781
|
+
classifyDeny,
|
|
1213
1782
|
matchGlob,
|
|
1214
1783
|
normalizeForbiddenPattern,
|
|
1784
|
+
unionWithDefaultForbiddenPatterns,
|
|
1215
1785
|
matchGlobInsensitive,
|
|
1216
1786
|
getTargetRecommendation,
|
|
1217
1787
|
walkForbiddenInodeRoots,
|
|
1218
1788
|
findMatchingException,
|
|
1219
1789
|
RoleValidator
|
|
1220
1790
|
};
|
|
1221
|
-
//# sourceMappingURL=chunk-
|
|
1791
|
+
//# sourceMappingURL=chunk-QIYQWOLO.js.map
|