@node9/proxy 1.14.1 → 1.16.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/README.md +17 -0
- package/dist/cli.js +3080 -2168
- package/dist/cli.mjs +3060 -2150
- package/dist/index.js +2110 -1367
- package/dist/index.mjs +2501 -1758
- package/package.json +5 -2
- package/dist/shields/builtin/aws.json +0 -59
- package/dist/shields/builtin/bash-safe.json +0 -92
- package/dist/shields/builtin/docker.json +0 -120
- package/dist/shields/builtin/filesystem.json +0 -30
- package/dist/shields/builtin/github.json +0 -26
- package/dist/shields/builtin/k8s.json +0 -92
- package/dist/shields/builtin/mcp-tool-gating.json +0 -7
- package/dist/shields/builtin/mongodb.json +0 -78
- package/dist/shields/builtin/postgres.json +0 -42
- package/dist/shields/builtin/project-jail.json +0 -64
- package/dist/shields/builtin/redis.json +0 -78
package/dist/index.js
CHANGED
|
@@ -288,8 +288,8 @@ function sanitizeConfig(raw) {
|
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
const lines = result.error.issues.map((issue) => {
|
|
291
|
-
const
|
|
292
|
-
return ` \u2022 ${
|
|
291
|
+
const path13 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
292
|
+
return ` \u2022 ${path13}: ${issue.message}`;
|
|
293
293
|
});
|
|
294
294
|
return {
|
|
295
295
|
sanitized,
|
|
@@ -302,794 +302,112 @@ ${lines.join("\n")}`
|
|
|
302
302
|
var import_fs2 = __toESM(require("fs"));
|
|
303
303
|
var import_path2 = __toESM(require("path"));
|
|
304
304
|
var import_os2 = __toESM(require("os"));
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
`);
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
if (typeof r.description !== "string") {
|
|
320
|
-
process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
|
|
321
|
-
`);
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
if (!Array.isArray(r.aliases)) {
|
|
325
|
-
process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
|
|
326
|
-
`);
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
if (!Array.isArray(r.smartRules)) {
|
|
330
|
-
process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
|
|
331
|
-
`);
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
if (!Array.isArray(r.dangerousWords)) {
|
|
335
|
-
process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
|
|
336
|
-
`);
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
return r;
|
|
340
|
-
}
|
|
341
|
-
function loadShieldsFromDir(dir, label) {
|
|
342
|
-
const result = {};
|
|
343
|
-
let entries;
|
|
344
|
-
try {
|
|
345
|
-
entries = import_fs2.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
346
|
-
} catch (err) {
|
|
347
|
-
if (err.code !== "ENOENT") {
|
|
348
|
-
process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err)}
|
|
349
|
-
`);
|
|
350
|
-
}
|
|
351
|
-
return result;
|
|
352
|
-
}
|
|
353
|
-
for (const file of entries) {
|
|
354
|
-
const filePath = import_path2.default.join(dir, file);
|
|
355
|
-
try {
|
|
356
|
-
const raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
|
|
357
|
-
const shield = validateShieldDefinition(raw, filePath);
|
|
358
|
-
if (shield) result[shield.name] = shield;
|
|
359
|
-
} catch (err) {
|
|
360
|
-
process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err)}
|
|
361
|
-
`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
return result;
|
|
365
|
-
}
|
|
366
|
-
function buildSHIELDS() {
|
|
367
|
-
const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
|
|
368
|
-
const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
|
|
369
|
-
return { ...builtins, ...userShields };
|
|
370
|
-
}
|
|
371
|
-
var SHIELDS = buildSHIELDS();
|
|
372
|
-
function resolveShieldName(input) {
|
|
373
|
-
const lower = input.toLowerCase();
|
|
374
|
-
if (SHIELDS[lower]) return lower;
|
|
375
|
-
for (const [name, def] of Object.entries(SHIELDS)) {
|
|
376
|
-
if (def.aliases.includes(lower)) return name;
|
|
377
|
-
}
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
function getShield(name) {
|
|
381
|
-
const resolved = resolveShieldName(name);
|
|
382
|
-
return resolved ? SHIELDS[resolved] : null;
|
|
383
|
-
}
|
|
384
|
-
var SHIELDS_STATE_FILE = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields.json");
|
|
385
|
-
function isShieldVerdict(v) {
|
|
386
|
-
return v === "allow" || v === "review" || v === "block";
|
|
387
|
-
}
|
|
388
|
-
function validateOverrides(raw) {
|
|
389
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
390
|
-
const result = {};
|
|
391
|
-
for (const [shieldName, rules] of Object.entries(raw)) {
|
|
392
|
-
if (!rules || typeof rules !== "object" || Array.isArray(rules)) continue;
|
|
393
|
-
const validRules = {};
|
|
394
|
-
for (const [ruleName, verdict] of Object.entries(rules)) {
|
|
395
|
-
if (isShieldVerdict(verdict)) {
|
|
396
|
-
validRules[ruleName] = verdict;
|
|
397
|
-
} else {
|
|
398
|
-
process.stderr.write(
|
|
399
|
-
`[node9] Warning: shields.json contains invalid verdict "${String(verdict)}" for ${shieldName}/${ruleName} \u2014 entry ignored. File may be corrupted or tampered with.
|
|
400
|
-
`
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
if (Object.keys(validRules).length > 0) result[shieldName] = validRules;
|
|
405
|
-
}
|
|
406
|
-
return result;
|
|
305
|
+
|
|
306
|
+
// packages/policy-engine/dist/index.mjs
|
|
307
|
+
var import_safe_regex2 = __toESM(require("safe-regex2"), 1);
|
|
308
|
+
var import_mvdan_sh = __toESM(require("mvdan-sh"), 1);
|
|
309
|
+
var import_picomatch = __toESM(require("picomatch"), 1);
|
|
310
|
+
var import_safe_regex22 = __toESM(require("safe-regex2"), 1);
|
|
311
|
+
var import_safe_regex23 = __toESM(require("safe-regex2"), 1);
|
|
312
|
+
var import_crypto2 = __toESM(require("crypto"), 1);
|
|
313
|
+
var ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
|
|
314
|
+
function isAssignmentContext(text) {
|
|
315
|
+
return ASSIGNMENT_CONTEXT_RE.test(text);
|
|
407
316
|
}
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
return { active, overrides: validateOverrides(parsed.overrides) };
|
|
417
|
-
} catch (err) {
|
|
418
|
-
if (err.code !== "ENOENT") {
|
|
419
|
-
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
420
|
-
`);
|
|
421
|
-
}
|
|
422
|
-
return { active: [] };
|
|
317
|
+
function shannonEntropy(s) {
|
|
318
|
+
if (s.length === 0) return 0;
|
|
319
|
+
const freq = /* @__PURE__ */ new Map();
|
|
320
|
+
for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
321
|
+
let h = 0;
|
|
322
|
+
for (const count of freq.values()) {
|
|
323
|
+
const p = count / s.length;
|
|
324
|
+
h -= p * Math.log2(p);
|
|
423
325
|
}
|
|
326
|
+
return h;
|
|
424
327
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
"
|
|
435
|
-
|
|
436
|
-
"
|
|
437
|
-
|
|
328
|
+
var DLP_STOPWORDS = [
|
|
329
|
+
"example",
|
|
330
|
+
"placeholder",
|
|
331
|
+
"changeme",
|
|
332
|
+
"your_key",
|
|
333
|
+
"your_token",
|
|
334
|
+
"your_secret",
|
|
335
|
+
"replace_me",
|
|
336
|
+
"insert_key",
|
|
337
|
+
"put_your",
|
|
338
|
+
"fake",
|
|
339
|
+
"dummy",
|
|
340
|
+
"sample",
|
|
341
|
+
"xxxxxxxx",
|
|
342
|
+
"aaaaaa",
|
|
343
|
+
"bbbbbb",
|
|
344
|
+
"00000000",
|
|
345
|
+
"${",
|
|
346
|
+
"{{",
|
|
347
|
+
"%{",
|
|
348
|
+
"<your",
|
|
349
|
+
"test_key",
|
|
350
|
+
"test_token",
|
|
351
|
+
"your",
|
|
352
|
+
"here"
|
|
438
353
|
];
|
|
439
|
-
var
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
enableHookLogDebug: true,
|
|
447
|
-
approvalTimeoutMs: 12e4,
|
|
448
|
-
// 120-second auto-deny timeout
|
|
449
|
-
flightRecorder: true,
|
|
450
|
-
auditHashArgs: true,
|
|
451
|
-
approvers: { native: true, browser: false, cloud: false, terminal: true },
|
|
452
|
-
cloudSyncIntervalHours: 5
|
|
354
|
+
var DLP_PATTERNS = [
|
|
355
|
+
// ── AWS ───────────────────────────────────────────────────────────────────
|
|
356
|
+
{
|
|
357
|
+
name: "AWS Access Key ID",
|
|
358
|
+
regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/,
|
|
359
|
+
severity: "block",
|
|
360
|
+
keywords: ["akia", "asia", "abia", "acca", "a3t"]
|
|
453
361
|
},
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
"describe_*",
|
|
462
|
-
"read",
|
|
463
|
-
"glob",
|
|
464
|
-
"grep",
|
|
465
|
-
"ls",
|
|
466
|
-
"notebookread",
|
|
467
|
-
"notebookedit",
|
|
468
|
-
"webfetch",
|
|
469
|
-
"websearch",
|
|
470
|
-
"exitplanmode",
|
|
471
|
-
"askuserquestion",
|
|
472
|
-
"agent",
|
|
473
|
-
"task*",
|
|
474
|
-
"toolsearch",
|
|
475
|
-
"mcp__ide__*",
|
|
476
|
-
"getDiagnostics"
|
|
477
|
-
],
|
|
478
|
-
toolInspection: {
|
|
479
|
-
bash: "command",
|
|
480
|
-
shell: "command",
|
|
481
|
-
run_shell_command: "command",
|
|
482
|
-
"terminal.execute": "command",
|
|
483
|
-
"postgres:query": "sql"
|
|
484
|
-
},
|
|
485
|
-
snapshot: {
|
|
486
|
-
tools: [
|
|
487
|
-
"str_replace_based_edit_tool",
|
|
488
|
-
"write_file",
|
|
489
|
-
"edit_file",
|
|
490
|
-
"create_file",
|
|
491
|
-
"edit",
|
|
492
|
-
"replace"
|
|
493
|
-
],
|
|
494
|
-
onlyPaths: [],
|
|
495
|
-
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
496
|
-
},
|
|
497
|
-
smartRules: [
|
|
498
|
-
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
499
|
-
{
|
|
500
|
-
name: "block-rm-rf-home",
|
|
501
|
-
tool: "bash",
|
|
502
|
-
conditionMode: "all",
|
|
503
|
-
conditions: [
|
|
504
|
-
{
|
|
505
|
-
field: "command",
|
|
506
|
-
op: "matches",
|
|
507
|
-
// Anchor rm as a shell command (not inside a string arg like a git commit message).
|
|
508
|
-
value: "(^|&&|\\|\\||;)\\s*rm\\b[^;&|]*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
|
|
509
|
-
},
|
|
510
|
-
{
|
|
511
|
-
field: "command",
|
|
512
|
-
op: "matches",
|
|
513
|
-
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
514
|
-
}
|
|
515
|
-
],
|
|
516
|
-
verdict: "block",
|
|
517
|
-
reason: "Recursive delete of home directory is irreversible",
|
|
518
|
-
description: "The AI wants to recursively delete your home directory. This will permanently destroy all your personal files and cannot be undone."
|
|
519
|
-
},
|
|
520
|
-
// ── SQL safety ────────────────────────────────────────────────────────
|
|
521
|
-
{
|
|
522
|
-
name: "no-delete-without-where",
|
|
523
|
-
tool: "*",
|
|
524
|
-
conditions: [
|
|
525
|
-
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
526
|
-
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
527
|
-
],
|
|
528
|
-
conditionMode: "all",
|
|
529
|
-
verdict: "review",
|
|
530
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table",
|
|
531
|
-
description: "The AI is running a SQL statement that will modify every row in the table \u2014 no WHERE filter was found. This could wipe or corrupt all your data."
|
|
532
|
-
},
|
|
533
|
-
{
|
|
534
|
-
name: "review-drop-truncate-shell",
|
|
535
|
-
tool: "bash",
|
|
536
|
-
conditions: [
|
|
537
|
-
{
|
|
538
|
-
field: "command",
|
|
539
|
-
op: "matches",
|
|
540
|
-
// Require a DB CLI in the command so grep/cat/echo of SQL strings don't trigger.
|
|
541
|
-
value: "(^|&&|\\|\\||;|\\|)\\s*(psql|mysql|sqlite3|sqlplus|cockroach|clickhouse-client|mongo)\\b",
|
|
542
|
-
flags: "i"
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
field: "command",
|
|
546
|
-
op: "matches",
|
|
547
|
-
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
548
|
-
flags: "i"
|
|
549
|
-
}
|
|
550
|
-
],
|
|
551
|
-
conditionMode: "all",
|
|
552
|
-
verdict: "review",
|
|
553
|
-
reason: "SQL DDL destructive statement inside a shell command",
|
|
554
|
-
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
555
|
-
},
|
|
556
|
-
// ── Git safety ────────────────────────────────────────────────────────
|
|
557
|
-
{
|
|
558
|
-
name: "review-force-push",
|
|
559
|
-
tool: "bash",
|
|
560
|
-
conditions: [
|
|
561
|
-
{
|
|
562
|
-
field: "command",
|
|
563
|
-
op: "matches",
|
|
564
|
-
// Anchor git as a shell command so node -e / python -c scripts containing
|
|
565
|
-
// "git push --force" as a string don't false-positive.
|
|
566
|
-
value: "(^|&&|\\|\\||;)\\s*git\\s+push[^;&|]*(--force|--force-with-lease|-f\\b)",
|
|
567
|
-
flags: "i"
|
|
568
|
-
}
|
|
569
|
-
],
|
|
570
|
-
conditionMode: "all",
|
|
571
|
-
verdict: "review",
|
|
572
|
-
reason: "Force push rewrites remote history \u2014 confirm this is intentional",
|
|
573
|
-
description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
name: "review-git-destructive",
|
|
577
|
-
tool: "bash",
|
|
578
|
-
conditions: [
|
|
579
|
-
{
|
|
580
|
-
field: "command",
|
|
581
|
-
op: "matches",
|
|
582
|
-
// Anchor git as a shell command so node -e / python -c scripts containing
|
|
583
|
-
// "git reset --hard" as a string don't false-positive.
|
|
584
|
-
value: "(^|&&|\\|\\||;)\\s*git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
|
|
585
|
-
flags: "i"
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
field: "command",
|
|
589
|
-
op: "notMatches",
|
|
590
|
-
// Exclude recovery ops and routine branch-surgery (--onto) — these are not destructive.
|
|
591
|
-
value: "\\bgit\\s+rebase\\s+--(abort|continue|skip|onto)\\b",
|
|
592
|
-
flags: "i"
|
|
593
|
-
}
|
|
594
|
-
],
|
|
595
|
-
conditionMode: "all",
|
|
596
|
-
verdict: "review",
|
|
597
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes",
|
|
598
|
-
description: "The AI wants to run a destructive git operation (reset, rebase, clean, or branch delete) that can permanently discard commits or uncommitted work."
|
|
599
|
-
},
|
|
600
|
-
// ── Shell safety ──────────────────────────────────────────────────────
|
|
601
|
-
{
|
|
602
|
-
name: "review-sudo",
|
|
603
|
-
tool: "bash",
|
|
604
|
-
conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
|
|
605
|
-
conditionMode: "all",
|
|
606
|
-
verdict: "review",
|
|
607
|
-
reason: "Command requires elevated privileges",
|
|
608
|
-
description: "The AI wants to run a command as root (sudo). Commands with root access can modify system files, install software, or change security settings."
|
|
609
|
-
},
|
|
610
|
-
{
|
|
611
|
-
name: "review-curl-pipe-shell",
|
|
612
|
-
tool: "bash",
|
|
613
|
-
conditions: [
|
|
614
|
-
{
|
|
615
|
-
field: "command",
|
|
616
|
-
op: "matches",
|
|
617
|
-
// Anchor curl/wget as a shell command so node -e scripts testing this
|
|
618
|
-
// regex pattern don't self-match as a false positive.
|
|
619
|
-
value: "(^|&&|\\|\\||;)\\s*(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
620
|
-
flags: "i"
|
|
621
|
-
}
|
|
622
|
-
],
|
|
623
|
-
conditionMode: "all",
|
|
624
|
-
verdict: "block",
|
|
625
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
626
|
-
description: "The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed."
|
|
627
|
-
}
|
|
628
|
-
],
|
|
629
|
-
dlp: { enabled: true, scanIgnoredTools: true },
|
|
630
|
-
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
631
|
-
skillPinning: { enabled: false, mode: "warn", roots: [] }
|
|
362
|
+
// ── GitHub ────────────────────────────────────────────────────────────────
|
|
363
|
+
{
|
|
364
|
+
name: "GitHub Token",
|
|
365
|
+
regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/,
|
|
366
|
+
severity: "block",
|
|
367
|
+
keywords: ["ghp_", "gho_", "ghu_", "ghs_"],
|
|
368
|
+
minEntropy: 3
|
|
632
369
|
},
|
|
633
|
-
environments: {}
|
|
634
|
-
};
|
|
635
|
-
var ADVISORY_SMART_RULES = [
|
|
636
|
-
// ── rm safety ─────────────────────────────────────────────────────────────
|
|
637
|
-
// tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
|
|
638
|
-
// Pattern '(^|&&|\|\||;)\s*rm\b' matches rm as a shell command (including in
|
|
639
|
-
// chained commands like 'cat foo && rm bar') but avoids false-positives on 'docker rm'.
|
|
640
370
|
{
|
|
641
|
-
name: "
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
646
|
-
{
|
|
647
|
-
field: "command",
|
|
648
|
-
op: "matches",
|
|
649
|
-
// Matches known-safe build artifact paths in the command.
|
|
650
|
-
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
651
|
-
}
|
|
652
|
-
],
|
|
653
|
-
verdict: "allow",
|
|
654
|
-
reason: "Deleting a known-safe build artifact path"
|
|
371
|
+
name: "GitHub Fine-Grained PAT",
|
|
372
|
+
regex: /\bgithub_pat_\w{82}\b/,
|
|
373
|
+
severity: "block",
|
|
374
|
+
keywords: ["github_pat_"]
|
|
655
375
|
},
|
|
376
|
+
// ── Slack ─────────────────────────────────────────────────────────────────
|
|
656
377
|
{
|
|
657
|
-
name: "
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
description: "The AI wants to delete files. Unlike moving to trash, rm is permanent \u2014 the files cannot be recovered without a backup."
|
|
378
|
+
name: "Slack Bot Token",
|
|
379
|
+
// Real tokens are ~50–80 chars; lower bound 20 avoids false negatives on partial tokens
|
|
380
|
+
regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/,
|
|
381
|
+
severity: "block",
|
|
382
|
+
keywords: ["xoxb-"]
|
|
663
383
|
},
|
|
664
|
-
// ──
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
// The postgres shield upgrades these from 'review' → 'block' for stricter teams;
|
|
668
|
-
// without a shield, users still get a human-approval gate on every destructive op.
|
|
384
|
+
// ── Anthropic ─────────────────────────────────────────────────────────────
|
|
385
|
+
// Listed before OpenAI — Anthropic keys start with sk-ant- which would also
|
|
386
|
+
// match the broader OpenAI sk- pattern; more specific rules must come first.
|
|
669
387
|
{
|
|
670
|
-
name: "
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead",
|
|
675
|
-
description: "The AI wants to drop a database table. This permanently deletes the table and all its data \u2014 there is no undo."
|
|
388
|
+
name: "Anthropic API Key",
|
|
389
|
+
regex: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA\b/,
|
|
390
|
+
severity: "block",
|
|
391
|
+
keywords: ["sk-ant-api03"]
|
|
676
392
|
},
|
|
677
393
|
{
|
|
678
|
-
name: "
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead",
|
|
683
|
-
description: "The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone."
|
|
394
|
+
name: "Anthropic Admin Key",
|
|
395
|
+
regex: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA\b/,
|
|
396
|
+
severity: "block",
|
|
397
|
+
keywords: ["sk-ant-admin01"]
|
|
684
398
|
},
|
|
399
|
+
// ── OpenAI ────────────────────────────────────────────────────────────────
|
|
685
400
|
{
|
|
686
|
-
name: "
|
|
687
|
-
|
|
688
|
-
conditions: [
|
|
689
|
-
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
690
|
-
],
|
|
691
|
-
verdict: "review",
|
|
692
|
-
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead",
|
|
693
|
-
description: "The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row."
|
|
694
|
-
}
|
|
695
|
-
];
|
|
696
|
-
var cachedConfig = null;
|
|
697
|
-
function getCredentials() {
|
|
698
|
-
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
699
|
-
if (process.env.NODE9_API_KEY) {
|
|
700
|
-
return {
|
|
701
|
-
apiKey: process.env.NODE9_API_KEY,
|
|
702
|
-
apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
try {
|
|
706
|
-
const credPath = import_path3.default.join(import_os3.default.homedir(), ".node9", "credentials.json");
|
|
707
|
-
if (import_fs3.default.existsSync(credPath)) {
|
|
708
|
-
const creds = JSON.parse(import_fs3.default.readFileSync(credPath, "utf-8"));
|
|
709
|
-
const profileName = process.env.NODE9_PROFILE || "default";
|
|
710
|
-
const profile = creds[profileName];
|
|
711
|
-
if (profile?.apiKey) {
|
|
712
|
-
return {
|
|
713
|
-
apiKey: profile.apiKey,
|
|
714
|
-
apiUrl: profile.apiUrl || DEFAULT_API_URL
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
if (creds.apiKey) {
|
|
718
|
-
return {
|
|
719
|
-
apiKey: creds.apiKey,
|
|
720
|
-
apiUrl: creds.apiUrl || DEFAULT_API_URL
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
} catch {
|
|
725
|
-
}
|
|
726
|
-
return null;
|
|
727
|
-
}
|
|
728
|
-
function getActiveEnvironment(config) {
|
|
729
|
-
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
730
|
-
return config.environments[env] ?? null;
|
|
731
|
-
}
|
|
732
|
-
function getConfig(cwd) {
|
|
733
|
-
if (!cwd && cachedConfig) return cachedConfig;
|
|
734
|
-
const globalPath = import_path3.default.join(import_os3.default.homedir(), ".node9", "config.json");
|
|
735
|
-
const projectPath = import_path3.default.join(cwd ?? process.cwd(), "node9.config.json");
|
|
736
|
-
const globalConfig = tryLoadConfig(globalPath);
|
|
737
|
-
const projectConfig = tryLoadConfig(projectPath);
|
|
738
|
-
const mergedSettings = {
|
|
739
|
-
...DEFAULT_CONFIG.settings,
|
|
740
|
-
approvers: { ...DEFAULT_CONFIG.settings.approvers }
|
|
741
|
-
};
|
|
742
|
-
const mergedPolicy = {
|
|
743
|
-
sandboxPaths: [...DEFAULT_CONFIG.policy.sandboxPaths],
|
|
744
|
-
dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
|
|
745
|
-
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
746
|
-
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
747
|
-
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
748
|
-
snapshot: {
|
|
749
|
-
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
750
|
-
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
751
|
-
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
752
|
-
},
|
|
753
|
-
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
754
|
-
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
|
|
755
|
-
skillPinning: {
|
|
756
|
-
...DEFAULT_CONFIG.policy.skillPinning,
|
|
757
|
-
roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
|
|
758
|
-
}
|
|
759
|
-
};
|
|
760
|
-
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
761
|
-
const applyLayer = (source) => {
|
|
762
|
-
if (!source) return;
|
|
763
|
-
const s = source.settings || {};
|
|
764
|
-
const p = source.policy || {};
|
|
765
|
-
if (s.mode !== void 0) mergedSettings.mode = s.mode;
|
|
766
|
-
if (s.autoStartDaemon !== void 0) mergedSettings.autoStartDaemon = s.autoStartDaemon;
|
|
767
|
-
if (s.enableUndo !== void 0) mergedSettings.enableUndo = s.enableUndo;
|
|
768
|
-
if (s.enableHookLogDebug !== void 0)
|
|
769
|
-
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
770
|
-
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
771
|
-
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
772
|
-
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
773
|
-
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
774
|
-
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
775
|
-
if (s.cloudSyncIntervalHours !== void 0)
|
|
776
|
-
mergedSettings.cloudSyncIntervalHours = s.cloudSyncIntervalHours;
|
|
777
|
-
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
778
|
-
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
779
|
-
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
780
|
-
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
781
|
-
if (p.toolInspection)
|
|
782
|
-
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
783
|
-
if (p.smartRules) {
|
|
784
|
-
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
785
|
-
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
786
|
-
const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
|
|
787
|
-
const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
|
|
788
|
-
const filteredNonBlocks = defaultNonBlocks.filter(
|
|
789
|
-
(r) => !r.name || !userRuleNames.has(r.name)
|
|
790
|
-
);
|
|
791
|
-
mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
|
|
792
|
-
}
|
|
793
|
-
if (p.snapshot) {
|
|
794
|
-
const s2 = p.snapshot;
|
|
795
|
-
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
796
|
-
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
797
|
-
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
798
|
-
}
|
|
799
|
-
if (p.dlp) {
|
|
800
|
-
const d = p.dlp;
|
|
801
|
-
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
802
|
-
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
803
|
-
}
|
|
804
|
-
if (p.loopDetection) {
|
|
805
|
-
const ld = p.loopDetection;
|
|
806
|
-
if (ld.enabled !== void 0) mergedPolicy.loopDetection.enabled = ld.enabled;
|
|
807
|
-
if (ld.threshold !== void 0) mergedPolicy.loopDetection.threshold = ld.threshold;
|
|
808
|
-
if (ld.windowSeconds !== void 0)
|
|
809
|
-
mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
|
|
810
|
-
}
|
|
811
|
-
if (p.skillPinning && typeof p.skillPinning === "object") {
|
|
812
|
-
const sp = p.skillPinning;
|
|
813
|
-
if (sp.enabled !== void 0) mergedPolicy.skillPinning.enabled = sp.enabled;
|
|
814
|
-
if (sp.mode !== void 0) mergedPolicy.skillPinning.mode = sp.mode;
|
|
815
|
-
if (Array.isArray(sp.roots)) {
|
|
816
|
-
for (const r of sp.roots) {
|
|
817
|
-
if (typeof r === "string" && r.length > 0) mergedPolicy.skillPinning.roots.push(r);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
const envs = source.environments || {};
|
|
822
|
-
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
823
|
-
if (envConfig && typeof envConfig === "object") {
|
|
824
|
-
const ec = envConfig;
|
|
825
|
-
mergedEnvironments[envName] = {
|
|
826
|
-
...mergedEnvironments[envName],
|
|
827
|
-
// Validate field types before merging — do not blindly spread user input
|
|
828
|
-
...typeof ec.requireApproval === "boolean" ? { requireApproval: ec.requireApproval } : {}
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
};
|
|
833
|
-
applyLayer(globalConfig);
|
|
834
|
-
applyLayer(projectConfig);
|
|
835
|
-
{
|
|
836
|
-
const cacheFile = import_path3.default.join(import_os3.default.homedir(), ".node9", "rules-cache.json");
|
|
837
|
-
try {
|
|
838
|
-
const raw = JSON.parse(import_fs3.default.readFileSync(cacheFile, "utf-8"));
|
|
839
|
-
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
840
|
-
applyLayer({ policy: { smartRules: raw.rules } });
|
|
841
|
-
}
|
|
842
|
-
} catch {
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
const shieldOverrides = readShieldOverrides();
|
|
846
|
-
for (const shieldName of readActiveShields()) {
|
|
847
|
-
const shield = getShield(shieldName);
|
|
848
|
-
if (!shield) continue;
|
|
849
|
-
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
850
|
-
const ruleOverrides = shieldOverrides[shieldName] ?? {};
|
|
851
|
-
for (const rule of shield.smartRules) {
|
|
852
|
-
if (!existingRuleNames.has(rule.name)) {
|
|
853
|
-
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
854
|
-
mergedPolicy.smartRules.push(
|
|
855
|
-
overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
860
|
-
for (const word of shield.dangerousWords) {
|
|
861
|
-
if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
865
|
-
for (const rule of ADVISORY_SMART_RULES) {
|
|
866
|
-
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
867
|
-
}
|
|
868
|
-
if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
|
|
869
|
-
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
870
|
-
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
871
|
-
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
872
|
-
mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
|
|
873
|
-
mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
|
|
874
|
-
mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
|
|
875
|
-
mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
|
|
876
|
-
const result = {
|
|
877
|
-
settings: mergedSettings,
|
|
878
|
-
policy: mergedPolicy,
|
|
879
|
-
environments: mergedEnvironments
|
|
880
|
-
};
|
|
881
|
-
if (!cwd) cachedConfig = result;
|
|
882
|
-
return result;
|
|
883
|
-
}
|
|
884
|
-
function tryLoadConfig(filePath) {
|
|
885
|
-
if (!import_fs3.default.existsSync(filePath)) return null;
|
|
886
|
-
let raw;
|
|
887
|
-
try {
|
|
888
|
-
raw = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
|
|
889
|
-
} catch (err) {
|
|
890
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
891
|
-
process.stderr.write(
|
|
892
|
-
`
|
|
893
|
-
\u26A0\uFE0F Node9: Failed to parse ${filePath}
|
|
894
|
-
${msg}
|
|
895
|
-
\u2192 Using default config
|
|
896
|
-
|
|
897
|
-
`
|
|
898
|
-
);
|
|
899
|
-
return null;
|
|
900
|
-
}
|
|
901
|
-
const SUPPORTED_VERSION = "1.0";
|
|
902
|
-
const SUPPORTED_MAJOR = SUPPORTED_VERSION.split(".")[0];
|
|
903
|
-
const fileVersion = raw?.version;
|
|
904
|
-
if (fileVersion !== void 0) {
|
|
905
|
-
const vStr = String(fileVersion);
|
|
906
|
-
const fileMajor = vStr.split(".")[0];
|
|
907
|
-
if (fileMajor !== SUPPORTED_MAJOR) {
|
|
908
|
-
process.stderr.write(
|
|
909
|
-
`
|
|
910
|
-
\u274C Node9: Config at ${filePath} has version "${vStr}" \u2014 major version is incompatible with this release (expected "${SUPPORTED_VERSION}"). Config will not be loaded.
|
|
911
|
-
|
|
912
|
-
`
|
|
913
|
-
);
|
|
914
|
-
return null;
|
|
915
|
-
} else if (vStr !== SUPPORTED_VERSION) {
|
|
916
|
-
process.stderr.write(
|
|
917
|
-
`
|
|
918
|
-
\u26A0\uFE0F Node9: Config at ${filePath} declares version "${vStr}" \u2014 expected "${SUPPORTED_VERSION}". Continuing with best-effort parsing.
|
|
919
|
-
|
|
920
|
-
`
|
|
921
|
-
);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
const { sanitized, error } = sanitizeConfig(raw);
|
|
925
|
-
if (error) {
|
|
926
|
-
process.stderr.write(
|
|
927
|
-
`
|
|
928
|
-
\u26A0\uFE0F Node9: Invalid config at ${filePath}:
|
|
929
|
-
${error.replace("Invalid config:\n", "")}
|
|
930
|
-
\u2192 Invalid fields ignored, using defaults for those keys
|
|
931
|
-
|
|
932
|
-
`
|
|
933
|
-
);
|
|
934
|
-
}
|
|
935
|
-
return sanitized;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// src/utils/regex.ts
|
|
939
|
-
var import_safe_regex2 = __toESM(require("safe-regex2"));
|
|
940
|
-
var MAX_REGEX_LENGTH = 100;
|
|
941
|
-
var REGEX_CACHE_MAX = 500;
|
|
942
|
-
var regexCache = /* @__PURE__ */ new Map();
|
|
943
|
-
function validateRegex(pattern) {
|
|
944
|
-
if (!pattern) return "Pattern is required";
|
|
945
|
-
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
946
|
-
try {
|
|
947
|
-
new RegExp(pattern);
|
|
948
|
-
} catch (e) {
|
|
949
|
-
return `Invalid regex syntax: ${e.message}`;
|
|
950
|
-
}
|
|
951
|
-
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
952
|
-
if (!(0, import_safe_regex2.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
953
|
-
return null;
|
|
954
|
-
}
|
|
955
|
-
function getCompiledRegex(pattern, flags = "") {
|
|
956
|
-
if (flags && !/^[gimsuy]+$/.test(flags)) {
|
|
957
|
-
if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Invalid regex flags: "${flags}"`);
|
|
958
|
-
return null;
|
|
959
|
-
}
|
|
960
|
-
const key = `${pattern}\0${flags}`;
|
|
961
|
-
if (regexCache.has(key)) {
|
|
962
|
-
const cached = regexCache.get(key);
|
|
963
|
-
regexCache.delete(key);
|
|
964
|
-
regexCache.set(key, cached);
|
|
965
|
-
return cached;
|
|
966
|
-
}
|
|
967
|
-
const err = validateRegex(pattern);
|
|
968
|
-
if (err) {
|
|
969
|
-
if (process.env.NODE9_DEBUG === "1")
|
|
970
|
-
console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
|
|
971
|
-
return null;
|
|
972
|
-
}
|
|
973
|
-
try {
|
|
974
|
-
const re = new RegExp(pattern, flags);
|
|
975
|
-
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
976
|
-
const oldest = regexCache.keys().next().value;
|
|
977
|
-
if (oldest) regexCache.delete(oldest);
|
|
978
|
-
}
|
|
979
|
-
regexCache.set(key, re);
|
|
980
|
-
return re;
|
|
981
|
-
} catch (e) {
|
|
982
|
-
if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Regex compile failed:`, e);
|
|
983
|
-
return null;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
// src/policy/index.ts
|
|
988
|
-
var import_path8 = __toESM(require("path"));
|
|
989
|
-
var import_picomatch = __toESM(require("picomatch"));
|
|
990
|
-
var import_mvdan_sh = __toESM(require("mvdan-sh"));
|
|
991
|
-
|
|
992
|
-
// src/dlp.ts
|
|
993
|
-
var import_fs4 = __toESM(require("fs"));
|
|
994
|
-
var import_path4 = __toESM(require("path"));
|
|
995
|
-
var ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
|
|
996
|
-
function isAssignmentContext(text) {
|
|
997
|
-
return ASSIGNMENT_CONTEXT_RE.test(text);
|
|
998
|
-
}
|
|
999
|
-
function shannonEntropy(s) {
|
|
1000
|
-
if (s.length === 0) return 0;
|
|
1001
|
-
const freq = /* @__PURE__ */ new Map();
|
|
1002
|
-
for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
1003
|
-
let h = 0;
|
|
1004
|
-
for (const count of freq.values()) {
|
|
1005
|
-
const p = count / s.length;
|
|
1006
|
-
h -= p * Math.log2(p);
|
|
1007
|
-
}
|
|
1008
|
-
return h;
|
|
1009
|
-
}
|
|
1010
|
-
var DLP_STOPWORDS = [
|
|
1011
|
-
"example",
|
|
1012
|
-
"placeholder",
|
|
1013
|
-
"changeme",
|
|
1014
|
-
"your_key",
|
|
1015
|
-
"your_token",
|
|
1016
|
-
"your_secret",
|
|
1017
|
-
"replace_me",
|
|
1018
|
-
"insert_key",
|
|
1019
|
-
"put_your",
|
|
1020
|
-
"fake",
|
|
1021
|
-
"dummy",
|
|
1022
|
-
"sample",
|
|
1023
|
-
"xxxxxxxx",
|
|
1024
|
-
"aaaaaa",
|
|
1025
|
-
"bbbbbb",
|
|
1026
|
-
"00000000",
|
|
1027
|
-
"${",
|
|
1028
|
-
"{{",
|
|
1029
|
-
"%{",
|
|
1030
|
-
"<your",
|
|
1031
|
-
"test_key",
|
|
1032
|
-
"test_token",
|
|
1033
|
-
"your",
|
|
1034
|
-
"here"
|
|
1035
|
-
];
|
|
1036
|
-
var DLP_PATTERNS = [
|
|
1037
|
-
// ── AWS ───────────────────────────────────────────────────────────────────
|
|
1038
|
-
{
|
|
1039
|
-
name: "AWS Access Key ID",
|
|
1040
|
-
regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/,
|
|
401
|
+
name: "OpenAI API Key",
|
|
402
|
+
regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/,
|
|
1041
403
|
severity: "block",
|
|
1042
|
-
keywords: ["
|
|
404
|
+
keywords: ["sk-"],
|
|
405
|
+
minEntropy: 3.5
|
|
1043
406
|
},
|
|
1044
|
-
// ──
|
|
407
|
+
// ── Stripe ────────────────────────────────────────────────────────────────
|
|
1045
408
|
{
|
|
1046
|
-
name: "
|
|
1047
|
-
regex: /\
|
|
1048
|
-
severity: "block",
|
|
1049
|
-
keywords: ["ghp_", "gho_", "ghu_", "ghs_"],
|
|
1050
|
-
minEntropy: 3
|
|
1051
|
-
},
|
|
1052
|
-
{
|
|
1053
|
-
name: "GitHub Fine-Grained PAT",
|
|
1054
|
-
regex: /\bgithub_pat_\w{82}\b/,
|
|
1055
|
-
severity: "block",
|
|
1056
|
-
keywords: ["github_pat_"]
|
|
1057
|
-
},
|
|
1058
|
-
// ── Slack ─────────────────────────────────────────────────────────────────
|
|
1059
|
-
{
|
|
1060
|
-
name: "Slack Bot Token",
|
|
1061
|
-
// Real tokens are ~50–80 chars; lower bound 20 avoids false negatives on partial tokens
|
|
1062
|
-
regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/,
|
|
1063
|
-
severity: "block",
|
|
1064
|
-
keywords: ["xoxb-"]
|
|
1065
|
-
},
|
|
1066
|
-
// ── Anthropic ─────────────────────────────────────────────────────────────
|
|
1067
|
-
// Listed before OpenAI — Anthropic keys start with sk-ant- which would also
|
|
1068
|
-
// match the broader OpenAI sk- pattern; more specific rules must come first.
|
|
1069
|
-
{
|
|
1070
|
-
name: "Anthropic API Key",
|
|
1071
|
-
regex: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA\b/,
|
|
1072
|
-
severity: "block",
|
|
1073
|
-
keywords: ["sk-ant-api03"]
|
|
1074
|
-
},
|
|
1075
|
-
{
|
|
1076
|
-
name: "Anthropic Admin Key",
|
|
1077
|
-
regex: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA\b/,
|
|
1078
|
-
severity: "block",
|
|
1079
|
-
keywords: ["sk-ant-admin01"]
|
|
1080
|
-
},
|
|
1081
|
-
// ── OpenAI ────────────────────────────────────────────────────────────────
|
|
1082
|
-
{
|
|
1083
|
-
name: "OpenAI API Key",
|
|
1084
|
-
regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/,
|
|
1085
|
-
severity: "block",
|
|
1086
|
-
keywords: ["sk-"],
|
|
1087
|
-
minEntropy: 3.5
|
|
1088
|
-
},
|
|
1089
|
-
// ── Stripe ────────────────────────────────────────────────────────────────
|
|
1090
|
-
{
|
|
1091
|
-
name: "Stripe Secret Key",
|
|
1092
|
-
regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/,
|
|
409
|
+
name: "Stripe Secret Key",
|
|
410
|
+
regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/,
|
|
1093
411
|
severity: "block",
|
|
1094
412
|
keywords: ["sk_live_", "sk_test_"]
|
|
1095
413
|
},
|
|
@@ -1452,39 +770,45 @@ var SENSITIVE_PATH_PATTERNS = [
|
|
|
1452
770
|
/[/\\]id_ed25519$/i,
|
|
1453
771
|
/[/\\]id_ecdsa$/i
|
|
1454
772
|
];
|
|
1455
|
-
function
|
|
1456
|
-
|
|
1457
|
-
let resolved;
|
|
1458
|
-
try {
|
|
1459
|
-
const absolute = import_path4.default.resolve(cwd, filePath);
|
|
1460
|
-
resolved = import_fs4.default.realpathSync.native(absolute);
|
|
1461
|
-
} catch (err) {
|
|
1462
|
-
const code = err.code;
|
|
1463
|
-
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
1464
|
-
resolved = import_path4.default.resolve(cwd, filePath);
|
|
1465
|
-
} else {
|
|
1466
|
-
return {
|
|
1467
|
-
patternName: "Sensitive File Path",
|
|
1468
|
-
fieldPath: "file_path",
|
|
1469
|
-
redactedSample: filePath,
|
|
1470
|
-
severity: "block"
|
|
1471
|
-
};
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
const normalised = resolved.replace(/\\/g, "/");
|
|
773
|
+
function matchSensitivePath(resolvedPath, originalPath) {
|
|
774
|
+
const normalised = resolvedPath.replace(/\\/g, "/");
|
|
1475
775
|
for (const pattern of SENSITIVE_PATH_PATTERNS) {
|
|
1476
776
|
if (pattern.test(normalised)) {
|
|
1477
777
|
return {
|
|
1478
778
|
patternName: "Sensitive File Path",
|
|
1479
779
|
fieldPath: "file_path",
|
|
1480
|
-
redactedSample:
|
|
1481
|
-
// show original path in alert, not resolved
|
|
780
|
+
redactedSample: originalPath,
|
|
1482
781
|
severity: "block"
|
|
1483
782
|
};
|
|
1484
783
|
}
|
|
1485
784
|
}
|
|
1486
785
|
return null;
|
|
1487
786
|
}
|
|
787
|
+
function sensitivePathMatch(originalPath) {
|
|
788
|
+
return {
|
|
789
|
+
patternName: "Sensitive File Path",
|
|
790
|
+
fieldPath: "file_path",
|
|
791
|
+
redactedSample: originalPath,
|
|
792
|
+
severity: "block"
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function assertBuiltinPatternsAreSafe() {
|
|
796
|
+
for (const p of DLP_PATTERNS) {
|
|
797
|
+
if (!(0, import_safe_regex2.default)(p.regex.source)) {
|
|
798
|
+
throw new Error(
|
|
799
|
+
`[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
for (const re of SENSITIVE_PATH_PATTERNS) {
|
|
804
|
+
if (!(0, import_safe_regex2.default)(re.source)) {
|
|
805
|
+
throw new Error(
|
|
806
|
+
`[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
assertBuiltinPatternsAreSafe();
|
|
1488
812
|
function maskSecret(raw, pattern) {
|
|
1489
813
|
const match = raw.match(pattern);
|
|
1490
814
|
if (!match) return "****";
|
|
@@ -1549,104 +873,165 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1549
873
|
}
|
|
1550
874
|
return null;
|
|
1551
875
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
var
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
];
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
876
|
+
var { syntax } = import_mvdan_sh.default;
|
|
877
|
+
var sharedParser = syntax.NewParser();
|
|
878
|
+
var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
879
|
+
"-m",
|
|
880
|
+
"--message",
|
|
881
|
+
"--body",
|
|
882
|
+
"--title",
|
|
883
|
+
"--description",
|
|
884
|
+
"--comment",
|
|
885
|
+
"--subject",
|
|
886
|
+
"--summary"
|
|
887
|
+
]);
|
|
888
|
+
var SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
|
|
889
|
+
var DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
|
|
890
|
+
function normalizeCommandForPolicy(command) {
|
|
891
|
+
try {
|
|
892
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
893
|
+
const strips = [];
|
|
894
|
+
syntax.Walk(f, (node) => {
|
|
895
|
+
if (!node) return false;
|
|
896
|
+
const n = node;
|
|
897
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
898
|
+
const args = n.Args || [];
|
|
899
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
900
|
+
const argParts = args[i].Parts || [];
|
|
901
|
+
if (argParts.length !== 1 || syntax.NodeType(argParts[0]) !== "Lit") continue;
|
|
902
|
+
const flagVal = argParts[0].Value || "";
|
|
903
|
+
if (!MESSAGE_FLAGS.has(flagVal.toLowerCase())) continue;
|
|
904
|
+
const next = args[i + 1];
|
|
905
|
+
const nextParts = next.Parts || [];
|
|
906
|
+
if (nextParts.length !== 1) continue;
|
|
907
|
+
const quotedNode = nextParts[0];
|
|
908
|
+
const nt = syntax.NodeType(quotedNode);
|
|
909
|
+
if (nt === "SglQuoted") {
|
|
910
|
+
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
911
|
+
} else if (nt === "DblQuoted") {
|
|
912
|
+
const innerParts = quotedNode.Parts || [];
|
|
913
|
+
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
914
|
+
if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return true;
|
|
918
|
+
});
|
|
919
|
+
if (strips.length === 0) return command;
|
|
920
|
+
strips.sort((a, b) => b[0] - a[0]);
|
|
921
|
+
let result = command;
|
|
922
|
+
for (const [start, end] of strips) {
|
|
923
|
+
result = result.slice(0, start) + '""' + result.slice(end);
|
|
1577
924
|
}
|
|
925
|
+
return result;
|
|
926
|
+
} catch {
|
|
927
|
+
return command;
|
|
1578
928
|
}
|
|
1579
|
-
return null;
|
|
1580
929
|
}
|
|
1581
|
-
function
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
}
|
|
1596
|
-
if (USER_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
1597
|
-
return { trustLevel: "user", reason: "" };
|
|
930
|
+
function scanArgsForDynamicExec(args, startIdx) {
|
|
931
|
+
let hasCmdSubst = false;
|
|
932
|
+
let hasParamExp = false;
|
|
933
|
+
let hasCurl = false;
|
|
934
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
935
|
+
syntax.Walk(args[i], (inner) => {
|
|
936
|
+
if (!inner) return false;
|
|
937
|
+
const inn = inner;
|
|
938
|
+
const it = syntax.NodeType(inn);
|
|
939
|
+
if (it === "CmdSubst") hasCmdSubst = true;
|
|
940
|
+
if (it === "ParamExp") hasParamExp = true;
|
|
941
|
+
if (it === "Lit" && DOWNLOAD_CMDS.has(inn.Value?.toLowerCase())) hasCurl = true;
|
|
942
|
+
return true;
|
|
943
|
+
});
|
|
1598
944
|
}
|
|
1599
|
-
|
|
945
|
+
if (hasCmdSubst && hasCurl) return "block";
|
|
946
|
+
if (hasCmdSubst || hasParamExp) return "review";
|
|
947
|
+
return null;
|
|
1600
948
|
}
|
|
1601
|
-
function
|
|
1602
|
-
const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
|
|
1603
|
-
if (import_path5.default.posix.isAbsolute(bare)) {
|
|
1604
|
-
const early = _classifyPath(bare, cwd);
|
|
1605
|
-
if (early.trustLevel === "suspect") {
|
|
1606
|
-
return { resolvedPath: bare, ...early };
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
let resolved;
|
|
1610
|
-
try {
|
|
1611
|
-
const found = findInPath(bare);
|
|
1612
|
-
if (!found) {
|
|
1613
|
-
return {
|
|
1614
|
-
resolvedPath: cmd,
|
|
1615
|
-
trustLevel: "unknown",
|
|
1616
|
-
reason: "binary not found in PATH"
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
|
-
resolved = import_fs5.default.realpathSync(found);
|
|
1620
|
-
} catch {
|
|
1621
|
-
return {
|
|
1622
|
-
resolvedPath: cmd,
|
|
1623
|
-
trustLevel: "unknown",
|
|
1624
|
-
reason: "binary not found in PATH"
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
949
|
+
function detectDangerousShellExec(command) {
|
|
1627
950
|
try {
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
951
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
952
|
+
let result = null;
|
|
953
|
+
syntax.Walk(f, (node) => {
|
|
954
|
+
if (!node || result === "block") return false;
|
|
955
|
+
const n = node;
|
|
956
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
957
|
+
const args = n.Args || [];
|
|
958
|
+
if (args.length === 0) return true;
|
|
959
|
+
const firstParts = args[0].Parts || [];
|
|
960
|
+
if (firstParts.length !== 1 || syntax.NodeType(firstParts[0]) !== "Lit") return true;
|
|
961
|
+
const cmdName = firstParts[0].Value?.toLowerCase() ?? "";
|
|
962
|
+
if (cmdName === "eval") {
|
|
963
|
+
const v = scanArgsForDynamicExec(args, 1);
|
|
964
|
+
if (v === "block" || v === "review" && result === null) result = v;
|
|
965
|
+
} else if (SHELL_INTERPRETERS.has(cmdName)) {
|
|
966
|
+
for (let i = 1; i < args.length - 1; i++) {
|
|
967
|
+
const flagParts = args[i].Parts || [];
|
|
968
|
+
if (flagParts.length !== 1 || syntax.NodeType(flagParts[0]) !== "Lit" || flagParts[0].Value !== "-c")
|
|
969
|
+
continue;
|
|
970
|
+
const v = scanArgsForDynamicExec(args, i + 1);
|
|
971
|
+
if (v === "block" || v === "review" && result === null) result = v;
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return true;
|
|
976
|
+
});
|
|
977
|
+
return result;
|
|
1636
978
|
} catch {
|
|
1637
|
-
return
|
|
1638
|
-
resolvedPath: resolved,
|
|
1639
|
-
trustLevel: "unknown",
|
|
1640
|
-
reason: "could not stat binary"
|
|
1641
|
-
};
|
|
979
|
+
return null;
|
|
1642
980
|
}
|
|
1643
|
-
const classify = _classifyPath(resolved, cwd);
|
|
1644
|
-
return { resolvedPath: resolved, ...classify };
|
|
1645
981
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
982
|
+
function analyzeShellCommand(command) {
|
|
983
|
+
const actions = [];
|
|
984
|
+
const paths = [];
|
|
985
|
+
const allTokens = [];
|
|
986
|
+
const addToken = (token) => {
|
|
987
|
+
const lower = token.toLowerCase();
|
|
988
|
+
allTokens.push(lower);
|
|
989
|
+
if (lower.includes("/")) allTokens.push(...lower.split("/").filter(Boolean));
|
|
990
|
+
if (lower.startsWith("-")) allTokens.push(lower.replace(/^-+/, ""));
|
|
991
|
+
};
|
|
992
|
+
try {
|
|
993
|
+
const f = sharedParser.Parse(command, "cmd");
|
|
994
|
+
syntax.Walk(f, (node) => {
|
|
995
|
+
if (!node) return false;
|
|
996
|
+
const n = node;
|
|
997
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
998
|
+
const wordValues = (n.Args || []).map((arg) => {
|
|
999
|
+
return (arg.Parts || []).map((p) => (p.Value ?? "").replace(/\\(.)/g, "$1")).join("");
|
|
1000
|
+
}).filter((s) => s.length > 0);
|
|
1001
|
+
if (wordValues.length > 0) {
|
|
1002
|
+
const cmd = wordValues[0].toLowerCase();
|
|
1003
|
+
if (!actions.includes(cmd)) actions.push(cmd);
|
|
1004
|
+
wordValues.forEach((w) => addToken(w));
|
|
1005
|
+
wordValues.slice(1).forEach((w) => {
|
|
1006
|
+
if (!w.startsWith("-")) paths.push(w);
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
return true;
|
|
1010
|
+
});
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
1013
|
+
if (allTokens.length === 0) {
|
|
1014
|
+
const normalized = command.replace(/\\(.)/g, "$1");
|
|
1015
|
+
const sanitized = normalized.replace(/["'<>]/g, " ");
|
|
1016
|
+
const segments = sanitized.split(/[|;&]|\$\(|\)|`/);
|
|
1017
|
+
segments.forEach((segment) => {
|
|
1018
|
+
const tokens = segment.trim().split(/\s+/).filter(Boolean);
|
|
1019
|
+
if (tokens.length > 0) {
|
|
1020
|
+
const action = tokens[0].toLowerCase();
|
|
1021
|
+
if (!actions.includes(action)) actions.push(action);
|
|
1022
|
+
tokens.forEach((t) => {
|
|
1023
|
+
addToken(t);
|
|
1024
|
+
if (t !== tokens[0] && !t.startsWith("-")) {
|
|
1025
|
+
if (!paths.includes(t)) paths.push(t);
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
return { actions, paths, allTokens };
|
|
1032
|
+
}
|
|
1033
|
+
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1034
|
+
"cat",
|
|
1650
1035
|
"head",
|
|
1651
1036
|
"tail",
|
|
1652
1037
|
"grep",
|
|
@@ -1791,9 +1176,10 @@ function analyzePipeChain(command) {
|
|
|
1791
1176
|
risk
|
|
1792
1177
|
};
|
|
1793
1178
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1179
|
+
function basename(p) {
|
|
1180
|
+
const segments = p.split(/[\\/]/);
|
|
1181
|
+
return segments[segments.length - 1] || "";
|
|
1182
|
+
}
|
|
1797
1183
|
var FLAGS_WITH_VALUES = {
|
|
1798
1184
|
curl: /* @__PURE__ */ new Set([
|
|
1799
1185
|
"-H",
|
|
@@ -1861,7 +1247,7 @@ var FLAGS_WITH_VALUES = {
|
|
|
1861
1247
|
// socat uses address syntax, not flags — no value-flags
|
|
1862
1248
|
};
|
|
1863
1249
|
function extractPositionalArgs(tokens, binary) {
|
|
1864
|
-
const binaryName =
|
|
1250
|
+
const binaryName = basename(binary).replace(/\.exe$/i, "");
|
|
1865
1251
|
const flagsWithValues = FLAGS_WITH_VALUES[binaryName] ?? /* @__PURE__ */ new Set();
|
|
1866
1252
|
const positional = [];
|
|
1867
1253
|
let skipNext = false;
|
|
@@ -1896,8 +1282,6 @@ function extractNetworkTargets(tokens, binary) {
|
|
|
1896
1282
|
return t;
|
|
1897
1283
|
}).filter(Boolean);
|
|
1898
1284
|
}
|
|
1899
|
-
|
|
1900
|
-
// src/policy/ssh-parser.ts
|
|
1901
1285
|
function tokenize(cmd) {
|
|
1902
1286
|
const tokens = [];
|
|
1903
1287
|
let current = "";
|
|
@@ -1957,60 +1341,42 @@ function extractAllSshHosts(tokens) {
|
|
|
1957
1341
|
}
|
|
1958
1342
|
return [...hosts].filter(Boolean);
|
|
1959
1343
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
var
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
return import_path7.default.join(import_os5.default.homedir(), ".node9", "trusted-hosts.json");
|
|
1967
|
-
}
|
|
1968
|
-
function readTrustedHosts() {
|
|
1344
|
+
var MAX_REGEX_LENGTH = 100;
|
|
1345
|
+
var REGEX_CACHE_MAX = 500;
|
|
1346
|
+
var regexCache = /* @__PURE__ */ new Map();
|
|
1347
|
+
function validateRegex(pattern) {
|
|
1348
|
+
if (!pattern) return "Pattern is required";
|
|
1349
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
1969
1350
|
try {
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
return
|
|
1973
|
-
} catch {
|
|
1974
|
-
return [];
|
|
1351
|
+
new RegExp(pattern);
|
|
1352
|
+
} catch (e) {
|
|
1353
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
1975
1354
|
}
|
|
1355
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
1356
|
+
if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
1357
|
+
return null;
|
|
1976
1358
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1359
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
1360
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
1361
|
+
const key = `${pattern}\0${flags}`;
|
|
1362
|
+
if (regexCache.has(key)) {
|
|
1363
|
+
const cached = regexCache.get(key);
|
|
1364
|
+
regexCache.delete(key);
|
|
1365
|
+
regexCache.set(key, cached);
|
|
1366
|
+
return cached;
|
|
1367
|
+
}
|
|
1368
|
+
if (validateRegex(pattern) !== null) return null;
|
|
1980
1369
|
try {
|
|
1981
|
-
|
|
1370
|
+
const re = new RegExp(pattern, flags);
|
|
1371
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
1372
|
+
const oldest = regexCache.keys().next().value;
|
|
1373
|
+
if (oldest) regexCache.delete(oldest);
|
|
1374
|
+
}
|
|
1375
|
+
regexCache.set(key, re);
|
|
1376
|
+
return re;
|
|
1982
1377
|
} catch {
|
|
1983
|
-
return
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
function getCachedHosts() {
|
|
1987
|
-
const now = Date.now();
|
|
1988
|
-
if (_cache && now < _cache.expiry) {
|
|
1989
|
-
const mtime = getFileMtime();
|
|
1990
|
-
if (mtime === _cache.mtime) return _cache.hosts;
|
|
1378
|
+
return null;
|
|
1991
1379
|
}
|
|
1992
|
-
const hosts = readTrustedHosts();
|
|
1993
|
-
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1994
|
-
return hosts;
|
|
1995
|
-
}
|
|
1996
|
-
function normalizeHost(raw) {
|
|
1997
|
-
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
1998
|
-
}
|
|
1999
|
-
function isTrustedHost(host) {
|
|
2000
|
-
const normalized = normalizeHost(host);
|
|
2001
|
-
return getCachedHosts().some((entry) => {
|
|
2002
|
-
const entryHost = entry.host.toLowerCase();
|
|
2003
|
-
if (entryHost.startsWith("*.")) {
|
|
2004
|
-
const domain = entryHost.slice(2);
|
|
2005
|
-
return normalized.endsWith("." + domain);
|
|
2006
|
-
}
|
|
2007
|
-
return normalized === entryHost;
|
|
2008
|
-
});
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
// src/policy/index.ts
|
|
2012
|
-
function tokenize2(toolName) {
|
|
2013
|
-
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
2014
1380
|
}
|
|
2015
1381
|
function matchesPattern(text, patterns) {
|
|
2016
1382
|
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
@@ -2022,444 +1388,1832 @@ function matchesPattern(text, patterns) {
|
|
|
2022
1388
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2023
1389
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2024
1390
|
}
|
|
2025
|
-
|
|
1391
|
+
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1392
|
+
function getNestedValue(obj, path13) {
|
|
2026
1393
|
if (!obj || typeof obj !== "object") return null;
|
|
2027
|
-
|
|
1394
|
+
const segments = path13.split(".");
|
|
1395
|
+
for (const seg of segments) {
|
|
1396
|
+
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1397
|
+
}
|
|
1398
|
+
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
2028
1399
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
for (let i = 0; i < args.length - 1; i++) {
|
|
2051
|
-
const argParts = args[i].Parts || [];
|
|
2052
|
-
if (argParts.length !== 1 || syntax.NodeType(argParts[0]) !== "Lit") continue;
|
|
2053
|
-
const flagVal = argParts[0].Value || "";
|
|
2054
|
-
if (!MESSAGE_FLAGS.has(flagVal.toLowerCase())) continue;
|
|
2055
|
-
const next = args[i + 1];
|
|
2056
|
-
const nextParts = next.Parts || [];
|
|
2057
|
-
if (nextParts.length !== 1) continue;
|
|
2058
|
-
const quotedNode = nextParts[0];
|
|
2059
|
-
const nt = syntax.NodeType(quotedNode);
|
|
2060
|
-
if (nt === "SglQuoted") {
|
|
2061
|
-
strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
2062
|
-
} else if (nt === "DblQuoted") {
|
|
2063
|
-
const innerParts = quotedNode.Parts || [];
|
|
2064
|
-
const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
|
|
2065
|
-
if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
|
|
2066
|
-
}
|
|
1400
|
+
function evaluateSmartConditions(args, rule) {
|
|
1401
|
+
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
1402
|
+
const mode = rule.conditionMode ?? "all";
|
|
1403
|
+
const results = rule.conditions.map((cond) => {
|
|
1404
|
+
const rawVal = getNestedValue(args, cond.field);
|
|
1405
|
+
const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
|
|
1406
|
+
const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
|
|
1407
|
+
switch (cond.op) {
|
|
1408
|
+
case "exists":
|
|
1409
|
+
return val !== null && val !== "";
|
|
1410
|
+
case "notExists":
|
|
1411
|
+
return val === null || val === "";
|
|
1412
|
+
case "contains":
|
|
1413
|
+
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
1414
|
+
case "notContains":
|
|
1415
|
+
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
1416
|
+
case "matches": {
|
|
1417
|
+
if (val === null || !cond.value) return false;
|
|
1418
|
+
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1419
|
+
if (!reM) return false;
|
|
1420
|
+
return reM.test(val);
|
|
2067
1421
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
1422
|
+
case "notMatches": {
|
|
1423
|
+
if (!cond.value) return false;
|
|
1424
|
+
if (val === null) return true;
|
|
1425
|
+
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1426
|
+
if (!reN) return false;
|
|
1427
|
+
return !reN.test(val);
|
|
1428
|
+
}
|
|
1429
|
+
case "matchesGlob":
|
|
1430
|
+
return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
|
|
1431
|
+
case "notMatchesGlob":
|
|
1432
|
+
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
|
|
1433
|
+
default:
|
|
1434
|
+
return false;
|
|
2075
1435
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
1436
|
+
});
|
|
1437
|
+
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
1438
|
+
}
|
|
1439
|
+
function tokenize2(toolName) {
|
|
1440
|
+
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
1441
|
+
}
|
|
1442
|
+
function extractShellCommand(toolName, args, toolInspection) {
|
|
1443
|
+
const patterns = Object.keys(toolInspection);
|
|
1444
|
+
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
1445
|
+
if (!matchingPattern) return null;
|
|
1446
|
+
const fieldPath = toolInspection[matchingPattern];
|
|
1447
|
+
const value = getNestedValue(args, fieldPath);
|
|
1448
|
+
return typeof value === "string" ? value : null;
|
|
1449
|
+
}
|
|
1450
|
+
function isSqlTool(toolName, toolInspection) {
|
|
1451
|
+
const patterns = Object.keys(toolInspection);
|
|
1452
|
+
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
1453
|
+
if (!matchingPattern) return false;
|
|
1454
|
+
const fieldName = toolInspection[matchingPattern];
|
|
1455
|
+
return fieldName === "sql" || fieldName === "query";
|
|
1456
|
+
}
|
|
1457
|
+
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
1458
|
+
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
1459
|
+
const { agent, cwd, activeEnvironment } = context;
|
|
1460
|
+
const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
|
|
1461
|
+
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
1462
|
+
if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
|
|
1463
|
+
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
1464
|
+
if (dlpMatch) {
|
|
1465
|
+
return {
|
|
1466
|
+
decision: dlpMatch.severity,
|
|
1467
|
+
blockedByLabel: `DLP: ${dlpMatch.patternName}`,
|
|
1468
|
+
reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (wouldBeIgnored) return { decision: "allow" };
|
|
1473
|
+
if (config.policy.smartRules.length > 0) {
|
|
1474
|
+
const matchedRule = config.policy.smartRules.find(
|
|
1475
|
+
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
1476
|
+
);
|
|
1477
|
+
if (matchedRule) {
|
|
1478
|
+
if (matchedRule.verdict === "allow")
|
|
1479
|
+
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
1480
|
+
return {
|
|
1481
|
+
decision: matchedRule.verdict,
|
|
1482
|
+
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
1483
|
+
reason: matchedRule.reason,
|
|
1484
|
+
tier: 2,
|
|
1485
|
+
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1486
|
+
...(matchedRule.description ?? matchedRule.reason) && {
|
|
1487
|
+
ruleDescription: matchedRule.description ?? matchedRule.reason
|
|
1488
|
+
},
|
|
1489
|
+
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1490
|
+
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1491
|
+
},
|
|
1492
|
+
...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
|
|
1493
|
+
recoveryCommand: matchedRule.recoveryCommand
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
let allTokens = [];
|
|
1499
|
+
let pathTokens = [];
|
|
1500
|
+
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
1501
|
+
if (shellCommand) {
|
|
1502
|
+
const analyzed = analyzeShellCommand(shellCommand);
|
|
1503
|
+
allTokens = analyzed.allTokens;
|
|
1504
|
+
pathTokens = analyzed.paths;
|
|
1505
|
+
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
1506
|
+
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
1507
|
+
return {
|
|
1508
|
+
decision: "review",
|
|
1509
|
+
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
1510
|
+
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
1511
|
+
tier: 3
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
1515
|
+
if (evalVerdict === "block") {
|
|
1516
|
+
return {
|
|
1517
|
+
decision: "block",
|
|
1518
|
+
blockedByLabel: "Node9: Eval Remote Execution",
|
|
1519
|
+
reason: "eval of remote download (curl/wget) is a near-certain supply-chain attack",
|
|
1520
|
+
ruleDescription: "The AI is downloading a script from the internet and running it immediately without inspection. This is a common way malware gets installed.",
|
|
1521
|
+
tier: 3
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
if (evalVerdict === "review") {
|
|
1525
|
+
return {
|
|
1526
|
+
decision: "review",
|
|
1527
|
+
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
1528
|
+
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
1529
|
+
ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
|
|
1530
|
+
tier: 3
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
const pipeAnalysis = analyzePipeChain(shellCommand);
|
|
1534
|
+
if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
|
|
1535
|
+
const sinks = pipeAnalysis.sinkTargets;
|
|
1536
|
+
const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
|
|
1537
|
+
if (pipeAnalysis.risk === "critical") {
|
|
1538
|
+
if (allTrusted) {
|
|
1539
|
+
return {
|
|
1540
|
+
decision: "review",
|
|
1541
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
1542
|
+
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
1543
|
+
tier: 3
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
return {
|
|
1547
|
+
decision: "block",
|
|
1548
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1549
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1550
|
+
tier: 3
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
if (allTrusted) {
|
|
1554
|
+
return {
|
|
1555
|
+
decision: "allow",
|
|
1556
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1557
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1558
|
+
tier: 3
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
return {
|
|
1562
|
+
decision: "review",
|
|
1563
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1564
|
+
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1565
|
+
tier: 3
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
const firstToken = analyzed.actions[0] ?? "";
|
|
1569
|
+
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
1570
|
+
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
1571
|
+
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
1572
|
+
allTokens.push(...sshHosts);
|
|
1573
|
+
}
|
|
1574
|
+
if (firstToken && firstToken.startsWith("/") && checkProvenance2) {
|
|
1575
|
+
const prov = checkProvenance2(firstToken, cwd);
|
|
1576
|
+
if (prov.trustLevel === "suspect") {
|
|
1577
|
+
return {
|
|
1578
|
+
decision: config.settings.mode === "strict" ? "block" : "review",
|
|
1579
|
+
blockedByLabel: "Node9: Suspect Binary",
|
|
1580
|
+
reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
|
|
1581
|
+
tier: 3
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
if (prov.trustLevel === "unknown" && config.settings.mode === "strict") {
|
|
1585
|
+
return {
|
|
1586
|
+
decision: "review",
|
|
1587
|
+
blockedByLabel: "Node9: Unknown Binary (strict mode)",
|
|
1588
|
+
reason: `Binary "${firstToken}" \u2014 ${prov.reason}`,
|
|
1589
|
+
tier: 3
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
1594
|
+
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1595
|
+
}
|
|
1596
|
+
} else {
|
|
1597
|
+
allTokens = tokenize2(toolName);
|
|
1598
|
+
if (args && typeof args === "object") {
|
|
1599
|
+
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
1600
|
+
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
1601
|
+
allTokens.push(...extraTokens);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const isManual = agent === "Terminal";
|
|
1605
|
+
if (isManual) {
|
|
1606
|
+
const SYSTEM_DISASTER_COMMANDS = ["mkfs", "shred", "dd", "drop", "truncate", "purge"];
|
|
1607
|
+
const hasSystemDisaster = allTokens.some(
|
|
1608
|
+
(t) => SYSTEM_DISASTER_COMMANDS.includes(t.toLowerCase())
|
|
1609
|
+
);
|
|
1610
|
+
const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
|
|
1611
|
+
if (hasSystemDisaster || isRootWipe) {
|
|
1612
|
+
return { decision: "review", blockedByLabel: "Manual Nuclear Protection", tier: 3 };
|
|
1613
|
+
}
|
|
1614
|
+
return { decision: "allow" };
|
|
1615
|
+
}
|
|
1616
|
+
if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
|
|
1617
|
+
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
1618
|
+
if (allInSandbox) return { decision: "allow" };
|
|
1619
|
+
}
|
|
1620
|
+
let matchedDangerousWord;
|
|
1621
|
+
const isDangerous = allTokens.some(
|
|
1622
|
+
(token) => config.policy.dangerousWords.some((word) => {
|
|
1623
|
+
const w = word.toLowerCase();
|
|
1624
|
+
const hit = token === w || (() => {
|
|
1625
|
+
try {
|
|
1626
|
+
return new RegExp(`\\b${w}\\b`, "i").test(token);
|
|
1627
|
+
} catch {
|
|
1628
|
+
return false;
|
|
1629
|
+
}
|
|
1630
|
+
})();
|
|
1631
|
+
if (hit && !matchedDangerousWord) matchedDangerousWord = word;
|
|
1632
|
+
return hit;
|
|
1633
|
+
})
|
|
1634
|
+
);
|
|
1635
|
+
if (isDangerous) {
|
|
1636
|
+
let matchedField;
|
|
1637
|
+
if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
|
|
1638
|
+
const obj = args;
|
|
1639
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1640
|
+
if (typeof value === "string") {
|
|
1641
|
+
try {
|
|
1642
|
+
if (new RegExp(
|
|
1643
|
+
`\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
1644
|
+
"i"
|
|
1645
|
+
).test(value)) {
|
|
1646
|
+
matchedField = key;
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
} catch {
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
decision: "review",
|
|
1656
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
|
|
1657
|
+
matchedWord: matchedDangerousWord,
|
|
1658
|
+
matchedField,
|
|
1659
|
+
ruleDescription: `This command contains a flagged keyword ("${matchedDangerousWord}") from your node9 config. Review it before allowing.`,
|
|
1660
|
+
tier: 6
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
if (config.settings.mode === "strict") {
|
|
1664
|
+
if (activeEnvironment?.requireApproval === false) return { decision: "allow" };
|
|
1665
|
+
return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)", tier: 7 };
|
|
1666
|
+
}
|
|
1667
|
+
return { decision: "allow" };
|
|
1668
|
+
}
|
|
1669
|
+
function isIgnoredTool(toolName, config) {
|
|
1670
|
+
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
1671
|
+
}
|
|
1672
|
+
var aws_default = {
|
|
1673
|
+
name: "aws",
|
|
1674
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
1675
|
+
aliases: ["amazon"],
|
|
1676
|
+
smartRules: [
|
|
1677
|
+
{
|
|
1678
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
1679
|
+
tool: "*",
|
|
1680
|
+
conditions: [
|
|
1681
|
+
{
|
|
1682
|
+
field: "command",
|
|
1683
|
+
op: "matches",
|
|
1684
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
1685
|
+
flags: "i"
|
|
1686
|
+
}
|
|
1687
|
+
],
|
|
1688
|
+
verdict: "block",
|
|
1689
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
name: "shield:aws:review-iam-changes",
|
|
1693
|
+
tool: "*",
|
|
1694
|
+
conditions: [
|
|
1695
|
+
{
|
|
1696
|
+
field: "command",
|
|
1697
|
+
op: "matches",
|
|
1698
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
1699
|
+
flags: "i"
|
|
1700
|
+
}
|
|
1701
|
+
],
|
|
1702
|
+
verdict: "review",
|
|
1703
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
name: "shield:aws:block-ec2-terminate",
|
|
1707
|
+
tool: "*",
|
|
1708
|
+
conditions: [
|
|
1709
|
+
{
|
|
1710
|
+
field: "command",
|
|
1711
|
+
op: "matches",
|
|
1712
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
1713
|
+
flags: "i"
|
|
1714
|
+
}
|
|
1715
|
+
],
|
|
1716
|
+
verdict: "block",
|
|
1717
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
name: "shield:aws:review-rds-delete",
|
|
1721
|
+
tool: "*",
|
|
1722
|
+
conditions: [
|
|
1723
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
1724
|
+
],
|
|
1725
|
+
verdict: "review",
|
|
1726
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
1727
|
+
}
|
|
1728
|
+
],
|
|
1729
|
+
dangerousWords: []
|
|
1730
|
+
};
|
|
1731
|
+
var bash_safe_default = {
|
|
1732
|
+
name: "bash-safe",
|
|
1733
|
+
description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
|
|
1734
|
+
aliases: ["bash", "shell"],
|
|
1735
|
+
smartRules: [
|
|
1736
|
+
{
|
|
1737
|
+
name: "shield:bash-safe:block-pipe-to-shell",
|
|
1738
|
+
tool: "bash",
|
|
1739
|
+
conditions: [
|
|
1740
|
+
{
|
|
1741
|
+
field: "command",
|
|
1742
|
+
op: "matches",
|
|
1743
|
+
value: "(^|&&|\\|\\||;)\\s*(curl|wget)\\s+[^|]*\\|\\s*(?:(bash|sh|zsh|fish)|(python3?|ruby|perl|node)\\b(?!\\s+-[cem]\\b))",
|
|
1744
|
+
flags: "i"
|
|
1745
|
+
}
|
|
1746
|
+
],
|
|
1747
|
+
verdict: "block",
|
|
1748
|
+
reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
name: "shield:bash-safe:block-obfuscated-exec",
|
|
1752
|
+
tool: "bash",
|
|
1753
|
+
conditions: [
|
|
1754
|
+
{
|
|
1755
|
+
field: "command",
|
|
1756
|
+
op: "matches",
|
|
1757
|
+
value: "\\bbase64\\s+(-d|--decode)[^|;&]*\\|\\s*(bash|sh|zsh)",
|
|
1758
|
+
flags: "i"
|
|
1759
|
+
}
|
|
1760
|
+
],
|
|
1761
|
+
verdict: "block",
|
|
1762
|
+
reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
|
|
1763
|
+
},
|
|
1764
|
+
{
|
|
1765
|
+
name: "shield:bash-safe:block-rm-root",
|
|
1766
|
+
tool: "bash",
|
|
1767
|
+
conditions: [
|
|
1768
|
+
{
|
|
1769
|
+
field: "command",
|
|
1770
|
+
op: "matches",
|
|
1771
|
+
value: "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\\s+(\\/|~|\\$HOME|\\$\\{HOME\\})\\s*$",
|
|
1772
|
+
flags: "i"
|
|
1773
|
+
}
|
|
1774
|
+
],
|
|
1775
|
+
verdict: "block",
|
|
1776
|
+
reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
name: "shield:bash-safe:block-disk-overwrite",
|
|
1780
|
+
tool: "bash",
|
|
1781
|
+
conditions: [
|
|
1782
|
+
{
|
|
1783
|
+
field: "command",
|
|
1784
|
+
op: "matches",
|
|
1785
|
+
value: "(^|&&|\\|\\||;)\\s*dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
|
|
1786
|
+
flags: "i"
|
|
1787
|
+
}
|
|
1788
|
+
],
|
|
1789
|
+
verdict: "block",
|
|
1790
|
+
reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
|
|
1791
|
+
},
|
|
1792
|
+
{
|
|
1793
|
+
name: "shield:bash-safe:block-eval-remote",
|
|
1794
|
+
tool: "bash",
|
|
1795
|
+
conditions: [
|
|
1796
|
+
{
|
|
1797
|
+
field: "command",
|
|
1798
|
+
op: "matches",
|
|
1799
|
+
value: "(^|&&|\\|\\||;)\\s*eval\\s+.*\\$\\((curl|wget)\\b",
|
|
1800
|
+
flags: "i"
|
|
1801
|
+
}
|
|
1802
|
+
],
|
|
1803
|
+
verdict: "block",
|
|
1804
|
+
reason: "eval of remote download is a near-certain supply-chain attack \u2014 blocked by bash-safe shield"
|
|
1805
|
+
},
|
|
1806
|
+
{
|
|
1807
|
+
name: "shield:bash-safe:review-eval-dynamic",
|
|
1808
|
+
tool: "bash",
|
|
1809
|
+
conditions: [
|
|
1810
|
+
{
|
|
1811
|
+
field: "command",
|
|
1812
|
+
op: "matches",
|
|
1813
|
+
value: '(^|&&|\\|\\||[;|\\n{(`])\\s*eval\\s+([\\$`(]|"[^"]*\\$)',
|
|
1814
|
+
flags: "i"
|
|
1815
|
+
}
|
|
1816
|
+
],
|
|
1817
|
+
verdict: "review",
|
|
1818
|
+
reason: "eval of dynamic content \u2014 backup regex rule for scan path (real-time uses AST detection)"
|
|
1819
|
+
}
|
|
1820
|
+
],
|
|
1821
|
+
dangerousWords: []
|
|
1822
|
+
};
|
|
1823
|
+
var docker_default = {
|
|
1824
|
+
name: "docker",
|
|
1825
|
+
description: "Protects Docker environments from destructive AI operations",
|
|
1826
|
+
aliases: [],
|
|
1827
|
+
smartRules: [
|
|
1828
|
+
{
|
|
1829
|
+
name: "shield:docker:block-system-prune",
|
|
1830
|
+
tool: "*",
|
|
1831
|
+
conditions: [
|
|
1832
|
+
{
|
|
1833
|
+
field: "command",
|
|
1834
|
+
op: "matches",
|
|
1835
|
+
value: "docker\\s+system\\s+prune",
|
|
1836
|
+
flags: "i"
|
|
1837
|
+
}
|
|
1838
|
+
],
|
|
1839
|
+
verdict: "block",
|
|
1840
|
+
reason: "docker system prune removes all unused containers, images, and volumes \u2014 blocked by Docker shield"
|
|
1841
|
+
},
|
|
1842
|
+
{
|
|
1843
|
+
name: "shield:docker:block-volume-prune",
|
|
1844
|
+
tool: "*",
|
|
1845
|
+
conditions: [
|
|
1846
|
+
{
|
|
1847
|
+
field: "command",
|
|
1848
|
+
op: "matches",
|
|
1849
|
+
value: "docker\\s+volume\\s+prune",
|
|
1850
|
+
flags: "i"
|
|
1851
|
+
}
|
|
1852
|
+
],
|
|
1853
|
+
verdict: "block",
|
|
1854
|
+
reason: "docker volume prune destroys all unused volumes and their data \u2014 blocked by Docker shield"
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
name: "shield:docker:block-rm-force",
|
|
1858
|
+
tool: "*",
|
|
1859
|
+
conditionMode: "all",
|
|
1860
|
+
conditions: [
|
|
1861
|
+
{
|
|
1862
|
+
field: "command",
|
|
1863
|
+
op: "matches",
|
|
1864
|
+
value: "docker\\s+rm\\b",
|
|
1865
|
+
flags: "i"
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
field: "command",
|
|
1869
|
+
op: "matches",
|
|
1870
|
+
value: "(^|\\s)(-f|--force)(\\s|$)",
|
|
1871
|
+
flags: "i"
|
|
1872
|
+
}
|
|
1873
|
+
],
|
|
1874
|
+
verdict: "block",
|
|
1875
|
+
reason: "Force-removing running containers is destructive \u2014 blocked by Docker shield"
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
name: "shield:docker:review-volume-rm",
|
|
1879
|
+
tool: "*",
|
|
1880
|
+
conditions: [
|
|
1881
|
+
{
|
|
1882
|
+
field: "command",
|
|
1883
|
+
op: "matches",
|
|
1884
|
+
value: "docker\\s+volume\\s+rm\\s+",
|
|
1885
|
+
flags: "i"
|
|
1886
|
+
}
|
|
1887
|
+
],
|
|
1888
|
+
verdict: "review",
|
|
1889
|
+
reason: "Volume removal deletes persistent data and requires human approval (Docker shield)"
|
|
1890
|
+
},
|
|
1891
|
+
{
|
|
1892
|
+
name: "shield:docker:review-stop-kill",
|
|
1893
|
+
tool: "*",
|
|
1894
|
+
conditions: [
|
|
1895
|
+
{
|
|
1896
|
+
field: "command",
|
|
1897
|
+
op: "matches",
|
|
1898
|
+
value: "docker\\s+(stop|kill)\\s+",
|
|
1899
|
+
flags: "i"
|
|
1900
|
+
}
|
|
1901
|
+
],
|
|
1902
|
+
verdict: "review",
|
|
1903
|
+
reason: "Stopping or killing containers requires human approval (Docker shield)"
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
name: "shield:docker:review-image-rm",
|
|
1907
|
+
tool: "*",
|
|
1908
|
+
conditions: [
|
|
1909
|
+
{
|
|
1910
|
+
field: "command",
|
|
1911
|
+
op: "matches",
|
|
1912
|
+
value: "docker\\s+image\\s+rm\\b",
|
|
1913
|
+
flags: "i"
|
|
1914
|
+
}
|
|
1915
|
+
],
|
|
1916
|
+
verdict: "review",
|
|
1917
|
+
reason: "Image removal requires human approval (Docker shield)"
|
|
1918
|
+
},
|
|
1919
|
+
{
|
|
1920
|
+
name: "shield:docker:review-rmi-force",
|
|
1921
|
+
tool: "*",
|
|
1922
|
+
conditionMode: "all",
|
|
1923
|
+
conditions: [
|
|
1924
|
+
{
|
|
1925
|
+
field: "command",
|
|
1926
|
+
op: "matches",
|
|
1927
|
+
value: "docker\\s+rmi\\b",
|
|
1928
|
+
flags: "i"
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
field: "command",
|
|
1932
|
+
op: "matches",
|
|
1933
|
+
value: "(^|\\s)(-f|--force)(\\s|$)",
|
|
1934
|
+
flags: "i"
|
|
1935
|
+
}
|
|
1936
|
+
],
|
|
1937
|
+
verdict: "review",
|
|
1938
|
+
reason: "Force image removal requires human approval (Docker shield)"
|
|
1939
|
+
}
|
|
1940
|
+
],
|
|
1941
|
+
dangerousWords: []
|
|
1942
|
+
};
|
|
1943
|
+
var filesystem_default = {
|
|
1944
|
+
name: "filesystem",
|
|
1945
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
1946
|
+
aliases: ["fs"],
|
|
1947
|
+
smartRules: [
|
|
1948
|
+
{
|
|
1949
|
+
name: "shield:filesystem:review-chmod-777",
|
|
1950
|
+
tool: "bash",
|
|
1951
|
+
conditions: [
|
|
1952
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
1953
|
+
],
|
|
1954
|
+
verdict: "review",
|
|
1955
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
name: "shield:filesystem:review-write-etc",
|
|
1959
|
+
tool: "bash",
|
|
1960
|
+
conditions: [
|
|
1961
|
+
{
|
|
1962
|
+
field: "command",
|
|
1963
|
+
op: "matches",
|
|
1964
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
1965
|
+
}
|
|
1966
|
+
],
|
|
1967
|
+
verdict: "review",
|
|
1968
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
1969
|
+
}
|
|
1970
|
+
],
|
|
1971
|
+
dangerousWords: ["wipefs"]
|
|
1972
|
+
};
|
|
1973
|
+
var github_default = {
|
|
1974
|
+
name: "github",
|
|
1975
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
1976
|
+
aliases: ["git"],
|
|
1977
|
+
smartRules: [
|
|
1978
|
+
{
|
|
1979
|
+
name: "shield:github:review-delete-branch-remote",
|
|
1980
|
+
tool: "bash",
|
|
1981
|
+
conditions: [
|
|
1982
|
+
{ field: "command", op: "matches", value: "git\\s+push\\s+.*--delete", flags: "i" }
|
|
1983
|
+
],
|
|
1984
|
+
verdict: "review",
|
|
1985
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
1986
|
+
},
|
|
1987
|
+
{
|
|
1988
|
+
name: "shield:github:block-delete-repo",
|
|
1989
|
+
tool: "*",
|
|
1990
|
+
conditions: [
|
|
1991
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
1992
|
+
],
|
|
1993
|
+
verdict: "block",
|
|
1994
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
1995
|
+
}
|
|
1996
|
+
],
|
|
1997
|
+
dangerousWords: []
|
|
1998
|
+
};
|
|
1999
|
+
var k8s_default = {
|
|
2000
|
+
name: "k8s",
|
|
2001
|
+
description: "Protects Kubernetes clusters from destructive AI operations",
|
|
2002
|
+
aliases: ["kubernetes", "kubectl"],
|
|
2003
|
+
smartRules: [
|
|
2004
|
+
{
|
|
2005
|
+
name: "shield:k8s:block-delete-namespace",
|
|
2006
|
+
tool: "*",
|
|
2007
|
+
conditions: [
|
|
2008
|
+
{
|
|
2009
|
+
field: "command",
|
|
2010
|
+
op: "matches",
|
|
2011
|
+
value: "kubectl\\s+delete\\s+(ns|namespace)\\s+",
|
|
2012
|
+
flags: "i"
|
|
2013
|
+
}
|
|
2014
|
+
],
|
|
2015
|
+
verdict: "block",
|
|
2016
|
+
reason: "Deleting a namespace destroys all resources inside it \u2014 blocked by k8s shield"
|
|
2017
|
+
},
|
|
2018
|
+
{
|
|
2019
|
+
name: "shield:k8s:block-delete-all",
|
|
2020
|
+
tool: "*",
|
|
2021
|
+
conditions: [
|
|
2022
|
+
{
|
|
2023
|
+
field: "command",
|
|
2024
|
+
op: "matches",
|
|
2025
|
+
value: "kubectl\\s+delete\\s+.*--all\\b",
|
|
2026
|
+
flags: "i"
|
|
2027
|
+
}
|
|
2028
|
+
],
|
|
2029
|
+
verdict: "block",
|
|
2030
|
+
reason: "kubectl delete --all is irreversible \u2014 blocked by k8s shield"
|
|
2031
|
+
},
|
|
2032
|
+
{
|
|
2033
|
+
name: "shield:k8s:block-helm-uninstall",
|
|
2034
|
+
tool: "*",
|
|
2035
|
+
conditions: [
|
|
2036
|
+
{
|
|
2037
|
+
field: "command",
|
|
2038
|
+
op: "matches",
|
|
2039
|
+
value: "helm\\s+(uninstall|delete|del)\\s+",
|
|
2040
|
+
flags: "i"
|
|
2041
|
+
}
|
|
2042
|
+
],
|
|
2043
|
+
verdict: "block",
|
|
2044
|
+
reason: "helm uninstall removes a release and its resources \u2014 blocked by k8s shield"
|
|
2045
|
+
},
|
|
2046
|
+
{
|
|
2047
|
+
name: "shield:k8s:review-scale-zero",
|
|
2048
|
+
tool: "*",
|
|
2049
|
+
conditions: [
|
|
2050
|
+
{
|
|
2051
|
+
field: "command",
|
|
2052
|
+
op: "matches",
|
|
2053
|
+
value: "kubectl\\s+scale\\s+.*--replicas=0",
|
|
2054
|
+
flags: "i"
|
|
2055
|
+
}
|
|
2056
|
+
],
|
|
2057
|
+
verdict: "review",
|
|
2058
|
+
reason: "Scaling to zero takes down a workload and requires human approval (k8s shield)"
|
|
2059
|
+
},
|
|
2060
|
+
{
|
|
2061
|
+
name: "shield:k8s:review-delete-deployment",
|
|
2062
|
+
tool: "*",
|
|
2063
|
+
conditions: [
|
|
2064
|
+
{
|
|
2065
|
+
field: "command",
|
|
2066
|
+
op: "matches",
|
|
2067
|
+
value: "kubectl\\s+delete\\s+(deployment|deploy|statefulset|sts|daemonset|ds)\\s+",
|
|
2068
|
+
flags: "i"
|
|
2069
|
+
}
|
|
2070
|
+
],
|
|
2071
|
+
verdict: "review",
|
|
2072
|
+
reason: "Deleting a workload requires human approval (k8s shield)"
|
|
2073
|
+
},
|
|
2074
|
+
{
|
|
2075
|
+
name: "shield:k8s:review-apply-force",
|
|
2076
|
+
tool: "*",
|
|
2077
|
+
conditions: [
|
|
2078
|
+
{
|
|
2079
|
+
field: "command",
|
|
2080
|
+
op: "matches",
|
|
2081
|
+
value: "kubectl\\s+(apply|replace)\\s+.*--force",
|
|
2082
|
+
flags: "i"
|
|
2083
|
+
}
|
|
2084
|
+
],
|
|
2085
|
+
verdict: "review",
|
|
2086
|
+
reason: "Force-apply overwrites live resources and requires human approval (k8s shield)"
|
|
2087
|
+
}
|
|
2088
|
+
],
|
|
2089
|
+
dangerousWords: []
|
|
2090
|
+
};
|
|
2091
|
+
var mcp_tool_gating_default = {
|
|
2092
|
+
name: "mcp-tool-gating",
|
|
2093
|
+
description: "Intercept MCP tool lists and require user approval before the agent can use any tools from a new server",
|
|
2094
|
+
aliases: ["mcp-gating", "mcp-tools"],
|
|
2095
|
+
smartRules: [],
|
|
2096
|
+
dangerousWords: []
|
|
2097
|
+
};
|
|
2098
|
+
var mongodb_default = {
|
|
2099
|
+
name: "mongodb",
|
|
2100
|
+
description: "Protects MongoDB databases from destructive AI operations",
|
|
2101
|
+
aliases: ["mongo"],
|
|
2102
|
+
smartRules: [
|
|
2103
|
+
{
|
|
2104
|
+
name: "shield:mongodb:block-drop-database",
|
|
2105
|
+
tool: "*",
|
|
2106
|
+
conditions: [
|
|
2107
|
+
{
|
|
2108
|
+
field: "command",
|
|
2109
|
+
op: "matches",
|
|
2110
|
+
value: "\\.dropDatabase\\s*\\(",
|
|
2111
|
+
flags: "i"
|
|
2112
|
+
}
|
|
2113
|
+
],
|
|
2114
|
+
verdict: "block",
|
|
2115
|
+
reason: "dropDatabase is irreversible \u2014 blocked by MongoDB shield"
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
name: "shield:mongodb:block-drop-collection",
|
|
2119
|
+
tool: "*",
|
|
2120
|
+
conditions: [
|
|
2121
|
+
{
|
|
2122
|
+
field: "command",
|
|
2123
|
+
op: "matches",
|
|
2124
|
+
value: "\\.drop\\s*\\(|db\\.getCollection\\([^)]+\\)\\.drop\\s*\\(",
|
|
2125
|
+
flags: "i"
|
|
2126
|
+
}
|
|
2127
|
+
],
|
|
2128
|
+
verdict: "block",
|
|
2129
|
+
reason: "Collection drop is irreversible \u2014 blocked by MongoDB shield"
|
|
2130
|
+
},
|
|
2131
|
+
{
|
|
2132
|
+
name: "shield:mongodb:block-delete-many-empty-filter",
|
|
2133
|
+
tool: "*",
|
|
2134
|
+
conditions: [
|
|
2135
|
+
{
|
|
2136
|
+
field: "command",
|
|
2137
|
+
op: "matches",
|
|
2138
|
+
value: "\\.deleteMany\\s*\\(\\s*\\{\\s*\\}\\s*\\)",
|
|
2139
|
+
flags: "i"
|
|
2140
|
+
}
|
|
2141
|
+
],
|
|
2142
|
+
verdict: "block",
|
|
2143
|
+
reason: "deleteMany({}) with empty filter wipes the entire collection \u2014 blocked by MongoDB shield"
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
name: "shield:mongodb:review-delete-many",
|
|
2147
|
+
tool: "*",
|
|
2148
|
+
conditions: [
|
|
2149
|
+
{
|
|
2150
|
+
field: "command",
|
|
2151
|
+
op: "matches",
|
|
2152
|
+
value: "\\.deleteMany\\s*\\(",
|
|
2153
|
+
flags: "i"
|
|
2154
|
+
}
|
|
2155
|
+
],
|
|
2156
|
+
verdict: "review",
|
|
2157
|
+
reason: "deleteMany requires human approval (MongoDB shield)"
|
|
2158
|
+
},
|
|
2159
|
+
{
|
|
2160
|
+
name: "shield:mongodb:review-drop-index",
|
|
2161
|
+
tool: "*",
|
|
2162
|
+
conditions: [
|
|
2163
|
+
{
|
|
2164
|
+
field: "command",
|
|
2165
|
+
op: "matches",
|
|
2166
|
+
value: "\\.dropIndex\\s*\\(|\\.dropIndexes\\s*\\(",
|
|
2167
|
+
flags: "i"
|
|
2168
|
+
}
|
|
2169
|
+
],
|
|
2170
|
+
verdict: "review",
|
|
2171
|
+
reason: "Index drops affect query performance and require human approval (MongoDB shield)"
|
|
2172
|
+
}
|
|
2173
|
+
],
|
|
2174
|
+
dangerousWords: ["dropDatabase", "dropCollection", "mongodrop"]
|
|
2175
|
+
};
|
|
2176
|
+
var postgres_default = {
|
|
2177
|
+
name: "postgres",
|
|
2178
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
2179
|
+
aliases: ["pg", "postgresql"],
|
|
2180
|
+
smartRules: [
|
|
2181
|
+
{
|
|
2182
|
+
name: "shield:postgres:block-drop-table",
|
|
2183
|
+
tool: "*",
|
|
2184
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
2185
|
+
verdict: "block",
|
|
2186
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
2187
|
+
},
|
|
2188
|
+
{
|
|
2189
|
+
name: "shield:postgres:block-truncate",
|
|
2190
|
+
tool: "*",
|
|
2191
|
+
conditions: [
|
|
2192
|
+
{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }
|
|
2193
|
+
],
|
|
2194
|
+
verdict: "block",
|
|
2195
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
2196
|
+
},
|
|
2197
|
+
{
|
|
2198
|
+
name: "shield:postgres:block-drop-column",
|
|
2199
|
+
tool: "*",
|
|
2200
|
+
conditions: [
|
|
2201
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
2202
|
+
],
|
|
2203
|
+
verdict: "block",
|
|
2204
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
2205
|
+
},
|
|
2206
|
+
{
|
|
2207
|
+
name: "shield:postgres:review-grant-revoke",
|
|
2208
|
+
tool: "*",
|
|
2209
|
+
conditions: [
|
|
2210
|
+
{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }
|
|
2211
|
+
],
|
|
2212
|
+
verdict: "review",
|
|
2213
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
2214
|
+
}
|
|
2215
|
+
],
|
|
2216
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
2217
|
+
};
|
|
2218
|
+
var project_jail_default = {
|
|
2219
|
+
name: "project-jail",
|
|
2220
|
+
description: "Restricts AI agents from reading sensitive credential files outside the current project",
|
|
2221
|
+
aliases: ["jail"],
|
|
2222
|
+
smartRules: [
|
|
2223
|
+
{
|
|
2224
|
+
name: "shield:project-jail:block-read-ssh",
|
|
2225
|
+
tool: "bash",
|
|
2226
|
+
conditions: [
|
|
2227
|
+
{
|
|
2228
|
+
field: "command",
|
|
2229
|
+
op: "matches",
|
|
2230
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.ssh[\\/\\\\]",
|
|
2231
|
+
flags: "i"
|
|
2232
|
+
}
|
|
2233
|
+
],
|
|
2234
|
+
verdict: "block",
|
|
2235
|
+
reason: "Reading SSH private keys is blocked by project-jail shield"
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
name: "shield:project-jail:block-read-aws",
|
|
2239
|
+
tool: "bash",
|
|
2240
|
+
conditions: [
|
|
2241
|
+
{
|
|
2242
|
+
field: "command",
|
|
2243
|
+
op: "matches",
|
|
2244
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.aws[\\/\\\\]",
|
|
2245
|
+
flags: "i"
|
|
2246
|
+
}
|
|
2247
|
+
],
|
|
2248
|
+
verdict: "block",
|
|
2249
|
+
reason: "Reading AWS credentials is blocked by project-jail shield"
|
|
2250
|
+
},
|
|
2251
|
+
{
|
|
2252
|
+
name: "shield:project-jail:block-read-env",
|
|
2253
|
+
tool: "bash",
|
|
2254
|
+
conditions: [
|
|
2255
|
+
{
|
|
2256
|
+
field: "command",
|
|
2257
|
+
op: "matches",
|
|
2258
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*\\.env(\\.local|\\.production|\\.staging)?\\b",
|
|
2259
|
+
flags: "i"
|
|
2260
|
+
}
|
|
2261
|
+
],
|
|
2262
|
+
verdict: "block",
|
|
2263
|
+
reason: "Reading .env files is blocked by project-jail shield"
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
name: "shield:project-jail:block-read-credentials",
|
|
2267
|
+
tool: "bash",
|
|
2268
|
+
conditions: [
|
|
2269
|
+
{
|
|
2270
|
+
field: "command",
|
|
2271
|
+
op: "matches",
|
|
2272
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
|
|
2273
|
+
flags: "i"
|
|
2274
|
+
}
|
|
2275
|
+
],
|
|
2276
|
+
verdict: "block",
|
|
2277
|
+
reason: "Reading credential files is blocked by project-jail shield"
|
|
2278
|
+
}
|
|
2279
|
+
],
|
|
2280
|
+
dangerousWords: []
|
|
2281
|
+
};
|
|
2282
|
+
var redis_default = {
|
|
2283
|
+
name: "redis",
|
|
2284
|
+
description: "Protects Redis instances from destructive AI operations",
|
|
2285
|
+
aliases: [],
|
|
2286
|
+
smartRules: [
|
|
2287
|
+
{
|
|
2288
|
+
name: "shield:redis:block-flushall",
|
|
2289
|
+
tool: "*",
|
|
2290
|
+
conditions: [
|
|
2291
|
+
{
|
|
2292
|
+
field: "command",
|
|
2293
|
+
op: "matches",
|
|
2294
|
+
value: "\\bFLUSHALL\\b",
|
|
2295
|
+
flags: "i"
|
|
2296
|
+
}
|
|
2297
|
+
],
|
|
2298
|
+
verdict: "block",
|
|
2299
|
+
reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
name: "shield:redis:block-flushdb",
|
|
2303
|
+
tool: "*",
|
|
2304
|
+
conditions: [
|
|
2305
|
+
{
|
|
2306
|
+
field: "command",
|
|
2307
|
+
op: "matches",
|
|
2308
|
+
value: "\\bFLUSHDB\\b",
|
|
2309
|
+
flags: "i"
|
|
2310
|
+
}
|
|
2311
|
+
],
|
|
2312
|
+
verdict: "block",
|
|
2313
|
+
reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
|
|
2314
|
+
},
|
|
2315
|
+
{
|
|
2316
|
+
name: "shield:redis:block-config-resetstat",
|
|
2317
|
+
tool: "*",
|
|
2318
|
+
conditions: [
|
|
2319
|
+
{
|
|
2320
|
+
field: "command",
|
|
2321
|
+
op: "matches",
|
|
2322
|
+
value: "\\bCONFIG\\s+RESETSTAT\\b",
|
|
2323
|
+
flags: "i"
|
|
2324
|
+
}
|
|
2325
|
+
],
|
|
2326
|
+
verdict: "block",
|
|
2327
|
+
reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
|
|
2328
|
+
},
|
|
2329
|
+
{
|
|
2330
|
+
name: "shield:redis:review-config-set",
|
|
2331
|
+
tool: "*",
|
|
2332
|
+
conditions: [
|
|
2333
|
+
{
|
|
2334
|
+
field: "command",
|
|
2335
|
+
op: "matches",
|
|
2336
|
+
value: "\\bCONFIG\\s+SET\\b",
|
|
2337
|
+
flags: "i"
|
|
2338
|
+
}
|
|
2339
|
+
],
|
|
2340
|
+
verdict: "review",
|
|
2341
|
+
reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
name: "shield:redis:review-del-wildcard",
|
|
2345
|
+
tool: "*",
|
|
2346
|
+
conditions: [
|
|
2347
|
+
{
|
|
2348
|
+
field: "command",
|
|
2349
|
+
op: "matches",
|
|
2350
|
+
value: "\\bDEL\\b.*[*?\\[]|redis-cli.*--scan.*\\|.*xargs.*del",
|
|
2351
|
+
flags: "i"
|
|
2352
|
+
}
|
|
2353
|
+
],
|
|
2354
|
+
verdict: "review",
|
|
2355
|
+
reason: "Wildcard key deletion requires human approval (Redis shield)"
|
|
2356
|
+
}
|
|
2357
|
+
],
|
|
2358
|
+
dangerousWords: ["FLUSHALL", "FLUSHDB"]
|
|
2359
|
+
};
|
|
2360
|
+
function isShieldVerdict(v) {
|
|
2361
|
+
return v === "allow" || v === "review" || v === "block";
|
|
2362
|
+
}
|
|
2363
|
+
function validateShieldDefinition(raw) {
|
|
2364
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2365
|
+
return { error: "Shield file is not an object" };
|
|
2366
|
+
}
|
|
2367
|
+
const r = raw;
|
|
2368
|
+
if (typeof r.name !== "string" || !r.name) return { error: "Shield file missing 'name'" };
|
|
2369
|
+
if (typeof r.description !== "string") return { error: "Shield file missing 'description'" };
|
|
2370
|
+
if (!Array.isArray(r.aliases)) return { error: "Shield file missing 'aliases' array" };
|
|
2371
|
+
if (!Array.isArray(r.smartRules)) return { error: "Shield file missing 'smartRules' array" };
|
|
2372
|
+
if (!Array.isArray(r.dangerousWords))
|
|
2373
|
+
return { error: "Shield file missing 'dangerousWords' array" };
|
|
2374
|
+
return { ok: r };
|
|
2375
|
+
}
|
|
2376
|
+
function validateOverrides(raw) {
|
|
2377
|
+
const warnings = [];
|
|
2378
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { overrides: {}, warnings };
|
|
2379
|
+
const result = {};
|
|
2380
|
+
for (const [shieldName, rules] of Object.entries(raw)) {
|
|
2381
|
+
if (!rules || typeof rules !== "object" || Array.isArray(rules)) continue;
|
|
2382
|
+
const validRules = {};
|
|
2383
|
+
for (const [ruleName, verdict] of Object.entries(rules)) {
|
|
2384
|
+
if (isShieldVerdict(verdict)) {
|
|
2385
|
+
validRules[ruleName] = verdict;
|
|
2386
|
+
} else {
|
|
2387
|
+
warnings.push(
|
|
2388
|
+
`shields.json contains invalid verdict "${String(verdict)}" for ${shieldName}/${ruleName} \u2014 entry ignored. File may be corrupted or tampered with.`
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
if (Object.keys(validRules).length > 0) result[shieldName] = validRules;
|
|
2393
|
+
}
|
|
2394
|
+
return { overrides: result, warnings };
|
|
2395
|
+
}
|
|
2396
|
+
var BUILTIN_SHIELDS = {
|
|
2397
|
+
[aws_default.name]: aws_default,
|
|
2398
|
+
[bash_safe_default.name]: bash_safe_default,
|
|
2399
|
+
[docker_default.name]: docker_default,
|
|
2400
|
+
[filesystem_default.name]: filesystem_default,
|
|
2401
|
+
[github_default.name]: github_default,
|
|
2402
|
+
[k8s_default.name]: k8s_default,
|
|
2403
|
+
[mcp_tool_gating_default.name]: mcp_tool_gating_default,
|
|
2404
|
+
[mongodb_default.name]: mongodb_default,
|
|
2405
|
+
[postgres_default.name]: postgres_default,
|
|
2406
|
+
[project_jail_default.name]: project_jail_default,
|
|
2407
|
+
[redis_default.name]: redis_default
|
|
2408
|
+
};
|
|
2409
|
+
function assertBuiltinShieldRegexesAreSafe() {
|
|
2410
|
+
for (const shield of Object.values(BUILTIN_SHIELDS)) {
|
|
2411
|
+
for (const rule of shield.smartRules) {
|
|
2412
|
+
const conditions = rule.conditions ?? [];
|
|
2413
|
+
for (const cond of conditions) {
|
|
2414
|
+
if (cond.op !== "matches" && cond.op !== "notMatches") continue;
|
|
2415
|
+
const pattern = cond.value;
|
|
2416
|
+
if (!pattern) continue;
|
|
2417
|
+
if (!(0, import_safe_regex23.default)(pattern)) {
|
|
2418
|
+
throw new Error(
|
|
2419
|
+
`[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has unsafe regex: ${pattern}`
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
assertBuiltinShieldRegexesAreSafe();
|
|
2427
|
+
var LOOP_MAX_RECORDS = 500;
|
|
2428
|
+
function computeArgsHash(args) {
|
|
2429
|
+
const str = JSON.stringify(args ?? "");
|
|
2430
|
+
return import_crypto2.default.createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
2431
|
+
}
|
|
2432
|
+
function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
2433
|
+
const hash = computeArgsHash(args);
|
|
2434
|
+
const cutoff = now - windowMs;
|
|
2435
|
+
const fresh = records.filter((r) => r.ts >= cutoff);
|
|
2436
|
+
fresh.push({ t: tool, h: hash, ts: now });
|
|
2437
|
+
const count = fresh.filter((r) => r.t === tool && r.h === hash).length;
|
|
2438
|
+
const nextRecords = fresh.slice(-LOOP_MAX_RECORDS);
|
|
2439
|
+
return { nextRecords, count, looping: count >= threshold };
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// src/shields.ts
|
|
2443
|
+
var USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
|
|
2444
|
+
function loadUserShields() {
|
|
2445
|
+
const result = {};
|
|
2446
|
+
let entries;
|
|
2447
|
+
try {
|
|
2448
|
+
entries = import_fs2.default.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
|
|
2449
|
+
} catch (err) {
|
|
2450
|
+
if (err.code !== "ENOENT") {
|
|
2451
|
+
process.stderr.write(
|
|
2452
|
+
`[node9] Could not read user shields dir ${USER_SHIELDS_DIR}: ${String(err)}
|
|
2453
|
+
`
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
return result;
|
|
2457
|
+
}
|
|
2458
|
+
for (const file of entries) {
|
|
2459
|
+
const filePath = import_path2.default.join(USER_SHIELDS_DIR, file);
|
|
2460
|
+
try {
|
|
2461
|
+
const raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
|
|
2462
|
+
const v = validateShieldDefinition(raw);
|
|
2463
|
+
if ("error" in v) {
|
|
2464
|
+
process.stderr.write(`[node9] ${v.error}: ${filePath}
|
|
2465
|
+
`);
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
result[v.ok.name] = v.ok;
|
|
2469
|
+
} catch (err) {
|
|
2470
|
+
process.stderr.write(`[node9] Failed to load user shield ${file}: ${String(err)}
|
|
2471
|
+
`);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
return result;
|
|
2475
|
+
}
|
|
2476
|
+
function buildSHIELDS() {
|
|
2477
|
+
return { ...BUILTIN_SHIELDS, ...loadUserShields() };
|
|
2478
|
+
}
|
|
2479
|
+
var SHIELDS = buildSHIELDS();
|
|
2480
|
+
function resolveShieldName(input) {
|
|
2481
|
+
const lower = input.toLowerCase();
|
|
2482
|
+
if (SHIELDS[lower]) return lower;
|
|
2483
|
+
for (const [name, def] of Object.entries(SHIELDS)) {
|
|
2484
|
+
if (def.aliases.includes(lower)) return name;
|
|
2485
|
+
}
|
|
2486
|
+
return null;
|
|
2487
|
+
}
|
|
2488
|
+
function getShield(name) {
|
|
2489
|
+
const resolved = resolveShieldName(name);
|
|
2490
|
+
return resolved ? SHIELDS[resolved] : null;
|
|
2491
|
+
}
|
|
2492
|
+
var SHIELDS_STATE_FILE = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields.json");
|
|
2493
|
+
function readShieldsFile() {
|
|
2494
|
+
try {
|
|
2495
|
+
const raw = import_fs2.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
2496
|
+
if (!raw.trim()) return { active: [] };
|
|
2497
|
+
const parsed = JSON.parse(raw);
|
|
2498
|
+
const active = Array.isArray(parsed.active) ? parsed.active.filter(
|
|
2499
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
2500
|
+
) : [];
|
|
2501
|
+
const { overrides, warnings } = validateOverrides(parsed.overrides);
|
|
2502
|
+
for (const w of warnings) {
|
|
2503
|
+
process.stderr.write(`[node9] Warning: ${w}
|
|
2504
|
+
`);
|
|
2505
|
+
}
|
|
2506
|
+
return { active, overrides };
|
|
2507
|
+
} catch (err) {
|
|
2508
|
+
if (err.code !== "ENOENT") {
|
|
2509
|
+
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
2510
|
+
`);
|
|
2511
|
+
}
|
|
2512
|
+
return { active: [] };
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
function readActiveShields() {
|
|
2516
|
+
return readShieldsFile().active;
|
|
2517
|
+
}
|
|
2518
|
+
function readShieldOverrides() {
|
|
2519
|
+
return readShieldsFile().overrides ?? {};
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// src/config/index.ts
|
|
2523
|
+
var DANGEROUS_WORDS = [
|
|
2524
|
+
"mkfs",
|
|
2525
|
+
// formats/wipes a filesystem partition
|
|
2526
|
+
"shred"
|
|
2527
|
+
// permanently overwrites file contents (unrecoverable)
|
|
2528
|
+
];
|
|
2529
|
+
var DEFAULT_CONFIG = {
|
|
2530
|
+
version: "1.0",
|
|
2531
|
+
settings: {
|
|
2532
|
+
mode: "standard",
|
|
2533
|
+
autoStartDaemon: true,
|
|
2534
|
+
enableUndo: true,
|
|
2535
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
2536
|
+
enableHookLogDebug: true,
|
|
2537
|
+
approvalTimeoutMs: 12e4,
|
|
2538
|
+
// 120-second auto-deny timeout
|
|
2539
|
+
flightRecorder: true,
|
|
2540
|
+
auditHashArgs: true,
|
|
2541
|
+
approvers: { native: true, browser: false, cloud: false, terminal: true },
|
|
2542
|
+
cloudSyncIntervalHours: 5
|
|
2543
|
+
},
|
|
2544
|
+
policy: {
|
|
2545
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
2546
|
+
dangerousWords: DANGEROUS_WORDS,
|
|
2547
|
+
ignoredTools: [
|
|
2548
|
+
"list_*",
|
|
2549
|
+
"get_*",
|
|
2550
|
+
"read_*",
|
|
2551
|
+
"describe_*",
|
|
2552
|
+
"read",
|
|
2553
|
+
"glob",
|
|
2554
|
+
"grep",
|
|
2555
|
+
"ls",
|
|
2556
|
+
"notebookread",
|
|
2557
|
+
"notebookedit",
|
|
2558
|
+
"webfetch",
|
|
2559
|
+
"websearch",
|
|
2560
|
+
"exitplanmode",
|
|
2561
|
+
"askuserquestion",
|
|
2562
|
+
"agent",
|
|
2563
|
+
"task*",
|
|
2564
|
+
"toolsearch",
|
|
2565
|
+
"mcp__ide__*",
|
|
2566
|
+
"getDiagnostics"
|
|
2567
|
+
],
|
|
2568
|
+
toolInspection: {
|
|
2569
|
+
bash: "command",
|
|
2570
|
+
shell: "command",
|
|
2571
|
+
run_shell_command: "command",
|
|
2572
|
+
"terminal.execute": "command",
|
|
2573
|
+
"postgres:query": "sql"
|
|
2574
|
+
},
|
|
2575
|
+
snapshot: {
|
|
2576
|
+
tools: [
|
|
2577
|
+
"str_replace_based_edit_tool",
|
|
2578
|
+
"write_file",
|
|
2579
|
+
"edit_file",
|
|
2580
|
+
"create_file",
|
|
2581
|
+
"edit",
|
|
2582
|
+
"replace"
|
|
2583
|
+
],
|
|
2584
|
+
onlyPaths: [],
|
|
2585
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
2586
|
+
},
|
|
2587
|
+
smartRules: [
|
|
2588
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
2589
|
+
{
|
|
2590
|
+
name: "block-rm-rf-home",
|
|
2591
|
+
tool: "bash",
|
|
2592
|
+
conditionMode: "all",
|
|
2593
|
+
conditions: [
|
|
2594
|
+
{
|
|
2595
|
+
field: "command",
|
|
2596
|
+
op: "matches",
|
|
2597
|
+
// Anchor rm as a shell command (not inside a string arg like a git commit message).
|
|
2598
|
+
value: "(^|&&|\\|\\||;)\\s*rm\\b[^;&|]*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
|
|
2599
|
+
},
|
|
2600
|
+
{
|
|
2601
|
+
field: "command",
|
|
2602
|
+
op: "matches",
|
|
2603
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
2604
|
+
}
|
|
2605
|
+
],
|
|
2606
|
+
verdict: "block",
|
|
2607
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
2608
|
+
description: "The AI wants to recursively delete your home directory. This will permanently destroy all your personal files and cannot be undone."
|
|
2609
|
+
},
|
|
2610
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
2611
|
+
{
|
|
2612
|
+
name: "no-delete-without-where",
|
|
2613
|
+
tool: "*",
|
|
2614
|
+
conditions: [
|
|
2615
|
+
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
2616
|
+
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
2617
|
+
],
|
|
2618
|
+
conditionMode: "all",
|
|
2619
|
+
verdict: "review",
|
|
2620
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table",
|
|
2621
|
+
description: "The AI is running a SQL statement that will modify every row in the table \u2014 no WHERE filter was found. This could wipe or corrupt all your data."
|
|
2622
|
+
},
|
|
2623
|
+
{
|
|
2624
|
+
name: "review-drop-truncate-shell",
|
|
2625
|
+
tool: "bash",
|
|
2626
|
+
conditions: [
|
|
2627
|
+
{
|
|
2628
|
+
field: "command",
|
|
2629
|
+
op: "matches",
|
|
2630
|
+
// Require a DB CLI in the command so grep/cat/echo of SQL strings don't trigger.
|
|
2631
|
+
value: "(^|&&|\\|\\||;|\\|)\\s*(psql|mysql|sqlite3|sqlplus|cockroach|clickhouse-client|mongo)\\b",
|
|
2632
|
+
flags: "i"
|
|
2633
|
+
},
|
|
2634
|
+
{
|
|
2635
|
+
field: "command",
|
|
2636
|
+
op: "matches",
|
|
2637
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
2638
|
+
flags: "i"
|
|
2639
|
+
}
|
|
2640
|
+
],
|
|
2641
|
+
conditionMode: "all",
|
|
2642
|
+
verdict: "review",
|
|
2643
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
2644
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
2645
|
+
},
|
|
2646
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
2647
|
+
{
|
|
2648
|
+
name: "review-force-push",
|
|
2649
|
+
tool: "bash",
|
|
2650
|
+
conditions: [
|
|
2651
|
+
{
|
|
2652
|
+
field: "command",
|
|
2653
|
+
op: "matches",
|
|
2654
|
+
// Anchor git as a shell command so node -e / python -c scripts containing
|
|
2655
|
+
// "git push --force" as a string don't false-positive.
|
|
2656
|
+
value: "(^|&&|\\|\\||;)\\s*git\\s+push[^;&|]*(--force|--force-with-lease|-f\\b)",
|
|
2657
|
+
flags: "i"
|
|
2658
|
+
}
|
|
2659
|
+
],
|
|
2660
|
+
conditionMode: "all",
|
|
2661
|
+
verdict: "review",
|
|
2662
|
+
reason: "Force push rewrites remote history \u2014 confirm this is intentional",
|
|
2663
|
+
description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
|
|
2664
|
+
},
|
|
2665
|
+
{
|
|
2666
|
+
name: "review-git-destructive",
|
|
2667
|
+
tool: "bash",
|
|
2668
|
+
conditions: [
|
|
2669
|
+
{
|
|
2670
|
+
field: "command",
|
|
2671
|
+
op: "matches",
|
|
2672
|
+
// Anchor git as a shell command so node -e / python -c scripts containing
|
|
2673
|
+
// "git reset --hard" as a string don't false-positive.
|
|
2674
|
+
value: "(^|&&|\\|\\||;)\\s*git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
|
|
2675
|
+
flags: "i"
|
|
2676
|
+
},
|
|
2677
|
+
{
|
|
2678
|
+
field: "command",
|
|
2679
|
+
op: "notMatches",
|
|
2680
|
+
// Exclude recovery ops and routine branch-surgery (--onto) — these are not destructive.
|
|
2681
|
+
value: "\\bgit\\s+rebase\\s+--(abort|continue|skip|onto)\\b",
|
|
2682
|
+
flags: "i"
|
|
2683
|
+
}
|
|
2684
|
+
],
|
|
2685
|
+
conditionMode: "all",
|
|
2686
|
+
verdict: "review",
|
|
2687
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes",
|
|
2688
|
+
description: "The AI wants to run a destructive git operation (reset, rebase, clean, or branch delete) that can permanently discard commits or uncommitted work."
|
|
2689
|
+
},
|
|
2690
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
2691
|
+
{
|
|
2692
|
+
name: "review-sudo",
|
|
2693
|
+
tool: "bash",
|
|
2694
|
+
conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
|
|
2695
|
+
conditionMode: "all",
|
|
2696
|
+
verdict: "review",
|
|
2697
|
+
reason: "Command requires elevated privileges",
|
|
2698
|
+
description: "The AI wants to run a command as root (sudo). Commands with root access can modify system files, install software, or change security settings."
|
|
2699
|
+
},
|
|
2700
|
+
{
|
|
2701
|
+
name: "review-curl-pipe-shell",
|
|
2702
|
+
tool: "bash",
|
|
2703
|
+
conditions: [
|
|
2704
|
+
{
|
|
2705
|
+
field: "command",
|
|
2706
|
+
op: "matches",
|
|
2707
|
+
// Anchor curl/wget as a shell command so node -e scripts testing this
|
|
2708
|
+
// regex pattern don't self-match as a false positive.
|
|
2709
|
+
value: "(^|&&|\\|\\||;)\\s*(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
2710
|
+
flags: "i"
|
|
2711
|
+
}
|
|
2712
|
+
],
|
|
2713
|
+
conditionMode: "all",
|
|
2714
|
+
verdict: "block",
|
|
2715
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
2716
|
+
description: "The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed."
|
|
2717
|
+
}
|
|
2718
|
+
],
|
|
2719
|
+
dlp: { enabled: true, scanIgnoredTools: true },
|
|
2720
|
+
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
2721
|
+
skillPinning: { enabled: false, mode: "warn", roots: [] }
|
|
2722
|
+
},
|
|
2723
|
+
environments: {}
|
|
2724
|
+
};
|
|
2725
|
+
var ADVISORY_SMART_RULES = [
|
|
2726
|
+
// ── rm safety ─────────────────────────────────────────────────────────────
|
|
2727
|
+
// tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
|
|
2728
|
+
// Pattern '(^|&&|\|\||;)\s*rm\b' matches rm as a shell command (including in
|
|
2729
|
+
// chained commands like 'cat foo && rm bar') but avoids false-positives on 'docker rm'.
|
|
2730
|
+
{
|
|
2731
|
+
name: "allow-rm-safe-paths",
|
|
2732
|
+
tool: "*",
|
|
2733
|
+
conditionMode: "all",
|
|
2734
|
+
conditions: [
|
|
2735
|
+
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
2736
|
+
{
|
|
2737
|
+
field: "command",
|
|
2738
|
+
op: "matches",
|
|
2739
|
+
// Matches known-safe build artifact paths in the command.
|
|
2740
|
+
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
2741
|
+
}
|
|
2742
|
+
],
|
|
2743
|
+
verdict: "allow",
|
|
2744
|
+
reason: "Deleting a known-safe build artifact path"
|
|
2745
|
+
},
|
|
2746
|
+
{
|
|
2747
|
+
name: "review-rm",
|
|
2748
|
+
tool: "*",
|
|
2749
|
+
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
2750
|
+
verdict: "review",
|
|
2751
|
+
reason: "rm can permanently delete files \u2014 confirm the target path",
|
|
2752
|
+
description: "The AI wants to delete files. Unlike moving to trash, rm is permanent \u2014 the files cannot be recovered without a backup."
|
|
2753
|
+
},
|
|
2754
|
+
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
2755
|
+
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
2756
|
+
// mcp__postgres__query) with a destructive SQL statement in the 'sql' field.
|
|
2757
|
+
// The postgres shield upgrades these from 'review' → 'block' for stricter teams;
|
|
2758
|
+
// without a shield, users still get a human-approval gate on every destructive op.
|
|
2759
|
+
{
|
|
2760
|
+
name: "review-drop-table-sql",
|
|
2761
|
+
tool: "*",
|
|
2762
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
2763
|
+
verdict: "review",
|
|
2764
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead",
|
|
2765
|
+
description: "The AI wants to drop a database table. This permanently deletes the table and all its data \u2014 there is no undo."
|
|
2766
|
+
},
|
|
2767
|
+
{
|
|
2768
|
+
name: "review-truncate-sql",
|
|
2769
|
+
tool: "*",
|
|
2770
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
2771
|
+
verdict: "review",
|
|
2772
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead",
|
|
2773
|
+
description: "The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone."
|
|
2774
|
+
},
|
|
2775
|
+
{
|
|
2776
|
+
name: "review-drop-column-sql",
|
|
2777
|
+
tool: "*",
|
|
2778
|
+
conditions: [
|
|
2779
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
2780
|
+
],
|
|
2781
|
+
verdict: "review",
|
|
2782
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead",
|
|
2783
|
+
description: "The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row."
|
|
2079
2784
|
}
|
|
2080
|
-
|
|
2081
|
-
var
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
if (!inner) return false;
|
|
2090
|
-
const inn = inner;
|
|
2091
|
-
const it = syntax.NodeType(inn);
|
|
2092
|
-
if (it === "CmdSubst") hasCmdSubst = true;
|
|
2093
|
-
if (it === "ParamExp") hasParamExp = true;
|
|
2094
|
-
if (it === "Lit" && DOWNLOAD_CMDS.has(inn.Value?.toLowerCase())) hasCurl = true;
|
|
2095
|
-
return true;
|
|
2096
|
-
});
|
|
2785
|
+
];
|
|
2786
|
+
var cachedConfig = null;
|
|
2787
|
+
function getCredentials() {
|
|
2788
|
+
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
2789
|
+
if (process.env.NODE9_API_KEY) {
|
|
2790
|
+
return {
|
|
2791
|
+
apiKey: process.env.NODE9_API_KEY,
|
|
2792
|
+
apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL
|
|
2793
|
+
};
|
|
2097
2794
|
}
|
|
2098
|
-
if (hasCmdSubst && hasCurl) return "block";
|
|
2099
|
-
if (hasCmdSubst || hasParamExp) return "review";
|
|
2100
|
-
return null;
|
|
2101
|
-
}
|
|
2102
|
-
function detectDangerousShellExec(command) {
|
|
2103
2795
|
try {
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
const
|
|
2109
|
-
if (
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
const cmdName = firstParts[0].Value?.toLowerCase() ?? "";
|
|
2115
|
-
if (cmdName === "eval") {
|
|
2116
|
-
const v = scanArgsForDynamicExec(args, 1);
|
|
2117
|
-
if (v === "block" || v === "review" && result === null) result = v;
|
|
2118
|
-
} else if (SHELL_INTERPRETERS.has(cmdName)) {
|
|
2119
|
-
for (let i = 1; i < args.length - 1; i++) {
|
|
2120
|
-
const flagParts = args[i].Parts || [];
|
|
2121
|
-
if (flagParts.length !== 1 || syntax.NodeType(flagParts[0]) !== "Lit" || flagParts[0].Value !== "-c")
|
|
2122
|
-
continue;
|
|
2123
|
-
const v = scanArgsForDynamicExec(args, i + 1);
|
|
2124
|
-
if (v === "block" || v === "review" && result === null) result = v;
|
|
2125
|
-
break;
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
return true;
|
|
2129
|
-
});
|
|
2130
|
-
return result;
|
|
2131
|
-
} catch {
|
|
2132
|
-
return null;
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
function evaluateSmartConditions(args, rule) {
|
|
2136
|
-
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
2137
|
-
const mode = rule.conditionMode ?? "all";
|
|
2138
|
-
const results = rule.conditions.map((cond) => {
|
|
2139
|
-
const rawVal = getNestedValue(args, cond.field);
|
|
2140
|
-
const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
|
|
2141
|
-
const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
|
|
2142
|
-
switch (cond.op) {
|
|
2143
|
-
case "exists":
|
|
2144
|
-
return val !== null && val !== "";
|
|
2145
|
-
case "notExists":
|
|
2146
|
-
return val === null || val === "";
|
|
2147
|
-
case "contains":
|
|
2148
|
-
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
2149
|
-
case "notContains":
|
|
2150
|
-
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
2151
|
-
case "matches": {
|
|
2152
|
-
if (val === null || !cond.value) return false;
|
|
2153
|
-
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2154
|
-
if (!reM) return false;
|
|
2155
|
-
return reM.test(val);
|
|
2796
|
+
const credPath = import_path3.default.join(import_os3.default.homedir(), ".node9", "credentials.json");
|
|
2797
|
+
if (import_fs3.default.existsSync(credPath)) {
|
|
2798
|
+
const creds = JSON.parse(import_fs3.default.readFileSync(credPath, "utf-8"));
|
|
2799
|
+
const profileName = process.env.NODE9_PROFILE || "default";
|
|
2800
|
+
const profile = creds[profileName];
|
|
2801
|
+
if (profile?.apiKey) {
|
|
2802
|
+
return {
|
|
2803
|
+
apiKey: profile.apiKey,
|
|
2804
|
+
apiUrl: profile.apiUrl || DEFAULT_API_URL
|
|
2805
|
+
};
|
|
2156
2806
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
return !reN.test(val);
|
|
2807
|
+
if (creds.apiKey) {
|
|
2808
|
+
return {
|
|
2809
|
+
apiKey: creds.apiKey,
|
|
2810
|
+
apiUrl: creds.apiUrl || DEFAULT_API_URL
|
|
2811
|
+
};
|
|
2163
2812
|
}
|
|
2164
|
-
case "matchesGlob":
|
|
2165
|
-
return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
|
|
2166
|
-
case "notMatchesGlob":
|
|
2167
|
-
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
|
|
2168
|
-
default:
|
|
2169
|
-
return false;
|
|
2170
2813
|
}
|
|
2171
|
-
});
|
|
2172
|
-
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
2173
|
-
}
|
|
2174
|
-
function extractShellCommand(toolName, args, toolInspection) {
|
|
2175
|
-
const patterns = Object.keys(toolInspection);
|
|
2176
|
-
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
2177
|
-
if (!matchingPattern) return null;
|
|
2178
|
-
const fieldPath = toolInspection[matchingPattern];
|
|
2179
|
-
const value = getNestedValue(args, fieldPath);
|
|
2180
|
-
return typeof value === "string" ? value : null;
|
|
2181
|
-
}
|
|
2182
|
-
function isSqlTool(toolName, toolInspection) {
|
|
2183
|
-
const patterns = Object.keys(toolInspection);
|
|
2184
|
-
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
2185
|
-
if (!matchingPattern) return false;
|
|
2186
|
-
const fieldName = toolInspection[matchingPattern];
|
|
2187
|
-
return fieldName === "sql" || fieldName === "query";
|
|
2188
|
-
}
|
|
2189
|
-
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2190
|
-
function analyzeShellCommand(command) {
|
|
2191
|
-
const actions = [];
|
|
2192
|
-
const paths = [];
|
|
2193
|
-
const allTokens = [];
|
|
2194
|
-
const addToken = (token) => {
|
|
2195
|
-
const lower = token.toLowerCase();
|
|
2196
|
-
allTokens.push(lower);
|
|
2197
|
-
if (lower.includes("/")) allTokens.push(...lower.split("/").filter(Boolean));
|
|
2198
|
-
if (lower.startsWith("-")) allTokens.push(lower.replace(/^-+/, ""));
|
|
2199
|
-
};
|
|
2200
|
-
try {
|
|
2201
|
-
const f = sharedParser.Parse(command, "cmd");
|
|
2202
|
-
syntax.Walk(f, (node) => {
|
|
2203
|
-
if (!node) return false;
|
|
2204
|
-
const n = node;
|
|
2205
|
-
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
2206
|
-
const wordValues = (n.Args || []).map((arg) => {
|
|
2207
|
-
return (arg.Parts || []).map((p) => (p.Value ?? "").replace(/\\(.)/g, "$1")).join("");
|
|
2208
|
-
}).filter((s) => s.length > 0);
|
|
2209
|
-
if (wordValues.length > 0) {
|
|
2210
|
-
const cmd = wordValues[0].toLowerCase();
|
|
2211
|
-
if (!actions.includes(cmd)) actions.push(cmd);
|
|
2212
|
-
wordValues.forEach((w) => addToken(w));
|
|
2213
|
-
wordValues.slice(1).forEach((w) => {
|
|
2214
|
-
if (!w.startsWith("-")) paths.push(w);
|
|
2215
|
-
});
|
|
2216
|
-
}
|
|
2217
|
-
return true;
|
|
2218
|
-
});
|
|
2219
2814
|
} catch {
|
|
2220
2815
|
}
|
|
2221
|
-
|
|
2222
|
-
const normalized = command.replace(/\\(.)/g, "$1");
|
|
2223
|
-
const sanitized = normalized.replace(/["'<>]/g, " ");
|
|
2224
|
-
const segments = sanitized.split(/[|;&]|\$\(|\)|`/);
|
|
2225
|
-
segments.forEach((segment) => {
|
|
2226
|
-
const tokens = segment.trim().split(/\s+/).filter(Boolean);
|
|
2227
|
-
if (tokens.length > 0) {
|
|
2228
|
-
const action = tokens[0].toLowerCase();
|
|
2229
|
-
if (!actions.includes(action)) actions.push(action);
|
|
2230
|
-
tokens.forEach((t) => {
|
|
2231
|
-
addToken(t);
|
|
2232
|
-
if (t !== tokens[0] && !t.startsWith("-")) {
|
|
2233
|
-
if (!paths.includes(t)) paths.push(t);
|
|
2234
|
-
}
|
|
2235
|
-
});
|
|
2236
|
-
}
|
|
2237
|
-
});
|
|
2238
|
-
}
|
|
2239
|
-
return { actions, paths, allTokens };
|
|
2816
|
+
return null;
|
|
2240
2817
|
}
|
|
2241
|
-
|
|
2242
|
-
const
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2818
|
+
function getActiveEnvironment(config) {
|
|
2819
|
+
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
2820
|
+
return config.environments[env] ?? null;
|
|
2821
|
+
}
|
|
2822
|
+
function getConfig(cwd) {
|
|
2823
|
+
if (!cwd && cachedConfig) return cachedConfig;
|
|
2824
|
+
const globalPath = import_path3.default.join(import_os3.default.homedir(), ".node9", "config.json");
|
|
2825
|
+
const projectPath = import_path3.default.join(cwd ?? process.cwd(), "node9.config.json");
|
|
2826
|
+
const globalConfig = tryLoadConfig(globalPath);
|
|
2827
|
+
const projectConfig = tryLoadConfig(projectPath);
|
|
2828
|
+
const mergedSettings = {
|
|
2829
|
+
...DEFAULT_CONFIG.settings,
|
|
2830
|
+
approvers: { ...DEFAULT_CONFIG.settings.approvers }
|
|
2831
|
+
};
|
|
2832
|
+
const mergedPolicy = {
|
|
2833
|
+
sandboxPaths: [...DEFAULT_CONFIG.policy.sandboxPaths],
|
|
2834
|
+
dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
|
|
2835
|
+
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
2836
|
+
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
2837
|
+
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
2838
|
+
snapshot: {
|
|
2839
|
+
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
2840
|
+
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
2841
|
+
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
2842
|
+
},
|
|
2843
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
2844
|
+
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
|
|
2845
|
+
skillPinning: {
|
|
2846
|
+
...DEFAULT_CONFIG.policy.skillPinning,
|
|
2847
|
+
roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
|
|
2252
2848
|
}
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
if (
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2849
|
+
};
|
|
2850
|
+
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
2851
|
+
const applyLayer = (source) => {
|
|
2852
|
+
if (!source) return;
|
|
2853
|
+
const s = source.settings || {};
|
|
2854
|
+
const p = source.policy || {};
|
|
2855
|
+
if (s.mode !== void 0) mergedSettings.mode = s.mode;
|
|
2856
|
+
if (s.autoStartDaemon !== void 0) mergedSettings.autoStartDaemon = s.autoStartDaemon;
|
|
2857
|
+
if (s.enableUndo !== void 0) mergedSettings.enableUndo = s.enableUndo;
|
|
2858
|
+
if (s.enableHookLogDebug !== void 0)
|
|
2859
|
+
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
2860
|
+
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
2861
|
+
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
2862
|
+
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
2863
|
+
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
2864
|
+
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
2865
|
+
if (s.cloudSyncIntervalHours !== void 0)
|
|
2866
|
+
mergedSettings.cloudSyncIntervalHours = s.cloudSyncIntervalHours;
|
|
2867
|
+
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
2868
|
+
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
2869
|
+
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
2870
|
+
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
2871
|
+
if (p.toolInspection)
|
|
2872
|
+
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
2873
|
+
if (p.smartRules) {
|
|
2874
|
+
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
2875
|
+
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
2876
|
+
const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
|
|
2877
|
+
const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
|
|
2878
|
+
const filteredNonBlocks = defaultNonBlocks.filter(
|
|
2879
|
+
(r) => !r.name || !userRuleNames.has(r.name)
|
|
2880
|
+
);
|
|
2881
|
+
mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
|
|
2278
2882
|
}
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
const analyzed = analyzeShellCommand(shellCommand);
|
|
2285
|
-
allTokens = analyzed.allTokens;
|
|
2286
|
-
pathTokens = analyzed.paths;
|
|
2287
|
-
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
2288
|
-
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
2289
|
-
return {
|
|
2290
|
-
decision: "review",
|
|
2291
|
-
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2292
|
-
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2293
|
-
tier: 3
|
|
2294
|
-
};
|
|
2883
|
+
if (p.snapshot) {
|
|
2884
|
+
const s2 = p.snapshot;
|
|
2885
|
+
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
2886
|
+
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
2887
|
+
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
2295
2888
|
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
blockedByLabel: "Node9: Eval Remote Execution",
|
|
2301
|
-
reason: "eval of remote download (curl/wget) is a near-certain supply-chain attack",
|
|
2302
|
-
ruleDescription: "The AI is downloading a script from the internet and running it immediately without inspection. This is a common way malware gets installed.",
|
|
2303
|
-
tier: 3
|
|
2304
|
-
};
|
|
2889
|
+
if (p.dlp) {
|
|
2890
|
+
const d = p.dlp;
|
|
2891
|
+
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
2892
|
+
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
2305
2893
|
}
|
|
2306
|
-
if (
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
tier: 3
|
|
2313
|
-
};
|
|
2894
|
+
if (p.loopDetection) {
|
|
2895
|
+
const ld = p.loopDetection;
|
|
2896
|
+
if (ld.enabled !== void 0) mergedPolicy.loopDetection.enabled = ld.enabled;
|
|
2897
|
+
if (ld.threshold !== void 0) mergedPolicy.loopDetection.threshold = ld.threshold;
|
|
2898
|
+
if (ld.windowSeconds !== void 0)
|
|
2899
|
+
mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
|
|
2314
2900
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
if (
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
decision: "review",
|
|
2323
|
-
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
2324
|
-
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
2325
|
-
tier: 3
|
|
2326
|
-
};
|
|
2901
|
+
if (p.skillPinning && typeof p.skillPinning === "object") {
|
|
2902
|
+
const sp = p.skillPinning;
|
|
2903
|
+
if (sp.enabled !== void 0) mergedPolicy.skillPinning.enabled = sp.enabled;
|
|
2904
|
+
if (sp.mode !== void 0) mergedPolicy.skillPinning.mode = sp.mode;
|
|
2905
|
+
if (Array.isArray(sp.roots)) {
|
|
2906
|
+
for (const r of sp.roots) {
|
|
2907
|
+
if (typeof r === "string" && r.length > 0) mergedPolicy.skillPinning.roots.push(r);
|
|
2327
2908
|
}
|
|
2328
|
-
return {
|
|
2329
|
-
decision: "block",
|
|
2330
|
-
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
2331
|
-
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
2332
|
-
tier: 3
|
|
2333
|
-
};
|
|
2334
2909
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2910
|
+
}
|
|
2911
|
+
const envs = source.environments || {};
|
|
2912
|
+
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
2913
|
+
if (envConfig && typeof envConfig === "object") {
|
|
2914
|
+
const ec = envConfig;
|
|
2915
|
+
mergedEnvironments[envName] = {
|
|
2916
|
+
...mergedEnvironments[envName],
|
|
2917
|
+
// Validate field types before merging — do not blindly spread user input
|
|
2918
|
+
...typeof ec.requireApproval === "boolean" ? { requireApproval: ec.requireApproval } : {}
|
|
2341
2919
|
};
|
|
2342
2920
|
}
|
|
2343
|
-
return {
|
|
2344
|
-
decision: "review",
|
|
2345
|
-
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
2346
|
-
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
2347
|
-
tier: 3
|
|
2348
|
-
};
|
|
2349
|
-
}
|
|
2350
|
-
const firstToken = analyzed.actions[0] ?? "";
|
|
2351
|
-
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
2352
|
-
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
2353
|
-
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
2354
|
-
allTokens.push(...sshHosts);
|
|
2355
2921
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
};
|
|
2922
|
+
};
|
|
2923
|
+
applyLayer(globalConfig);
|
|
2924
|
+
applyLayer(projectConfig);
|
|
2925
|
+
{
|
|
2926
|
+
const cacheFile = import_path3.default.join(import_os3.default.homedir(), ".node9", "rules-cache.json");
|
|
2927
|
+
try {
|
|
2928
|
+
const raw = JSON.parse(import_fs3.default.readFileSync(cacheFile, "utf-8"));
|
|
2929
|
+
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
2930
|
+
applyLayer({ policy: { smartRules: raw.rules } });
|
|
2365
2931
|
}
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2932
|
+
} catch {
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
const shieldOverrides = readShieldOverrides();
|
|
2936
|
+
for (const shieldName of readActiveShields()) {
|
|
2937
|
+
const shield = getShield(shieldName);
|
|
2938
|
+
if (!shield) continue;
|
|
2939
|
+
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2940
|
+
const ruleOverrides = shieldOverrides[shieldName] ?? {};
|
|
2941
|
+
for (const rule of shield.smartRules) {
|
|
2942
|
+
if (!existingRuleNames.has(rule.name)) {
|
|
2943
|
+
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
2944
|
+
mergedPolicy.smartRules.push(
|
|
2945
|
+
overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
|
|
2946
|
+
);
|
|
2373
2947
|
}
|
|
2374
2948
|
}
|
|
2375
|
-
|
|
2376
|
-
|
|
2949
|
+
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
2950
|
+
for (const word of shield.dangerousWords) {
|
|
2951
|
+
if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2955
|
+
for (const rule of ADVISORY_SMART_RULES) {
|
|
2956
|
+
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2957
|
+
}
|
|
2958
|
+
if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
|
|
2959
|
+
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
2960
|
+
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
2961
|
+
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
2962
|
+
mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
|
|
2963
|
+
mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
|
|
2964
|
+
mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
|
|
2965
|
+
mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
|
|
2966
|
+
const result = {
|
|
2967
|
+
settings: mergedSettings,
|
|
2968
|
+
policy: mergedPolicy,
|
|
2969
|
+
environments: mergedEnvironments
|
|
2970
|
+
};
|
|
2971
|
+
if (!cwd) cachedConfig = result;
|
|
2972
|
+
return result;
|
|
2973
|
+
}
|
|
2974
|
+
function tryLoadConfig(filePath) {
|
|
2975
|
+
if (!import_fs3.default.existsSync(filePath)) return null;
|
|
2976
|
+
let raw;
|
|
2977
|
+
try {
|
|
2978
|
+
raw = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
|
|
2979
|
+
} catch (err) {
|
|
2980
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2981
|
+
process.stderr.write(
|
|
2982
|
+
`
|
|
2983
|
+
\u26A0\uFE0F Node9: Failed to parse ${filePath}
|
|
2984
|
+
${msg}
|
|
2985
|
+
\u2192 Using default config
|
|
2986
|
+
|
|
2987
|
+
`
|
|
2988
|
+
);
|
|
2989
|
+
return null;
|
|
2990
|
+
}
|
|
2991
|
+
const SUPPORTED_VERSION = "1.0";
|
|
2992
|
+
const SUPPORTED_MAJOR = SUPPORTED_VERSION.split(".")[0];
|
|
2993
|
+
const fileVersion = raw?.version;
|
|
2994
|
+
if (fileVersion !== void 0) {
|
|
2995
|
+
const vStr = String(fileVersion);
|
|
2996
|
+
const fileMajor = vStr.split(".")[0];
|
|
2997
|
+
if (fileMajor !== SUPPORTED_MAJOR) {
|
|
2998
|
+
process.stderr.write(
|
|
2999
|
+
`
|
|
3000
|
+
\u274C Node9: Config at ${filePath} has version "${vStr}" \u2014 major version is incompatible with this release (expected "${SUPPORTED_VERSION}"). Config will not be loaded.
|
|
3001
|
+
|
|
3002
|
+
`
|
|
3003
|
+
);
|
|
3004
|
+
return null;
|
|
3005
|
+
} else if (vStr !== SUPPORTED_VERSION) {
|
|
3006
|
+
process.stderr.write(
|
|
3007
|
+
`
|
|
3008
|
+
\u26A0\uFE0F Node9: Config at ${filePath} declares version "${vStr}" \u2014 expected "${SUPPORTED_VERSION}". Continuing with best-effort parsing.
|
|
3009
|
+
|
|
3010
|
+
`
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
const { sanitized, error } = sanitizeConfig(raw);
|
|
3015
|
+
if (error) {
|
|
3016
|
+
process.stderr.write(
|
|
3017
|
+
`
|
|
3018
|
+
\u26A0\uFE0F Node9: Invalid config at ${filePath}:
|
|
3019
|
+
${error.replace("Invalid config:\n", "")}
|
|
3020
|
+
\u2192 Invalid fields ignored, using defaults for those keys
|
|
3021
|
+
|
|
3022
|
+
`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
return sanitized;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// src/policy/index.ts
|
|
3029
|
+
var import_picomatch2 = __toESM(require("picomatch"));
|
|
3030
|
+
|
|
3031
|
+
// src/dlp.ts
|
|
3032
|
+
var import_fs4 = __toESM(require("fs"));
|
|
3033
|
+
var import_path4 = __toESM(require("path"));
|
|
3034
|
+
function scanFilePath(filePath, cwd = process.cwd()) {
|
|
3035
|
+
if (!filePath) return null;
|
|
3036
|
+
let resolved;
|
|
3037
|
+
try {
|
|
3038
|
+
const absolute = import_path4.default.resolve(cwd, filePath);
|
|
3039
|
+
resolved = import_fs4.default.realpathSync.native(absolute);
|
|
3040
|
+
} catch (err) {
|
|
3041
|
+
const code = err.code;
|
|
3042
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
3043
|
+
resolved = import_path4.default.resolve(cwd, filePath);
|
|
3044
|
+
} else {
|
|
3045
|
+
return sensitivePathMatch(filePath);
|
|
2377
3046
|
}
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
3047
|
+
}
|
|
3048
|
+
return matchSensitivePath(resolved, filePath);
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// src/utils/provenance.ts
|
|
3052
|
+
var import_fs5 = __toESM(require("fs"));
|
|
3053
|
+
var import_path5 = __toESM(require("path"));
|
|
3054
|
+
var import_os4 = __toESM(require("os"));
|
|
3055
|
+
var SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
|
|
3056
|
+
var MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
|
|
3057
|
+
var USER_PREFIXES = [
|
|
3058
|
+
import_path5.default.join(import_os4.default.homedir(), "bin"),
|
|
3059
|
+
import_path5.default.join(import_os4.default.homedir(), ".local", "bin"),
|
|
3060
|
+
import_path5.default.join(import_os4.default.homedir(), ".cargo", "bin"),
|
|
3061
|
+
import_path5.default.join(import_os4.default.homedir(), ".npm-global", "bin"),
|
|
3062
|
+
import_path5.default.join(import_os4.default.homedir(), ".volta", "bin")
|
|
3063
|
+
];
|
|
3064
|
+
var SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
|
|
3065
|
+
function findInPath(cmd) {
|
|
3066
|
+
if (import_path5.default.posix.isAbsolute(cmd)) return cmd;
|
|
3067
|
+
const pathEnv = process.env.PATH ?? "";
|
|
3068
|
+
for (const dir of pathEnv.split(import_path5.default.delimiter)) {
|
|
3069
|
+
if (!dir) continue;
|
|
3070
|
+
const full = import_path5.default.join(dir, cmd);
|
|
3071
|
+
try {
|
|
3072
|
+
import_fs5.default.accessSync(full, import_fs5.default.constants.X_OK);
|
|
3073
|
+
return full;
|
|
3074
|
+
} catch {
|
|
2384
3075
|
}
|
|
2385
3076
|
}
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
3077
|
+
return null;
|
|
3078
|
+
}
|
|
3079
|
+
function _classifyPath(resolved, cwd) {
|
|
3080
|
+
if (cwd && resolved.startsWith(cwd + "/")) {
|
|
3081
|
+
return { trustLevel: "user", reason: "binary in project directory" };
|
|
3082
|
+
}
|
|
3083
|
+
const osTmp = import_os4.default.tmpdir();
|
|
3084
|
+
const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
|
|
3085
|
+
if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
3086
|
+
return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
|
|
3087
|
+
}
|
|
3088
|
+
if (SYSTEM_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
3089
|
+
return { trustLevel: "system", reason: "" };
|
|
3090
|
+
}
|
|
3091
|
+
if (MANAGED_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
3092
|
+
return { trustLevel: "managed", reason: "" };
|
|
3093
|
+
}
|
|
3094
|
+
if (USER_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
3095
|
+
return { trustLevel: "user", reason: "" };
|
|
3096
|
+
}
|
|
3097
|
+
return { trustLevel: "unknown", reason: "binary in unrecognized location" };
|
|
3098
|
+
}
|
|
3099
|
+
function checkProvenance(cmd, cwd) {
|
|
3100
|
+
const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
|
|
3101
|
+
if (import_path5.default.posix.isAbsolute(bare)) {
|
|
3102
|
+
const early = _classifyPath(bare, cwd);
|
|
3103
|
+
if (early.trustLevel === "suspect") {
|
|
3104
|
+
return { resolvedPath: bare, ...early };
|
|
2395
3105
|
}
|
|
2396
|
-
return { decision: "allow" };
|
|
2397
3106
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
3107
|
+
let resolved;
|
|
3108
|
+
try {
|
|
3109
|
+
const found = findInPath(bare);
|
|
3110
|
+
if (!found) {
|
|
3111
|
+
return {
|
|
3112
|
+
resolvedPath: cmd,
|
|
3113
|
+
trustLevel: "unknown",
|
|
3114
|
+
reason: "binary not found in PATH"
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
resolved = import_fs5.default.realpathSync(found);
|
|
3118
|
+
} catch {
|
|
3119
|
+
return {
|
|
3120
|
+
resolvedPath: cmd,
|
|
3121
|
+
trustLevel: "unknown",
|
|
3122
|
+
reason: "binary not found in PATH"
|
|
3123
|
+
};
|
|
2401
3124
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
(
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
return false;
|
|
2411
|
-
}
|
|
2412
|
-
})();
|
|
2413
|
-
if (hit && !matchedDangerousWord) matchedDangerousWord = word;
|
|
2414
|
-
return hit;
|
|
2415
|
-
})
|
|
2416
|
-
);
|
|
2417
|
-
if (isDangerous) {
|
|
2418
|
-
let matchedField;
|
|
2419
|
-
if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
|
|
2420
|
-
const obj = args;
|
|
2421
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
2422
|
-
if (typeof value === "string") {
|
|
2423
|
-
try {
|
|
2424
|
-
if (new RegExp(
|
|
2425
|
-
`\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
2426
|
-
"i"
|
|
2427
|
-
).test(value)) {
|
|
2428
|
-
matchedField = key;
|
|
2429
|
-
break;
|
|
2430
|
-
}
|
|
2431
|
-
} catch {
|
|
2432
|
-
}
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
3125
|
+
try {
|
|
3126
|
+
const stat = import_fs5.default.statSync(resolved);
|
|
3127
|
+
if (stat.mode & 2) {
|
|
3128
|
+
return {
|
|
3129
|
+
resolvedPath: resolved,
|
|
3130
|
+
trustLevel: "suspect",
|
|
3131
|
+
reason: "binary is world-writable"
|
|
3132
|
+
};
|
|
2435
3133
|
}
|
|
3134
|
+
} catch {
|
|
2436
3135
|
return {
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
matchedField,
|
|
2441
|
-
ruleDescription: `This command contains a flagged keyword ("${matchedDangerousWord}") from your node9 config. Review it before allowing.`,
|
|
2442
|
-
tier: 6
|
|
3136
|
+
resolvedPath: resolved,
|
|
3137
|
+
trustLevel: "unknown",
|
|
3138
|
+
reason: "could not stat binary"
|
|
2443
3139
|
};
|
|
2444
3140
|
}
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
3141
|
+
const classify = _classifyPath(resolved, cwd);
|
|
3142
|
+
return { resolvedPath: resolved, ...classify };
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
// src/auth/trusted-hosts.ts
|
|
3146
|
+
var import_fs6 = __toESM(require("fs"));
|
|
3147
|
+
var import_path6 = __toESM(require("path"));
|
|
3148
|
+
var import_os5 = __toESM(require("os"));
|
|
3149
|
+
function getTrustedHostsPath() {
|
|
3150
|
+
return import_path6.default.join(import_os5.default.homedir(), ".node9", "trusted-hosts.json");
|
|
3151
|
+
}
|
|
3152
|
+
function readTrustedHosts() {
|
|
3153
|
+
try {
|
|
3154
|
+
const raw = import_fs6.default.readFileSync(getTrustedHostsPath(), "utf8");
|
|
3155
|
+
const parsed = JSON.parse(raw);
|
|
3156
|
+
return Array.isArray(parsed.hosts) ? parsed.hosts : [];
|
|
3157
|
+
} catch {
|
|
3158
|
+
return [];
|
|
2449
3159
|
}
|
|
2450
|
-
return { decision: "allow" };
|
|
2451
3160
|
}
|
|
2452
|
-
|
|
3161
|
+
var _cache = null;
|
|
3162
|
+
var CACHE_TTL_MS = 5e3;
|
|
3163
|
+
function getFileMtime() {
|
|
3164
|
+
try {
|
|
3165
|
+
return import_fs6.default.statSync(getTrustedHostsPath()).mtimeMs;
|
|
3166
|
+
} catch {
|
|
3167
|
+
return 0;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
function getCachedHosts() {
|
|
3171
|
+
const now = Date.now();
|
|
3172
|
+
if (_cache && now < _cache.expiry) {
|
|
3173
|
+
const mtime = getFileMtime();
|
|
3174
|
+
if (mtime === _cache.mtime) return _cache.hosts;
|
|
3175
|
+
}
|
|
3176
|
+
const hosts = readTrustedHosts();
|
|
3177
|
+
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
3178
|
+
return hosts;
|
|
3179
|
+
}
|
|
3180
|
+
function normalizeHost(raw) {
|
|
3181
|
+
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
3182
|
+
}
|
|
3183
|
+
function isTrustedHost(host) {
|
|
3184
|
+
const normalized = normalizeHost(host);
|
|
3185
|
+
return getCachedHosts().some((entry) => {
|
|
3186
|
+
const entryHost = entry.host.toLowerCase();
|
|
3187
|
+
if (entryHost.startsWith("*.")) {
|
|
3188
|
+
const domain = entryHost.slice(2);
|
|
3189
|
+
return normalized.endsWith("." + domain);
|
|
3190
|
+
}
|
|
3191
|
+
return normalized === entryHost;
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
// src/policy/index.ts
|
|
3196
|
+
async function evaluatePolicy2(toolName, args, agent, cwd) {
|
|
2453
3197
|
const config = getConfig();
|
|
2454
|
-
|
|
3198
|
+
const activeEnvironment = getActiveEnvironment(config) ?? void 0;
|
|
3199
|
+
return evaluatePolicy(
|
|
3200
|
+
config,
|
|
3201
|
+
toolName,
|
|
3202
|
+
args,
|
|
3203
|
+
{ agent, cwd, activeEnvironment },
|
|
3204
|
+
{ checkProvenance, isTrustedHost }
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
3207
|
+
function isIgnoredTool2(toolName) {
|
|
3208
|
+
return isIgnoredTool(toolName, getConfig());
|
|
2455
3209
|
}
|
|
2456
3210
|
|
|
2457
3211
|
// src/auth/state.ts
|
|
2458
3212
|
var import_fs7 = __toESM(require("fs"));
|
|
2459
|
-
var
|
|
3213
|
+
var import_path7 = __toESM(require("path"));
|
|
2460
3214
|
var import_os6 = __toESM(require("os"));
|
|
2461
|
-
var PAUSED_FILE =
|
|
2462
|
-
var TRUST_FILE =
|
|
3215
|
+
var PAUSED_FILE = import_path7.default.join(import_os6.default.homedir(), ".node9", "PAUSED");
|
|
3216
|
+
var TRUST_FILE = import_path7.default.join(import_os6.default.homedir(), ".node9", "trust.json");
|
|
2463
3217
|
function extractCommandPattern(toolName, args) {
|
|
2464
3218
|
const lower = toolName.toLowerCase();
|
|
2465
3219
|
if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
|
|
@@ -2486,7 +3240,7 @@ function checkPause() {
|
|
|
2486
3240
|
}
|
|
2487
3241
|
}
|
|
2488
3242
|
function atomicWriteSync(filePath, data, options) {
|
|
2489
|
-
const dir =
|
|
3243
|
+
const dir = import_path7.default.dirname(filePath);
|
|
2490
3244
|
if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
|
|
2491
3245
|
const tmpPath = `${filePath}.${import_os6.default.hostname()}.${process.pid}.tmp`;
|
|
2492
3246
|
import_fs7.default.writeFileSync(tmpPath, data, options);
|
|
@@ -2541,7 +3295,7 @@ function writeTrustSession(toolName, durationMs, args) {
|
|
|
2541
3295
|
}
|
|
2542
3296
|
function getPersistentDecision(toolName) {
|
|
2543
3297
|
try {
|
|
2544
|
-
const file =
|
|
3298
|
+
const file = import_path7.default.join(import_os6.default.homedir(), ".node9", "decisions.json");
|
|
2545
3299
|
if (!import_fs7.default.existsSync(file)) return null;
|
|
2546
3300
|
const decisions = JSON.parse(import_fs7.default.readFileSync(file, "utf-8"));
|
|
2547
3301
|
const d = decisions[toolName];
|
|
@@ -2554,10 +3308,10 @@ function getPersistentDecision(toolName) {
|
|
|
2554
3308
|
// src/auth/daemon.ts
|
|
2555
3309
|
var import_fs8 = __toESM(require("fs"));
|
|
2556
3310
|
var import_net = __toESM(require("net"));
|
|
2557
|
-
var
|
|
3311
|
+
var import_path8 = __toESM(require("path"));
|
|
2558
3312
|
var import_os7 = __toESM(require("os"));
|
|
2559
3313
|
var import_child_process = require("child_process");
|
|
2560
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
3314
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path8.default.join(import_os7.default.tmpdir(), "node9-activity.sock");
|
|
2561
3315
|
function notifyActivitySocket(data) {
|
|
2562
3316
|
return new Promise((resolve) => {
|
|
2563
3317
|
try {
|
|
@@ -2590,7 +3344,7 @@ var DAEMON_PORT = 7391;
|
|
|
2590
3344
|
var DAEMON_HOST = "127.0.0.1";
|
|
2591
3345
|
function getInternalToken() {
|
|
2592
3346
|
try {
|
|
2593
|
-
const pidFile =
|
|
3347
|
+
const pidFile = import_path8.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
|
|
2594
3348
|
if (!import_fs8.default.existsSync(pidFile)) return null;
|
|
2595
3349
|
const data = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
|
|
2596
3350
|
process.kill(data.pid, 0);
|
|
@@ -2600,7 +3354,7 @@ function getInternalToken() {
|
|
|
2600
3354
|
}
|
|
2601
3355
|
}
|
|
2602
3356
|
function isDaemonRunning() {
|
|
2603
|
-
const pidFile =
|
|
3357
|
+
const pidFile = import_path8.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
|
|
2604
3358
|
if (import_fs8.default.existsSync(pidFile)) {
|
|
2605
3359
|
let pid;
|
|
2606
3360
|
let port;
|
|
@@ -2773,10 +3527,10 @@ var import_crypto3 = require("crypto");
|
|
|
2773
3527
|
|
|
2774
3528
|
// src/ui/native.ts
|
|
2775
3529
|
var import_child_process2 = require("child_process");
|
|
2776
|
-
var
|
|
3530
|
+
var import_path10 = __toESM(require("path"));
|
|
2777
3531
|
|
|
2778
3532
|
// src/context-sniper.ts
|
|
2779
|
-
var
|
|
3533
|
+
var import_path9 = __toESM(require("path"));
|
|
2780
3534
|
function smartTruncate(str, maxLen = 500) {
|
|
2781
3535
|
if (str.length <= maxLen) return str;
|
|
2782
3536
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -2844,7 +3598,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
2844
3598
|
intent = "EDIT";
|
|
2845
3599
|
if (obj.file_path) {
|
|
2846
3600
|
editFilePath = String(obj.file_path);
|
|
2847
|
-
editFileName =
|
|
3601
|
+
editFileName = import_path9.default.basename(editFilePath);
|
|
2848
3602
|
}
|
|
2849
3603
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
2850
3604
|
contextSnippet = result.snippet;
|
|
@@ -2899,7 +3653,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
2899
3653
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2900
3654
|
const obj = parsed;
|
|
2901
3655
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
2902
|
-
const file = obj.file_path ?
|
|
3656
|
+
const file = obj.file_path ? import_path10.default.basename(String(obj.file_path)) : "file";
|
|
2903
3657
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
2904
3658
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
2905
3659
|
return {
|
|
@@ -3100,7 +3854,7 @@ init_audit();
|
|
|
3100
3854
|
// src/auth/cloud.ts
|
|
3101
3855
|
var import_fs9 = __toESM(require("fs"));
|
|
3102
3856
|
var import_os8 = __toESM(require("os"));
|
|
3103
|
-
var
|
|
3857
|
+
var import_path11 = __toESM(require("path"));
|
|
3104
3858
|
init_audit();
|
|
3105
3859
|
var DLP_SAMPLE_MAX_LEN = 200;
|
|
3106
3860
|
var DLP_PATTERN_MAX_LEN = 100;
|
|
@@ -3177,7 +3931,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
3177
3931
|
let ciContext;
|
|
3178
3932
|
if (process.env.CI) {
|
|
3179
3933
|
try {
|
|
3180
|
-
const ciContextPath =
|
|
3934
|
+
const ciContextPath = import_path11.default.join(import_os8.default.homedir(), ".node9", "ci-context.json");
|
|
3181
3935
|
const stats = import_fs9.default.statSync(ciContextPath);
|
|
3182
3936
|
if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
|
|
3183
3937
|
const raw = import_fs9.default.readFileSync(ciContextPath, "utf8");
|
|
@@ -3293,16 +4047,10 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
3293
4047
|
|
|
3294
4048
|
// src/loop-detector.ts
|
|
3295
4049
|
var import_fs10 = __toESM(require("fs"));
|
|
3296
|
-
var
|
|
4050
|
+
var import_path12 = __toESM(require("path"));
|
|
3297
4051
|
var import_os9 = __toESM(require("os"));
|
|
3298
|
-
var import_crypto2 = __toESM(require("crypto"));
|
|
3299
4052
|
function loopStateFile() {
|
|
3300
|
-
return
|
|
3301
|
-
}
|
|
3302
|
-
var MAX_RECORDS = 500;
|
|
3303
|
-
function computeArgsHash(args) {
|
|
3304
|
-
const str = JSON.stringify(args ?? "");
|
|
3305
|
-
return import_crypto2.default.createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
4053
|
+
return import_path12.default.join(import_os9.default.homedir(), ".node9", "loop-state.json");
|
|
3306
4054
|
}
|
|
3307
4055
|
function readState() {
|
|
3308
4056
|
try {
|
|
@@ -3316,7 +4064,7 @@ function readState() {
|
|
|
3316
4064
|
}
|
|
3317
4065
|
}
|
|
3318
4066
|
function writeState(records) {
|
|
3319
|
-
const dir =
|
|
4067
|
+
const dir = import_path12.default.dirname(loopStateFile());
|
|
3320
4068
|
if (!import_fs10.default.existsSync(dir)) import_fs10.default.mkdirSync(dir, { recursive: true });
|
|
3321
4069
|
const tmpPath = `${loopStateFile()}.${import_os9.default.hostname()}.${process.pid}.tmp`;
|
|
3322
4070
|
import_fs10.default.writeFileSync(tmpPath, JSON.stringify(records));
|
|
@@ -3324,14 +4072,9 @@ function writeState(records) {
|
|
|
3324
4072
|
}
|
|
3325
4073
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
3326
4074
|
try {
|
|
3327
|
-
const
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
const records = readState().filter((r) => r.ts >= cutoff);
|
|
3331
|
-
records.push({ t: tool, h: hash, ts: now });
|
|
3332
|
-
const count = records.filter((r) => r.t === tool && r.h === hash).length;
|
|
3333
|
-
writeState(records.slice(-MAX_RECORDS));
|
|
3334
|
-
return { looping: count >= threshold, count };
|
|
4075
|
+
const result = evaluateLoopWindow(readState(), tool, args, threshold, windowMs, Date.now());
|
|
4076
|
+
writeState(result.nextRecords);
|
|
4077
|
+
return { looping: result.looping, count: result.count };
|
|
3335
4078
|
} catch {
|
|
3336
4079
|
return { looping: false, count: 0 };
|
|
3337
4080
|
}
|
|
@@ -3464,7 +4207,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3464
4207
|
}
|
|
3465
4208
|
}
|
|
3466
4209
|
}
|
|
3467
|
-
if (config.policy.dlp.enabled && (!
|
|
4210
|
+
if (config.policy.dlp.enabled && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
3468
4211
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
3469
4212
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
3470
4213
|
const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
|
|
@@ -3514,8 +4257,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3514
4257
|
}
|
|
3515
4258
|
}
|
|
3516
4259
|
if (isObserveMode) {
|
|
3517
|
-
if (!
|
|
3518
|
-
const policyResult = await
|
|
4260
|
+
if (!isIgnoredTool2(toolName)) {
|
|
4261
|
+
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
3519
4262
|
const wouldBlock = policyResult.decision === "block";
|
|
3520
4263
|
if (!isManual)
|
|
3521
4264
|
appendLocalAudit(
|
|
@@ -3539,8 +4282,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3539
4282
|
return { approved: true, checkedBy: "audit" };
|
|
3540
4283
|
}
|
|
3541
4284
|
if (config.settings.mode === "audit") {
|
|
3542
|
-
if (!
|
|
3543
|
-
const policyResult = await
|
|
4285
|
+
if (!isIgnoredTool2(toolName)) {
|
|
4286
|
+
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
3544
4287
|
if (policyResult.decision === "review") {
|
|
3545
4288
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
3546
4289
|
if (approvers.cloud && creds?.apiKey) {
|
|
@@ -3550,7 +4293,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3550
4293
|
}
|
|
3551
4294
|
return { approved: true, checkedBy: "audit" };
|
|
3552
4295
|
}
|
|
3553
|
-
if (!taintWarning && !
|
|
4296
|
+
if (!taintWarning && !isIgnoredTool2(toolName)) {
|
|
3554
4297
|
const ld = config.policy.loopDetection;
|
|
3555
4298
|
if (ld.enabled) {
|
|
3556
4299
|
const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
|
|
@@ -3568,7 +4311,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3568
4311
|
};
|
|
3569
4312
|
}
|
|
3570
4313
|
}
|
|
3571
|
-
const policyResult = await
|
|
4314
|
+
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent);
|
|
3572
4315
|
if (policyResult.decision === "allow") {
|
|
3573
4316
|
if (approvers.cloud && creds?.apiKey)
|
|
3574
4317
|
await auditLocalAllow(toolName, args, "local-policy", creds, meta);
|