@node9/proxy 1.0.13 → 1.0.14
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 +117 -119
- package/dist/cli.js +736 -360
- package/dist/cli.mjs +731 -355
- package/dist/index.js +441 -123
- package/dist/index.mjs +441 -123
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -29,9 +29,9 @@ var import_commander = require("commander");
|
|
|
29
29
|
// src/core.ts
|
|
30
30
|
var import_chalk2 = __toESM(require("chalk"));
|
|
31
31
|
var import_prompts = require("@inquirer/prompts");
|
|
32
|
-
var
|
|
33
|
-
var
|
|
34
|
-
var
|
|
32
|
+
var import_fs2 = __toESM(require("fs"));
|
|
33
|
+
var import_path4 = __toESM(require("path"));
|
|
34
|
+
var import_os2 = __toESM(require("os"));
|
|
35
35
|
var import_picomatch = __toESM(require("picomatch"));
|
|
36
36
|
var import_sh_syntax = require("sh-syntax");
|
|
37
37
|
|
|
@@ -361,25 +361,26 @@ var import_zod = require("zod");
|
|
|
361
361
|
var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
362
362
|
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
363
363
|
});
|
|
364
|
-
var validRegex = noNewlines.refine(
|
|
365
|
-
(s) => {
|
|
366
|
-
try {
|
|
367
|
-
new RegExp(s);
|
|
368
|
-
return true;
|
|
369
|
-
} catch {
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
},
|
|
373
|
-
{ message: "Value must be a valid regular expression" }
|
|
374
|
-
);
|
|
375
364
|
var SmartConditionSchema = import_zod.z.object({
|
|
376
365
|
field: import_zod.z.string().min(1, "Condition field must not be empty"),
|
|
377
|
-
op: import_zod.z.enum(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
366
|
+
op: import_zod.z.enum(
|
|
367
|
+
[
|
|
368
|
+
"matches",
|
|
369
|
+
"notMatches",
|
|
370
|
+
"contains",
|
|
371
|
+
"notContains",
|
|
372
|
+
"exists",
|
|
373
|
+
"notExists",
|
|
374
|
+
"matchesGlob",
|
|
375
|
+
"notMatchesGlob"
|
|
376
|
+
],
|
|
377
|
+
{
|
|
378
|
+
errorMap: () => ({
|
|
379
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
),
|
|
383
|
+
value: import_zod.z.string().optional(),
|
|
383
384
|
flags: import_zod.z.string().optional()
|
|
384
385
|
});
|
|
385
386
|
var SmartRuleSchema = import_zod.z.object({
|
|
@@ -392,11 +393,6 @@ var SmartRuleSchema = import_zod.z.object({
|
|
|
392
393
|
}),
|
|
393
394
|
reason: import_zod.z.string().optional()
|
|
394
395
|
});
|
|
395
|
-
var PolicyRuleSchema = import_zod.z.object({
|
|
396
|
-
action: import_zod.z.string().min(1),
|
|
397
|
-
allowPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
398
|
-
blockPaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
399
|
-
});
|
|
400
396
|
var ConfigFileSchema = import_zod.z.object({
|
|
401
397
|
version: import_zod.z.string().optional(),
|
|
402
398
|
settings: import_zod.z.object({
|
|
@@ -421,12 +417,15 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
421
417
|
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
422
418
|
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
423
419
|
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
424
|
-
rules: import_zod.z.array(PolicyRuleSchema).optional(),
|
|
425
420
|
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
426
421
|
snapshot: import_zod.z.object({
|
|
427
422
|
tools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
428
423
|
onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
429
424
|
ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
425
|
+
}).optional(),
|
|
426
|
+
dlp: import_zod.z.object({
|
|
427
|
+
enabled: import_zod.z.boolean().optional(),
|
|
428
|
+
scanIgnoredTools: import_zod.z.boolean().optional()
|
|
430
429
|
}).optional()
|
|
431
430
|
}).optional(),
|
|
432
431
|
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
@@ -448,8 +447,8 @@ function sanitizeConfig(raw) {
|
|
|
448
447
|
}
|
|
449
448
|
}
|
|
450
449
|
const lines = result.error.issues.map((issue) => {
|
|
451
|
-
const
|
|
452
|
-
return ` \u2022 ${
|
|
450
|
+
const path9 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
451
|
+
return ` \u2022 ${path9}: ${issue.message}`;
|
|
453
452
|
});
|
|
454
453
|
return {
|
|
455
454
|
sanitized,
|
|
@@ -458,18 +457,301 @@ ${lines.join("\n")}`
|
|
|
458
457
|
};
|
|
459
458
|
}
|
|
460
459
|
|
|
460
|
+
// src/shields.ts
|
|
461
|
+
var import_fs = __toESM(require("fs"));
|
|
462
|
+
var import_path3 = __toESM(require("path"));
|
|
463
|
+
var import_os = __toESM(require("os"));
|
|
464
|
+
var import_crypto = __toESM(require("crypto"));
|
|
465
|
+
var SHIELDS = {
|
|
466
|
+
postgres: {
|
|
467
|
+
name: "postgres",
|
|
468
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
469
|
+
aliases: ["pg", "postgresql"],
|
|
470
|
+
smartRules: [
|
|
471
|
+
{
|
|
472
|
+
name: "shield:postgres:block-drop-table",
|
|
473
|
+
tool: "*",
|
|
474
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
475
|
+
verdict: "block",
|
|
476
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
name: "shield:postgres:block-truncate",
|
|
480
|
+
tool: "*",
|
|
481
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
482
|
+
verdict: "block",
|
|
483
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: "shield:postgres:block-drop-column",
|
|
487
|
+
tool: "*",
|
|
488
|
+
conditions: [
|
|
489
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
490
|
+
],
|
|
491
|
+
verdict: "block",
|
|
492
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: "shield:postgres:review-grant-revoke",
|
|
496
|
+
tool: "*",
|
|
497
|
+
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
498
|
+
verdict: "review",
|
|
499
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
500
|
+
}
|
|
501
|
+
],
|
|
502
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
503
|
+
},
|
|
504
|
+
github: {
|
|
505
|
+
name: "github",
|
|
506
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
507
|
+
aliases: ["git"],
|
|
508
|
+
smartRules: [
|
|
509
|
+
{
|
|
510
|
+
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
511
|
+
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
512
|
+
name: "shield:github:review-delete-branch-remote",
|
|
513
|
+
tool: "bash",
|
|
514
|
+
conditions: [
|
|
515
|
+
{
|
|
516
|
+
field: "command",
|
|
517
|
+
op: "matches",
|
|
518
|
+
value: "git\\s+push\\s+.*--delete",
|
|
519
|
+
flags: "i"
|
|
520
|
+
}
|
|
521
|
+
],
|
|
522
|
+
verdict: "review",
|
|
523
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: "shield:github:block-delete-repo",
|
|
527
|
+
tool: "*",
|
|
528
|
+
conditions: [
|
|
529
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
530
|
+
],
|
|
531
|
+
verdict: "block",
|
|
532
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
533
|
+
}
|
|
534
|
+
],
|
|
535
|
+
dangerousWords: []
|
|
536
|
+
},
|
|
537
|
+
aws: {
|
|
538
|
+
name: "aws",
|
|
539
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
540
|
+
aliases: ["amazon"],
|
|
541
|
+
smartRules: [
|
|
542
|
+
{
|
|
543
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
544
|
+
tool: "*",
|
|
545
|
+
conditions: [
|
|
546
|
+
{
|
|
547
|
+
field: "command",
|
|
548
|
+
op: "matches",
|
|
549
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
550
|
+
flags: "i"
|
|
551
|
+
}
|
|
552
|
+
],
|
|
553
|
+
verdict: "block",
|
|
554
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: "shield:aws:review-iam-changes",
|
|
558
|
+
tool: "*",
|
|
559
|
+
conditions: [
|
|
560
|
+
{
|
|
561
|
+
field: "command",
|
|
562
|
+
op: "matches",
|
|
563
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
564
|
+
flags: "i"
|
|
565
|
+
}
|
|
566
|
+
],
|
|
567
|
+
verdict: "review",
|
|
568
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: "shield:aws:block-ec2-terminate",
|
|
572
|
+
tool: "*",
|
|
573
|
+
conditions: [
|
|
574
|
+
{
|
|
575
|
+
field: "command",
|
|
576
|
+
op: "matches",
|
|
577
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
578
|
+
flags: "i"
|
|
579
|
+
}
|
|
580
|
+
],
|
|
581
|
+
verdict: "block",
|
|
582
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: "shield:aws:review-rds-delete",
|
|
586
|
+
tool: "*",
|
|
587
|
+
conditions: [
|
|
588
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
589
|
+
],
|
|
590
|
+
verdict: "review",
|
|
591
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
592
|
+
}
|
|
593
|
+
],
|
|
594
|
+
dangerousWords: []
|
|
595
|
+
},
|
|
596
|
+
filesystem: {
|
|
597
|
+
name: "filesystem",
|
|
598
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
599
|
+
aliases: ["fs"],
|
|
600
|
+
smartRules: [
|
|
601
|
+
{
|
|
602
|
+
name: "shield:filesystem:review-chmod-777",
|
|
603
|
+
tool: "bash",
|
|
604
|
+
conditions: [
|
|
605
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
606
|
+
],
|
|
607
|
+
verdict: "review",
|
|
608
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
name: "shield:filesystem:review-write-etc",
|
|
612
|
+
tool: "bash",
|
|
613
|
+
conditions: [
|
|
614
|
+
{
|
|
615
|
+
field: "command",
|
|
616
|
+
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
617
|
+
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
618
|
+
op: "matches",
|
|
619
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
620
|
+
}
|
|
621
|
+
],
|
|
622
|
+
verdict: "review",
|
|
623
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
624
|
+
}
|
|
625
|
+
],
|
|
626
|
+
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
627
|
+
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
628
|
+
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
629
|
+
dangerousWords: ["wipefs"]
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
function resolveShieldName(input) {
|
|
633
|
+
const lower = input.toLowerCase();
|
|
634
|
+
if (SHIELDS[lower]) return lower;
|
|
635
|
+
for (const [name, def] of Object.entries(SHIELDS)) {
|
|
636
|
+
if (def.aliases.includes(lower)) return name;
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
function getShield(name) {
|
|
641
|
+
const resolved = resolveShieldName(name);
|
|
642
|
+
return resolved ? SHIELDS[resolved] : null;
|
|
643
|
+
}
|
|
644
|
+
function listShields() {
|
|
645
|
+
return Object.values(SHIELDS);
|
|
646
|
+
}
|
|
647
|
+
var SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
|
|
648
|
+
function readActiveShields() {
|
|
649
|
+
try {
|
|
650
|
+
const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
651
|
+
if (!raw.trim()) return [];
|
|
652
|
+
const parsed = JSON.parse(raw);
|
|
653
|
+
if (Array.isArray(parsed.active)) {
|
|
654
|
+
return parsed.active.filter(
|
|
655
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
} catch (err) {
|
|
659
|
+
if (err.code !== "ENOENT") {
|
|
660
|
+
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
661
|
+
`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
function writeActiveShields(active) {
|
|
667
|
+
import_fs.default.mkdirSync(import_path3.default.dirname(SHIELDS_STATE_FILE), { recursive: true });
|
|
668
|
+
const tmp = `${SHIELDS_STATE_FILE}.${import_crypto.default.randomBytes(6).toString("hex")}.tmp`;
|
|
669
|
+
import_fs.default.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
|
|
670
|
+
import_fs.default.renameSync(tmp, SHIELDS_STATE_FILE);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/dlp.ts
|
|
674
|
+
var DLP_PATTERNS = [
|
|
675
|
+
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
676
|
+
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
677
|
+
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
|
|
678
|
+
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
679
|
+
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
680
|
+
{
|
|
681
|
+
name: "Private Key (PEM)",
|
|
682
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
683
|
+
severity: "block"
|
|
684
|
+
},
|
|
685
|
+
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
686
|
+
];
|
|
687
|
+
function maskSecret(raw, pattern) {
|
|
688
|
+
const match = raw.match(pattern);
|
|
689
|
+
if (!match) return "****";
|
|
690
|
+
const secret = match[0];
|
|
691
|
+
if (secret.length < 8) return "****";
|
|
692
|
+
const prefix = secret.slice(0, 4);
|
|
693
|
+
const suffix = secret.slice(-4);
|
|
694
|
+
const stars = "*".repeat(Math.min(secret.length - 8, 12));
|
|
695
|
+
return `${prefix}${stars}${suffix}`;
|
|
696
|
+
}
|
|
697
|
+
var MAX_DEPTH = 5;
|
|
698
|
+
var MAX_STRING_BYTES = 1e5;
|
|
699
|
+
var MAX_JSON_PARSE_BYTES = 1e4;
|
|
700
|
+
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
701
|
+
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
702
|
+
if (Array.isArray(args)) {
|
|
703
|
+
for (let i = 0; i < args.length; i++) {
|
|
704
|
+
const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
|
|
705
|
+
if (match) return match;
|
|
706
|
+
}
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
if (typeof args === "object") {
|
|
710
|
+
for (const [key, value] of Object.entries(args)) {
|
|
711
|
+
const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
|
|
712
|
+
if (match) return match;
|
|
713
|
+
}
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
if (typeof args === "string") {
|
|
717
|
+
const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
|
|
718
|
+
for (const pattern of DLP_PATTERNS) {
|
|
719
|
+
if (pattern.regex.test(text)) {
|
|
720
|
+
return {
|
|
721
|
+
patternName: pattern.name,
|
|
722
|
+
fieldPath,
|
|
723
|
+
redactedSample: maskSecret(text, pattern.regex),
|
|
724
|
+
severity: pattern.severity
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (text.length < MAX_JSON_PARSE_BYTES) {
|
|
729
|
+
const trimmed = text.trim();
|
|
730
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
731
|
+
try {
|
|
732
|
+
const parsed = JSON.parse(text);
|
|
733
|
+
const inner = scanArgs(parsed, depth + 1, fieldPath);
|
|
734
|
+
if (inner) return inner;
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
|
|
461
743
|
// src/core.ts
|
|
462
|
-
var PAUSED_FILE =
|
|
463
|
-
var TRUST_FILE =
|
|
464
|
-
var LOCAL_AUDIT_LOG =
|
|
465
|
-
var HOOK_DEBUG_LOG =
|
|
744
|
+
var PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
|
|
745
|
+
var TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
|
|
746
|
+
var LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
|
|
747
|
+
var HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
|
|
466
748
|
function checkPause() {
|
|
467
749
|
try {
|
|
468
|
-
if (!
|
|
469
|
-
const state = JSON.parse(
|
|
750
|
+
if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
751
|
+
const state = JSON.parse(import_fs2.default.readFileSync(PAUSED_FILE, "utf-8"));
|
|
470
752
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
471
753
|
try {
|
|
472
|
-
|
|
754
|
+
import_fs2.default.unlinkSync(PAUSED_FILE);
|
|
473
755
|
} catch {
|
|
474
756
|
}
|
|
475
757
|
return { paused: false };
|
|
@@ -480,11 +762,11 @@ function checkPause() {
|
|
|
480
762
|
}
|
|
481
763
|
}
|
|
482
764
|
function atomicWriteSync(filePath, data, options) {
|
|
483
|
-
const dir =
|
|
484
|
-
if (!
|
|
485
|
-
const tmpPath = `${filePath}.${
|
|
486
|
-
|
|
487
|
-
|
|
765
|
+
const dir = import_path4.default.dirname(filePath);
|
|
766
|
+
if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
|
|
767
|
+
const tmpPath = `${filePath}.${import_os2.default.hostname()}.${process.pid}.tmp`;
|
|
768
|
+
import_fs2.default.writeFileSync(tmpPath, data, options);
|
|
769
|
+
import_fs2.default.renameSync(tmpPath, filePath);
|
|
488
770
|
}
|
|
489
771
|
function pauseNode9(durationMs, durationStr) {
|
|
490
772
|
const state = { expiry: Date.now() + durationMs, duration: durationStr };
|
|
@@ -492,18 +774,18 @@ function pauseNode9(durationMs, durationStr) {
|
|
|
492
774
|
}
|
|
493
775
|
function resumeNode9() {
|
|
494
776
|
try {
|
|
495
|
-
if (
|
|
777
|
+
if (import_fs2.default.existsSync(PAUSED_FILE)) import_fs2.default.unlinkSync(PAUSED_FILE);
|
|
496
778
|
} catch {
|
|
497
779
|
}
|
|
498
780
|
}
|
|
499
781
|
function getActiveTrustSession(toolName) {
|
|
500
782
|
try {
|
|
501
|
-
if (!
|
|
502
|
-
const trust = JSON.parse(
|
|
783
|
+
if (!import_fs2.default.existsSync(TRUST_FILE)) return false;
|
|
784
|
+
const trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
503
785
|
const now = Date.now();
|
|
504
786
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
505
787
|
if (active.length !== trust.entries.length) {
|
|
506
|
-
|
|
788
|
+
import_fs2.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
507
789
|
}
|
|
508
790
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
509
791
|
} catch {
|
|
@@ -514,8 +796,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
514
796
|
try {
|
|
515
797
|
let trust = { entries: [] };
|
|
516
798
|
try {
|
|
517
|
-
if (
|
|
518
|
-
trust = JSON.parse(
|
|
799
|
+
if (import_fs2.default.existsSync(TRUST_FILE)) {
|
|
800
|
+
trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
519
801
|
}
|
|
520
802
|
} catch {
|
|
521
803
|
}
|
|
@@ -531,9 +813,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
531
813
|
}
|
|
532
814
|
function appendToLog(logPath, entry) {
|
|
533
815
|
try {
|
|
534
|
-
const dir =
|
|
535
|
-
if (!
|
|
536
|
-
|
|
816
|
+
const dir = import_path4.default.dirname(logPath);
|
|
817
|
+
if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
|
|
818
|
+
import_fs2.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
537
819
|
} catch {
|
|
538
820
|
}
|
|
539
821
|
}
|
|
@@ -545,7 +827,7 @@ function appendHookDebug(toolName, args, meta) {
|
|
|
545
827
|
args: safeArgs,
|
|
546
828
|
agent: meta?.agent,
|
|
547
829
|
mcpServer: meta?.mcpServer,
|
|
548
|
-
hostname:
|
|
830
|
+
hostname: import_os2.default.hostname(),
|
|
549
831
|
cwd: process.cwd()
|
|
550
832
|
});
|
|
551
833
|
}
|
|
@@ -559,7 +841,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
|
559
841
|
checkedBy,
|
|
560
842
|
agent: meta?.agent,
|
|
561
843
|
mcpServer: meta?.mcpServer,
|
|
562
|
-
hostname:
|
|
844
|
+
hostname: import_os2.default.hostname()
|
|
563
845
|
});
|
|
564
846
|
}
|
|
565
847
|
function tokenize(toolName) {
|
|
@@ -575,9 +857,9 @@ function matchesPattern(text, patterns) {
|
|
|
575
857
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
576
858
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
577
859
|
}
|
|
578
|
-
function getNestedValue(obj,
|
|
860
|
+
function getNestedValue(obj, path9) {
|
|
579
861
|
if (!obj || typeof obj !== "object") return null;
|
|
580
|
-
return
|
|
862
|
+
return path9.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
581
863
|
}
|
|
582
864
|
function shouldSnapshot(toolName, args, config) {
|
|
583
865
|
if (!config.settings.enableUndo) return false;
|
|
@@ -622,6 +904,10 @@ function evaluateSmartConditions(args, rule) {
|
|
|
622
904
|
return true;
|
|
623
905
|
}
|
|
624
906
|
}
|
|
907
|
+
case "matchesGlob":
|
|
908
|
+
return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
|
|
909
|
+
case "notMatchesGlob":
|
|
910
|
+
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : true;
|
|
625
911
|
default:
|
|
626
912
|
return false;
|
|
627
913
|
}
|
|
@@ -785,25 +1071,27 @@ var DEFAULT_CONFIG = {
|
|
|
785
1071
|
onlyPaths: [],
|
|
786
1072
|
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
787
1073
|
},
|
|
788
|
-
rules: [
|
|
789
|
-
// Only use the legacy rules format for simple path-based rm control.
|
|
790
|
-
// All other command-level enforcement lives in smartRules below.
|
|
791
|
-
{
|
|
792
|
-
action: "rm",
|
|
793
|
-
allowPaths: [
|
|
794
|
-
"**/node_modules/**",
|
|
795
|
-
"dist/**",
|
|
796
|
-
"build/**",
|
|
797
|
-
".next/**",
|
|
798
|
-
"coverage/**",
|
|
799
|
-
".cache/**",
|
|
800
|
-
"tmp/**",
|
|
801
|
-
"temp/**",
|
|
802
|
-
".DS_Store"
|
|
803
|
-
]
|
|
804
|
-
}
|
|
805
|
-
],
|
|
806
1074
|
smartRules: [
|
|
1075
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
1076
|
+
{
|
|
1077
|
+
name: "block-rm-rf-home",
|
|
1078
|
+
tool: "bash",
|
|
1079
|
+
conditionMode: "all",
|
|
1080
|
+
conditions: [
|
|
1081
|
+
{
|
|
1082
|
+
field: "command",
|
|
1083
|
+
op: "matches",
|
|
1084
|
+
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
field: "command",
|
|
1088
|
+
op: "matches",
|
|
1089
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
1090
|
+
}
|
|
1091
|
+
],
|
|
1092
|
+
verdict: "block",
|
|
1093
|
+
reason: "Recursive delete of home directory is irreversible"
|
|
1094
|
+
},
|
|
807
1095
|
// ── SQL safety ────────────────────────────────────────────────────────
|
|
808
1096
|
{
|
|
809
1097
|
name: "no-delete-without-where",
|
|
@@ -894,19 +1182,45 @@ var DEFAULT_CONFIG = {
|
|
|
894
1182
|
verdict: "block",
|
|
895
1183
|
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
896
1184
|
}
|
|
897
|
-
]
|
|
1185
|
+
],
|
|
1186
|
+
dlp: { enabled: true, scanIgnoredTools: true }
|
|
898
1187
|
},
|
|
899
1188
|
environments: {}
|
|
900
1189
|
};
|
|
1190
|
+
var ADVISORY_SMART_RULES = [
|
|
1191
|
+
{
|
|
1192
|
+
name: "allow-rm-safe-paths",
|
|
1193
|
+
tool: "*",
|
|
1194
|
+
conditionMode: "all",
|
|
1195
|
+
conditions: [
|
|
1196
|
+
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
1197
|
+
{
|
|
1198
|
+
field: "command",
|
|
1199
|
+
op: "matches",
|
|
1200
|
+
// Matches known-safe build artifact paths in the command.
|
|
1201
|
+
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
1202
|
+
}
|
|
1203
|
+
],
|
|
1204
|
+
verdict: "allow",
|
|
1205
|
+
reason: "Deleting a known-safe build artifact path"
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
name: "review-rm",
|
|
1209
|
+
tool: "*",
|
|
1210
|
+
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
1211
|
+
verdict: "review",
|
|
1212
|
+
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
1213
|
+
}
|
|
1214
|
+
];
|
|
901
1215
|
var cachedConfig = null;
|
|
902
1216
|
function _resetConfigCache() {
|
|
903
1217
|
cachedConfig = null;
|
|
904
1218
|
}
|
|
905
1219
|
function getGlobalSettings() {
|
|
906
1220
|
try {
|
|
907
|
-
const globalConfigPath =
|
|
908
|
-
if (
|
|
909
|
-
const parsed = JSON.parse(
|
|
1221
|
+
const globalConfigPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
|
|
1222
|
+
if (import_fs2.default.existsSync(globalConfigPath)) {
|
|
1223
|
+
const parsed = JSON.parse(import_fs2.default.readFileSync(globalConfigPath, "utf-8"));
|
|
910
1224
|
const settings = parsed.settings || {};
|
|
911
1225
|
return {
|
|
912
1226
|
mode: settings.mode || "standard",
|
|
@@ -928,9 +1242,9 @@ function getGlobalSettings() {
|
|
|
928
1242
|
}
|
|
929
1243
|
function getInternalToken() {
|
|
930
1244
|
try {
|
|
931
|
-
const pidFile =
|
|
932
|
-
if (!
|
|
933
|
-
const data = JSON.parse(
|
|
1245
|
+
const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
|
|
1246
|
+
if (!import_fs2.default.existsSync(pidFile)) return null;
|
|
1247
|
+
const data = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
|
|
934
1248
|
process.kill(data.pid, 0);
|
|
935
1249
|
return data.internalToken ?? null;
|
|
936
1250
|
} catch {
|
|
@@ -945,7 +1259,8 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
945
1259
|
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
946
1260
|
);
|
|
947
1261
|
if (matchedRule) {
|
|
948
|
-
if (matchedRule.verdict === "allow")
|
|
1262
|
+
if (matchedRule.verdict === "allow")
|
|
1263
|
+
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
949
1264
|
return {
|
|
950
1265
|
decision: matchedRule.verdict,
|
|
951
1266
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -956,13 +1271,11 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
956
1271
|
}
|
|
957
1272
|
}
|
|
958
1273
|
let allTokens = [];
|
|
959
|
-
let actionTokens = [];
|
|
960
1274
|
let pathTokens = [];
|
|
961
1275
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
962
1276
|
if (shellCommand) {
|
|
963
1277
|
const analyzed = await analyzeShellCommand(shellCommand);
|
|
964
1278
|
allTokens = analyzed.allTokens;
|
|
965
|
-
actionTokens = analyzed.actions;
|
|
966
1279
|
pathTokens = analyzed.paths;
|
|
967
1280
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
968
1281
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
@@ -970,11 +1283,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
970
1283
|
}
|
|
971
1284
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
972
1285
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
973
|
-
actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
974
1286
|
}
|
|
975
1287
|
} else {
|
|
976
1288
|
allTokens = tokenize(toolName);
|
|
977
|
-
actionTokens = [toolName];
|
|
978
1289
|
if (args && typeof args === "object") {
|
|
979
1290
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
980
1291
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
@@ -997,29 +1308,6 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
997
1308
|
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
998
1309
|
if (allInSandbox) return { decision: "allow" };
|
|
999
1310
|
}
|
|
1000
|
-
for (const action of actionTokens) {
|
|
1001
|
-
const rule = config.policy.rules.find(
|
|
1002
|
-
(r) => r.action === action || matchesPattern(action, r.action)
|
|
1003
|
-
);
|
|
1004
|
-
if (rule) {
|
|
1005
|
-
if (pathTokens.length > 0) {
|
|
1006
|
-
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
1007
|
-
if (anyBlocked)
|
|
1008
|
-
return {
|
|
1009
|
-
decision: "review",
|
|
1010
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
|
|
1011
|
-
tier: 5
|
|
1012
|
-
};
|
|
1013
|
-
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
1014
|
-
if (allAllowed) return { decision: "allow" };
|
|
1015
|
-
}
|
|
1016
|
-
return {
|
|
1017
|
-
decision: "review",
|
|
1018
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
|
|
1019
|
-
tier: 5
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
1311
|
let matchedDangerousWord;
|
|
1024
1312
|
const isDangerous = allTokens.some(
|
|
1025
1313
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
@@ -1071,9 +1359,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
1071
1359
|
}
|
|
1072
1360
|
async function explainPolicy(toolName, args) {
|
|
1073
1361
|
const steps = [];
|
|
1074
|
-
const globalPath =
|
|
1075
|
-
const projectPath =
|
|
1076
|
-
const credsPath =
|
|
1362
|
+
const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
|
|
1363
|
+
const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
|
|
1364
|
+
const credsPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
|
|
1077
1365
|
const waterfall = [
|
|
1078
1366
|
{
|
|
1079
1367
|
tier: 1,
|
|
@@ -1084,19 +1372,19 @@ async function explainPolicy(toolName, args) {
|
|
|
1084
1372
|
{
|
|
1085
1373
|
tier: 2,
|
|
1086
1374
|
label: "Cloud policy",
|
|
1087
|
-
status:
|
|
1088
|
-
note:
|
|
1375
|
+
status: import_fs2.default.existsSync(credsPath) ? "active" : "missing",
|
|
1376
|
+
note: import_fs2.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
|
|
1089
1377
|
},
|
|
1090
1378
|
{
|
|
1091
1379
|
tier: 3,
|
|
1092
1380
|
label: "Project config",
|
|
1093
|
-
status:
|
|
1381
|
+
status: import_fs2.default.existsSync(projectPath) ? "active" : "missing",
|
|
1094
1382
|
path: projectPath
|
|
1095
1383
|
},
|
|
1096
1384
|
{
|
|
1097
1385
|
tier: 4,
|
|
1098
1386
|
label: "Global config",
|
|
1099
|
-
status:
|
|
1387
|
+
status: import_fs2.default.existsSync(globalPath) ? "active" : "missing",
|
|
1100
1388
|
path: globalPath
|
|
1101
1389
|
},
|
|
1102
1390
|
{
|
|
@@ -1107,7 +1395,28 @@ async function explainPolicy(toolName, args) {
|
|
|
1107
1395
|
}
|
|
1108
1396
|
];
|
|
1109
1397
|
const config = getConfig();
|
|
1110
|
-
|
|
1398
|
+
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
1399
|
+
if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
|
|
1400
|
+
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
1401
|
+
if (dlpMatch) {
|
|
1402
|
+
steps.push({
|
|
1403
|
+
name: "DLP Content Scanner",
|
|
1404
|
+
outcome: dlpMatch.severity === "block" ? "block" : "review",
|
|
1405
|
+
detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
|
|
1406
|
+
isFinal: dlpMatch.severity === "block"
|
|
1407
|
+
});
|
|
1408
|
+
if (dlpMatch.severity === "block") {
|
|
1409
|
+
return { tool: toolName, args, waterfall, steps, decision: "block" };
|
|
1410
|
+
}
|
|
1411
|
+
} else {
|
|
1412
|
+
steps.push({
|
|
1413
|
+
name: "DLP Content Scanner",
|
|
1414
|
+
outcome: "checked",
|
|
1415
|
+
detail: "No sensitive credentials detected in args"
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (wouldBeIgnored) {
|
|
1111
1420
|
steps.push({
|
|
1112
1421
|
name: "Ignored tools",
|
|
1113
1422
|
outcome: "allow",
|
|
@@ -1160,13 +1469,11 @@ async function explainPolicy(toolName, args) {
|
|
|
1160
1469
|
steps.push({ name: "Smart rules", outcome: "skip", detail: "No smart rules configured" });
|
|
1161
1470
|
}
|
|
1162
1471
|
let allTokens = [];
|
|
1163
|
-
let actionTokens = [];
|
|
1164
1472
|
let pathTokens = [];
|
|
1165
1473
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
1166
1474
|
if (shellCommand) {
|
|
1167
1475
|
const analyzed = await analyzeShellCommand(shellCommand);
|
|
1168
1476
|
allTokens = analyzed.allTokens;
|
|
1169
|
-
actionTokens = analyzed.actions;
|
|
1170
1477
|
pathTokens = analyzed.paths;
|
|
1171
1478
|
const patterns = Object.keys(config.policy.toolInspection);
|
|
1172
1479
|
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
@@ -1200,7 +1507,6 @@ async function explainPolicy(toolName, args) {
|
|
|
1200
1507
|
});
|
|
1201
1508
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
1202
1509
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1203
|
-
actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1204
1510
|
steps.push({
|
|
1205
1511
|
name: "SQL token stripping",
|
|
1206
1512
|
outcome: "checked",
|
|
@@ -1209,7 +1515,6 @@ async function explainPolicy(toolName, args) {
|
|
|
1209
1515
|
}
|
|
1210
1516
|
} else {
|
|
1211
1517
|
allTokens = tokenize(toolName);
|
|
1212
|
-
actionTokens = [toolName];
|
|
1213
1518
|
let detail = `No toolInspection match for "${toolName}" \u2014 tokens: [${allTokens.join(", ")}]`;
|
|
1214
1519
|
if (args && typeof args === "object") {
|
|
1215
1520
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
@@ -1250,65 +1555,6 @@ async function explainPolicy(toolName, args) {
|
|
|
1250
1555
|
detail: pathTokens.length === 0 ? "No path tokens found in input" : "No sandbox paths configured"
|
|
1251
1556
|
});
|
|
1252
1557
|
}
|
|
1253
|
-
let ruleMatched = false;
|
|
1254
|
-
for (const action of actionTokens) {
|
|
1255
|
-
const rule = config.policy.rules.find(
|
|
1256
|
-
(r) => r.action === action || matchesPattern(action, r.action)
|
|
1257
|
-
);
|
|
1258
|
-
if (rule) {
|
|
1259
|
-
ruleMatched = true;
|
|
1260
|
-
if (pathTokens.length > 0) {
|
|
1261
|
-
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
1262
|
-
if (anyBlocked) {
|
|
1263
|
-
steps.push({
|
|
1264
|
-
name: "Policy rules",
|
|
1265
|
-
outcome: "review",
|
|
1266
|
-
detail: `Rule "${rule.action}" matched + path is in blockPaths`,
|
|
1267
|
-
isFinal: true
|
|
1268
|
-
});
|
|
1269
|
-
return {
|
|
1270
|
-
tool: toolName,
|
|
1271
|
-
args,
|
|
1272
|
-
waterfall,
|
|
1273
|
-
steps,
|
|
1274
|
-
decision: "review",
|
|
1275
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
1279
|
-
if (allAllowed) {
|
|
1280
|
-
steps.push({
|
|
1281
|
-
name: "Policy rules",
|
|
1282
|
-
outcome: "allow",
|
|
1283
|
-
detail: `Rule "${rule.action}" matched + all paths are in allowPaths`,
|
|
1284
|
-
isFinal: true
|
|
1285
|
-
});
|
|
1286
|
-
return { tool: toolName, args, waterfall, steps, decision: "allow" };
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
steps.push({
|
|
1290
|
-
name: "Policy rules",
|
|
1291
|
-
outcome: "review",
|
|
1292
|
-
detail: `Rule "${rule.action}" matched \u2014 default block (no path exception)`,
|
|
1293
|
-
isFinal: true
|
|
1294
|
-
});
|
|
1295
|
-
return {
|
|
1296
|
-
tool: toolName,
|
|
1297
|
-
args,
|
|
1298
|
-
waterfall,
|
|
1299
|
-
steps,
|
|
1300
|
-
decision: "review",
|
|
1301
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
if (!ruleMatched) {
|
|
1306
|
-
steps.push({
|
|
1307
|
-
name: "Policy rules",
|
|
1308
|
-
outcome: "skip",
|
|
1309
|
-
detail: config.policy.rules.length === 0 ? "No rules configured" : `No rule matched [${actionTokens.join(", ")}]`
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
1558
|
let matchedDangerousWord;
|
|
1313
1559
|
const isDangerous = uniqueTokens.some(
|
|
1314
1560
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
@@ -1377,9 +1623,9 @@ var DAEMON_PORT = 7391;
|
|
|
1377
1623
|
var DAEMON_HOST = "127.0.0.1";
|
|
1378
1624
|
function isDaemonRunning() {
|
|
1379
1625
|
try {
|
|
1380
|
-
const pidFile =
|
|
1381
|
-
if (!
|
|
1382
|
-
const { pid, port } = JSON.parse(
|
|
1626
|
+
const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
|
|
1627
|
+
if (!import_fs2.default.existsSync(pidFile)) return false;
|
|
1628
|
+
const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
|
|
1383
1629
|
if (port !== DAEMON_PORT) return false;
|
|
1384
1630
|
process.kill(pid, 0);
|
|
1385
1631
|
return true;
|
|
@@ -1389,9 +1635,9 @@ function isDaemonRunning() {
|
|
|
1389
1635
|
}
|
|
1390
1636
|
function getPersistentDecision(toolName) {
|
|
1391
1637
|
try {
|
|
1392
|
-
const file =
|
|
1393
|
-
if (!
|
|
1394
|
-
const decisions = JSON.parse(
|
|
1638
|
+
const file = import_path4.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
|
|
1639
|
+
if (!import_fs2.default.existsSync(file)) return null;
|
|
1640
|
+
const decisions = JSON.parse(import_fs2.default.readFileSync(file, "utf-8"));
|
|
1395
1641
|
const d = decisions[toolName];
|
|
1396
1642
|
if (d === "allow" || d === "deny") return d;
|
|
1397
1643
|
} catch {
|
|
@@ -1490,6 +1736,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1490
1736
|
let policyMatchedField;
|
|
1491
1737
|
let policyMatchedWord;
|
|
1492
1738
|
let riskMetadata;
|
|
1739
|
+
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
1740
|
+
const dlpMatch = scanArgs(args);
|
|
1741
|
+
if (dlpMatch) {
|
|
1742
|
+
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
1743
|
+
if (dlpMatch.severity === "block") {
|
|
1744
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
|
|
1745
|
+
return {
|
|
1746
|
+
approved: false,
|
|
1747
|
+
reason: dlpReason,
|
|
1748
|
+
blockedBy: "local-config",
|
|
1749
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1493
1755
|
if (config.settings.mode === "audit") {
|
|
1494
1756
|
if (!isIgnoredTool(toolName)) {
|
|
1495
1757
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
@@ -1729,7 +1991,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1729
1991
|
racePromises.push(
|
|
1730
1992
|
(async () => {
|
|
1731
1993
|
try {
|
|
1732
|
-
|
|
1994
|
+
if (explainableLabel.includes("DLP")) {
|
|
1995
|
+
console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
1996
|
+
console.log(
|
|
1997
|
+
import_chalk2.default.red.bold(` A sensitive secret was detected in the tool arguments!`)
|
|
1998
|
+
);
|
|
1999
|
+
} else {
|
|
2000
|
+
console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
2001
|
+
}
|
|
1733
2002
|
console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
|
|
1734
2003
|
console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
|
|
1735
2004
|
if (isRemoteLocked) {
|
|
@@ -1834,8 +2103,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1834
2103
|
}
|
|
1835
2104
|
function getConfig() {
|
|
1836
2105
|
if (cachedConfig) return cachedConfig;
|
|
1837
|
-
const globalPath =
|
|
1838
|
-
const projectPath =
|
|
2106
|
+
const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
|
|
2107
|
+
const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
|
|
1839
2108
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1840
2109
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1841
2110
|
const mergedSettings = {
|
|
@@ -1847,13 +2116,13 @@ function getConfig() {
|
|
|
1847
2116
|
dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
|
|
1848
2117
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
1849
2118
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
1850
|
-
rules: [...DEFAULT_CONFIG.policy.rules],
|
|
1851
2119
|
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
1852
2120
|
snapshot: {
|
|
1853
2121
|
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
1854
2122
|
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
1855
2123
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1856
|
-
}
|
|
2124
|
+
},
|
|
2125
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp }
|
|
1857
2126
|
};
|
|
1858
2127
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
1859
2128
|
const applyLayer = (source) => {
|
|
@@ -1873,7 +2142,6 @@ function getConfig() {
|
|
|
1873
2142
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
1874
2143
|
if (p.toolInspection)
|
|
1875
2144
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
1876
|
-
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
1877
2145
|
if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
|
|
1878
2146
|
if (p.snapshot) {
|
|
1879
2147
|
const s2 = p.snapshot;
|
|
@@ -1881,6 +2149,11 @@ function getConfig() {
|
|
|
1881
2149
|
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1882
2150
|
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1883
2151
|
}
|
|
2152
|
+
if (p.dlp) {
|
|
2153
|
+
const d = p.dlp;
|
|
2154
|
+
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
2155
|
+
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
2156
|
+
}
|
|
1884
2157
|
const envs = source.environments || {};
|
|
1885
2158
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
1886
2159
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -1895,6 +2168,19 @@ function getConfig() {
|
|
|
1895
2168
|
};
|
|
1896
2169
|
applyLayer(globalConfig);
|
|
1897
2170
|
applyLayer(projectConfig);
|
|
2171
|
+
for (const shieldName of readActiveShields()) {
|
|
2172
|
+
const shield = getShield(shieldName);
|
|
2173
|
+
if (!shield) continue;
|
|
2174
|
+
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2175
|
+
for (const rule of shield.smartRules) {
|
|
2176
|
+
if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2177
|
+
}
|
|
2178
|
+
for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
|
|
2179
|
+
}
|
|
2180
|
+
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2181
|
+
for (const rule of ADVISORY_SMART_RULES) {
|
|
2182
|
+
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2183
|
+
}
|
|
1898
2184
|
if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
|
|
1899
2185
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
1900
2186
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
@@ -1910,10 +2196,10 @@ function getConfig() {
|
|
|
1910
2196
|
return cachedConfig;
|
|
1911
2197
|
}
|
|
1912
2198
|
function tryLoadConfig(filePath) {
|
|
1913
|
-
if (!
|
|
2199
|
+
if (!import_fs2.default.existsSync(filePath)) return null;
|
|
1914
2200
|
let raw;
|
|
1915
2201
|
try {
|
|
1916
|
-
raw = JSON.parse(
|
|
2202
|
+
raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
|
|
1917
2203
|
} catch (err) {
|
|
1918
2204
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1919
2205
|
process.stderr.write(
|
|
@@ -1975,9 +2261,9 @@ function getCredentials() {
|
|
|
1975
2261
|
};
|
|
1976
2262
|
}
|
|
1977
2263
|
try {
|
|
1978
|
-
const credPath =
|
|
1979
|
-
if (
|
|
1980
|
-
const creds = JSON.parse(
|
|
2264
|
+
const credPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
|
|
2265
|
+
if (import_fs2.default.existsSync(credPath)) {
|
|
2266
|
+
const creds = JSON.parse(import_fs2.default.readFileSync(credPath, "utf-8"));
|
|
1981
2267
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
1982
2268
|
const profile = creds[profileName];
|
|
1983
2269
|
if (profile?.apiKey) {
|
|
@@ -2008,9 +2294,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
2008
2294
|
context: {
|
|
2009
2295
|
agent: meta?.agent,
|
|
2010
2296
|
mcpServer: meta?.mcpServer,
|
|
2011
|
-
hostname:
|
|
2297
|
+
hostname: import_os2.default.hostname(),
|
|
2012
2298
|
cwd: process.cwd(),
|
|
2013
|
-
platform:
|
|
2299
|
+
platform: import_os2.default.platform()
|
|
2014
2300
|
}
|
|
2015
2301
|
}),
|
|
2016
2302
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -2031,9 +2317,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
2031
2317
|
context: {
|
|
2032
2318
|
agent: meta?.agent,
|
|
2033
2319
|
mcpServer: meta?.mcpServer,
|
|
2034
|
-
hostname:
|
|
2320
|
+
hostname: import_os2.default.hostname(),
|
|
2035
2321
|
cwd: process.cwd(),
|
|
2036
|
-
platform:
|
|
2322
|
+
platform: import_os2.default.platform()
|
|
2037
2323
|
},
|
|
2038
2324
|
...riskMetadata && { riskMetadata }
|
|
2039
2325
|
}),
|
|
@@ -2092,9 +2378,9 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
2092
2378
|
}
|
|
2093
2379
|
|
|
2094
2380
|
// src/setup.ts
|
|
2095
|
-
var
|
|
2096
|
-
var
|
|
2097
|
-
var
|
|
2381
|
+
var import_fs3 = __toESM(require("fs"));
|
|
2382
|
+
var import_path5 = __toESM(require("path"));
|
|
2383
|
+
var import_os3 = __toESM(require("os"));
|
|
2098
2384
|
var import_chalk3 = __toESM(require("chalk"));
|
|
2099
2385
|
var import_prompts2 = require("@inquirer/prompts");
|
|
2100
2386
|
function printDaemonTip() {
|
|
@@ -2110,22 +2396,22 @@ function fullPathCommand(subcommand) {
|
|
|
2110
2396
|
}
|
|
2111
2397
|
function readJson(filePath) {
|
|
2112
2398
|
try {
|
|
2113
|
-
if (
|
|
2114
|
-
return JSON.parse(
|
|
2399
|
+
if (import_fs3.default.existsSync(filePath)) {
|
|
2400
|
+
return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
|
|
2115
2401
|
}
|
|
2116
2402
|
} catch {
|
|
2117
2403
|
}
|
|
2118
2404
|
return null;
|
|
2119
2405
|
}
|
|
2120
2406
|
function writeJson(filePath, data) {
|
|
2121
|
-
const dir =
|
|
2122
|
-
if (!
|
|
2123
|
-
|
|
2407
|
+
const dir = import_path5.default.dirname(filePath);
|
|
2408
|
+
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
2409
|
+
import_fs3.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
2124
2410
|
}
|
|
2125
2411
|
async function setupClaude() {
|
|
2126
|
-
const homeDir2 =
|
|
2127
|
-
const mcpPath =
|
|
2128
|
-
const hooksPath =
|
|
2412
|
+
const homeDir2 = import_os3.default.homedir();
|
|
2413
|
+
const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
|
|
2414
|
+
const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
|
|
2129
2415
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
2130
2416
|
const settings = readJson(hooksPath) ?? {};
|
|
2131
2417
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -2199,8 +2485,8 @@ async function setupClaude() {
|
|
|
2199
2485
|
}
|
|
2200
2486
|
}
|
|
2201
2487
|
async function setupGemini() {
|
|
2202
|
-
const homeDir2 =
|
|
2203
|
-
const settingsPath =
|
|
2488
|
+
const homeDir2 = import_os3.default.homedir();
|
|
2489
|
+
const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
|
|
2204
2490
|
const settings = readJson(settingsPath) ?? {};
|
|
2205
2491
|
const servers = settings.mcpServers ?? {};
|
|
2206
2492
|
let anythingChanged = false;
|
|
@@ -2282,36 +2568,11 @@ async function setupGemini() {
|
|
|
2282
2568
|
}
|
|
2283
2569
|
}
|
|
2284
2570
|
async function setupCursor() {
|
|
2285
|
-
const homeDir2 =
|
|
2286
|
-
const mcpPath =
|
|
2287
|
-
const hooksPath = import_path4.default.join(homeDir2, ".cursor", "hooks.json");
|
|
2571
|
+
const homeDir2 = import_os3.default.homedir();
|
|
2572
|
+
const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
|
|
2288
2573
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
2289
|
-
const hooksFile = readJson(hooksPath) ?? { version: 1 };
|
|
2290
2574
|
const servers = mcpConfig.mcpServers ?? {};
|
|
2291
2575
|
let anythingChanged = false;
|
|
2292
|
-
if (!hooksFile.hooks) hooksFile.hooks = {};
|
|
2293
|
-
const hasPreHook = hooksFile.hooks.preToolUse?.some(
|
|
2294
|
-
(h) => h.command === "node9" && h.args?.includes("check") || h.command?.includes("cli.js")
|
|
2295
|
-
);
|
|
2296
|
-
if (!hasPreHook) {
|
|
2297
|
-
if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
|
|
2298
|
-
hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
|
|
2299
|
-
console.log(import_chalk3.default.green(" \u2705 preToolUse hook added \u2192 node9 check"));
|
|
2300
|
-
anythingChanged = true;
|
|
2301
|
-
}
|
|
2302
|
-
const hasPostHook = hooksFile.hooks.postToolUse?.some(
|
|
2303
|
-
(h) => h.command === "node9" && h.args?.includes("log") || h.command?.includes("cli.js")
|
|
2304
|
-
);
|
|
2305
|
-
if (!hasPostHook) {
|
|
2306
|
-
if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
|
|
2307
|
-
hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
|
|
2308
|
-
console.log(import_chalk3.default.green(" \u2705 postToolUse hook added \u2192 node9 log"));
|
|
2309
|
-
anythingChanged = true;
|
|
2310
|
-
}
|
|
2311
|
-
if (anythingChanged) {
|
|
2312
|
-
writeJson(hooksPath, hooksFile);
|
|
2313
|
-
console.log("");
|
|
2314
|
-
}
|
|
2315
2576
|
const serversToWrap = [];
|
|
2316
2577
|
for (const [name, server] of Object.entries(servers)) {
|
|
2317
2578
|
if (!server.command || server.command === "node9") continue;
|
|
@@ -2340,13 +2601,23 @@ async function setupCursor() {
|
|
|
2340
2601
|
}
|
|
2341
2602
|
console.log("");
|
|
2342
2603
|
}
|
|
2604
|
+
console.log(
|
|
2605
|
+
import_chalk3.default.yellow(
|
|
2606
|
+
" \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
|
|
2607
|
+
)
|
|
2608
|
+
);
|
|
2609
|
+
console.log("");
|
|
2343
2610
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2344
|
-
console.log(
|
|
2611
|
+
console.log(
|
|
2612
|
+
import_chalk3.default.blue(
|
|
2613
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
|
|
2614
|
+
)
|
|
2615
|
+
);
|
|
2345
2616
|
printDaemonTip();
|
|
2346
2617
|
return;
|
|
2347
2618
|
}
|
|
2348
2619
|
if (anythingChanged) {
|
|
2349
|
-
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
|
|
2620
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
|
|
2350
2621
|
console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
|
|
2351
2622
|
printDaemonTip();
|
|
2352
2623
|
}
|
|
@@ -3396,34 +3667,34 @@ var UI_HTML_TEMPLATE = ui_default;
|
|
|
3396
3667
|
|
|
3397
3668
|
// src/daemon/index.ts
|
|
3398
3669
|
var import_http = __toESM(require("http"));
|
|
3399
|
-
var
|
|
3400
|
-
var
|
|
3401
|
-
var
|
|
3670
|
+
var import_fs4 = __toESM(require("fs"));
|
|
3671
|
+
var import_path6 = __toESM(require("path"));
|
|
3672
|
+
var import_os4 = __toESM(require("os"));
|
|
3402
3673
|
var import_child_process2 = require("child_process");
|
|
3403
|
-
var
|
|
3674
|
+
var import_crypto2 = require("crypto");
|
|
3404
3675
|
var import_chalk4 = __toESM(require("chalk"));
|
|
3405
3676
|
var DAEMON_PORT2 = 7391;
|
|
3406
3677
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
3407
|
-
var homeDir =
|
|
3408
|
-
var DAEMON_PID_FILE =
|
|
3409
|
-
var DECISIONS_FILE =
|
|
3410
|
-
var GLOBAL_CONFIG_FILE =
|
|
3411
|
-
var CREDENTIALS_FILE =
|
|
3412
|
-
var AUDIT_LOG_FILE =
|
|
3413
|
-
var TRUST_FILE2 =
|
|
3678
|
+
var homeDir = import_os4.default.homedir();
|
|
3679
|
+
var DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
|
|
3680
|
+
var DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
|
|
3681
|
+
var GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
|
|
3682
|
+
var CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
|
|
3683
|
+
var AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
|
|
3684
|
+
var TRUST_FILE2 = import_path6.default.join(homeDir, ".node9", "trust.json");
|
|
3414
3685
|
function atomicWriteSync2(filePath, data, options) {
|
|
3415
|
-
const dir =
|
|
3416
|
-
if (!
|
|
3417
|
-
const tmpPath = `${filePath}.${(0,
|
|
3418
|
-
|
|
3419
|
-
|
|
3686
|
+
const dir = import_path6.default.dirname(filePath);
|
|
3687
|
+
if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
3688
|
+
const tmpPath = `${filePath}.${(0, import_crypto2.randomUUID)()}.tmp`;
|
|
3689
|
+
import_fs4.default.writeFileSync(tmpPath, data, options);
|
|
3690
|
+
import_fs4.default.renameSync(tmpPath, filePath);
|
|
3420
3691
|
}
|
|
3421
3692
|
function writeTrustEntry(toolName, durationMs) {
|
|
3422
3693
|
try {
|
|
3423
3694
|
let trust = { entries: [] };
|
|
3424
3695
|
try {
|
|
3425
|
-
if (
|
|
3426
|
-
trust = JSON.parse(
|
|
3696
|
+
if (import_fs4.default.existsSync(TRUST_FILE2))
|
|
3697
|
+
trust = JSON.parse(import_fs4.default.readFileSync(TRUST_FILE2, "utf-8"));
|
|
3427
3698
|
} catch {
|
|
3428
3699
|
}
|
|
3429
3700
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -3456,16 +3727,16 @@ function appendAuditLog(data) {
|
|
|
3456
3727
|
decision: data.decision,
|
|
3457
3728
|
source: "daemon"
|
|
3458
3729
|
};
|
|
3459
|
-
const dir =
|
|
3460
|
-
if (!
|
|
3461
|
-
|
|
3730
|
+
const dir = import_path6.default.dirname(AUDIT_LOG_FILE);
|
|
3731
|
+
if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
3732
|
+
import_fs4.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
3462
3733
|
} catch {
|
|
3463
3734
|
}
|
|
3464
3735
|
}
|
|
3465
3736
|
function getAuditHistory(limit = 20) {
|
|
3466
3737
|
try {
|
|
3467
|
-
if (!
|
|
3468
|
-
const lines =
|
|
3738
|
+
if (!import_fs4.default.existsSync(AUDIT_LOG_FILE)) return [];
|
|
3739
|
+
const lines = import_fs4.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
3469
3740
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
3470
3741
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
3471
3742
|
} catch {
|
|
@@ -3475,7 +3746,7 @@ function getAuditHistory(limit = 20) {
|
|
|
3475
3746
|
var AUTO_DENY_MS = 12e4;
|
|
3476
3747
|
function getOrgName() {
|
|
3477
3748
|
try {
|
|
3478
|
-
if (
|
|
3749
|
+
if (import_fs4.default.existsSync(CREDENTIALS_FILE)) {
|
|
3479
3750
|
return "Node9 Cloud";
|
|
3480
3751
|
}
|
|
3481
3752
|
} catch {
|
|
@@ -3484,13 +3755,13 @@ function getOrgName() {
|
|
|
3484
3755
|
}
|
|
3485
3756
|
var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
3486
3757
|
function hasStoredSlackKey() {
|
|
3487
|
-
return
|
|
3758
|
+
return import_fs4.default.existsSync(CREDENTIALS_FILE);
|
|
3488
3759
|
}
|
|
3489
3760
|
function writeGlobalSetting(key, value) {
|
|
3490
3761
|
let config = {};
|
|
3491
3762
|
try {
|
|
3492
|
-
if (
|
|
3493
|
-
config = JSON.parse(
|
|
3763
|
+
if (import_fs4.default.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
3764
|
+
config = JSON.parse(import_fs4.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
3494
3765
|
}
|
|
3495
3766
|
} catch {
|
|
3496
3767
|
}
|
|
@@ -3514,7 +3785,7 @@ function abandonPending() {
|
|
|
3514
3785
|
});
|
|
3515
3786
|
if (autoStarted) {
|
|
3516
3787
|
try {
|
|
3517
|
-
|
|
3788
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
3518
3789
|
} catch {
|
|
3519
3790
|
}
|
|
3520
3791
|
setTimeout(() => {
|
|
@@ -3552,8 +3823,8 @@ function readBody(req) {
|
|
|
3552
3823
|
}
|
|
3553
3824
|
function readPersistentDecisions() {
|
|
3554
3825
|
try {
|
|
3555
|
-
if (
|
|
3556
|
-
return JSON.parse(
|
|
3826
|
+
if (import_fs4.default.existsSync(DECISIONS_FILE)) {
|
|
3827
|
+
return JSON.parse(import_fs4.default.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
3557
3828
|
}
|
|
3558
3829
|
} catch {
|
|
3559
3830
|
}
|
|
@@ -3569,8 +3840,8 @@ function writePersistentDecision(toolName, decision) {
|
|
|
3569
3840
|
}
|
|
3570
3841
|
}
|
|
3571
3842
|
function startDaemon() {
|
|
3572
|
-
const csrfToken = (0,
|
|
3573
|
-
const internalToken = (0,
|
|
3843
|
+
const csrfToken = (0, import_crypto2.randomUUID)();
|
|
3844
|
+
const internalToken = (0, import_crypto2.randomUUID)();
|
|
3574
3845
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
3575
3846
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
3576
3847
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
@@ -3580,7 +3851,7 @@ function startDaemon() {
|
|
|
3580
3851
|
idleTimer = setTimeout(() => {
|
|
3581
3852
|
if (autoStarted) {
|
|
3582
3853
|
try {
|
|
3583
|
-
|
|
3854
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
3584
3855
|
} catch {
|
|
3585
3856
|
}
|
|
3586
3857
|
}
|
|
@@ -3651,7 +3922,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3651
3922
|
mcpServer,
|
|
3652
3923
|
riskMetadata
|
|
3653
3924
|
} = JSON.parse(body);
|
|
3654
|
-
const id = (0,
|
|
3925
|
+
const id = (0, import_crypto2.randomUUID)();
|
|
3655
3926
|
const entry = {
|
|
3656
3927
|
id,
|
|
3657
3928
|
toolName,
|
|
@@ -3898,14 +4169,14 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3898
4169
|
server.on("error", (e) => {
|
|
3899
4170
|
if (e.code === "EADDRINUSE") {
|
|
3900
4171
|
try {
|
|
3901
|
-
if (
|
|
3902
|
-
const { pid } = JSON.parse(
|
|
4172
|
+
if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
|
|
4173
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
3903
4174
|
process.kill(pid, 0);
|
|
3904
4175
|
return process.exit(0);
|
|
3905
4176
|
}
|
|
3906
4177
|
} catch {
|
|
3907
4178
|
try {
|
|
3908
|
-
|
|
4179
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
3909
4180
|
} catch {
|
|
3910
4181
|
}
|
|
3911
4182
|
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
@@ -3925,25 +4196,25 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3925
4196
|
});
|
|
3926
4197
|
}
|
|
3927
4198
|
function stopDaemon() {
|
|
3928
|
-
if (!
|
|
4199
|
+
if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
|
|
3929
4200
|
try {
|
|
3930
|
-
const { pid } = JSON.parse(
|
|
4201
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
3931
4202
|
process.kill(pid, "SIGTERM");
|
|
3932
4203
|
console.log(import_chalk4.default.green("\u2705 Stopped."));
|
|
3933
4204
|
} catch {
|
|
3934
4205
|
console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
|
|
3935
4206
|
} finally {
|
|
3936
4207
|
try {
|
|
3937
|
-
|
|
4208
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
3938
4209
|
} catch {
|
|
3939
4210
|
}
|
|
3940
4211
|
}
|
|
3941
4212
|
}
|
|
3942
4213
|
function daemonStatus() {
|
|
3943
|
-
if (!
|
|
4214
|
+
if (!import_fs4.default.existsSync(DAEMON_PID_FILE))
|
|
3944
4215
|
return console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
|
|
3945
4216
|
try {
|
|
3946
|
-
const { pid } = JSON.parse(
|
|
4217
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
3947
4218
|
process.kill(pid, 0);
|
|
3948
4219
|
console.log(import_chalk4.default.green("Node9 daemon: running"));
|
|
3949
4220
|
} catch {
|
|
@@ -3957,30 +4228,30 @@ var import_execa = require("execa");
|
|
|
3957
4228
|
var import_execa2 = require("execa");
|
|
3958
4229
|
var import_chalk5 = __toESM(require("chalk"));
|
|
3959
4230
|
var import_readline = __toESM(require("readline"));
|
|
3960
|
-
var
|
|
3961
|
-
var
|
|
3962
|
-
var
|
|
4231
|
+
var import_fs6 = __toESM(require("fs"));
|
|
4232
|
+
var import_path8 = __toESM(require("path"));
|
|
4233
|
+
var import_os6 = __toESM(require("os"));
|
|
3963
4234
|
|
|
3964
4235
|
// src/undo.ts
|
|
3965
4236
|
var import_child_process3 = require("child_process");
|
|
3966
|
-
var
|
|
3967
|
-
var
|
|
3968
|
-
var
|
|
3969
|
-
var SNAPSHOT_STACK_PATH =
|
|
3970
|
-
var UNDO_LATEST_PATH =
|
|
4237
|
+
var import_fs5 = __toESM(require("fs"));
|
|
4238
|
+
var import_path7 = __toESM(require("path"));
|
|
4239
|
+
var import_os5 = __toESM(require("os"));
|
|
4240
|
+
var SNAPSHOT_STACK_PATH = import_path7.default.join(import_os5.default.homedir(), ".node9", "snapshots.json");
|
|
4241
|
+
var UNDO_LATEST_PATH = import_path7.default.join(import_os5.default.homedir(), ".node9", "undo_latest.txt");
|
|
3971
4242
|
var MAX_SNAPSHOTS = 10;
|
|
3972
4243
|
function readStack() {
|
|
3973
4244
|
try {
|
|
3974
|
-
if (
|
|
3975
|
-
return JSON.parse(
|
|
4245
|
+
if (import_fs5.default.existsSync(SNAPSHOT_STACK_PATH))
|
|
4246
|
+
return JSON.parse(import_fs5.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
3976
4247
|
} catch {
|
|
3977
4248
|
}
|
|
3978
4249
|
return [];
|
|
3979
4250
|
}
|
|
3980
4251
|
function writeStack(stack) {
|
|
3981
|
-
const dir =
|
|
3982
|
-
if (!
|
|
3983
|
-
|
|
4252
|
+
const dir = import_path7.default.dirname(SNAPSHOT_STACK_PATH);
|
|
4253
|
+
if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
|
|
4254
|
+
import_fs5.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
3984
4255
|
}
|
|
3985
4256
|
function buildArgsSummary(tool, args) {
|
|
3986
4257
|
if (!args || typeof args !== "object") return "";
|
|
@@ -3996,13 +4267,13 @@ function buildArgsSummary(tool, args) {
|
|
|
3996
4267
|
async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
3997
4268
|
try {
|
|
3998
4269
|
const cwd = process.cwd();
|
|
3999
|
-
if (!
|
|
4000
|
-
const tempIndex =
|
|
4270
|
+
if (!import_fs5.default.existsSync(import_path7.default.join(cwd, ".git"))) return null;
|
|
4271
|
+
const tempIndex = import_path7.default.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
4001
4272
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
4002
4273
|
(0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
|
|
4003
4274
|
const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
|
|
4004
4275
|
const treeHash = treeRes.stdout.toString().trim();
|
|
4005
|
-
if (
|
|
4276
|
+
if (import_fs5.default.existsSync(tempIndex)) import_fs5.default.unlinkSync(tempIndex);
|
|
4006
4277
|
if (!treeHash || treeRes.status !== 0) return null;
|
|
4007
4278
|
const commitRes = (0, import_child_process3.spawnSync)("git", [
|
|
4008
4279
|
"commit-tree",
|
|
@@ -4023,7 +4294,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
|
4023
4294
|
stack.push(entry);
|
|
4024
4295
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
4025
4296
|
writeStack(stack);
|
|
4026
|
-
|
|
4297
|
+
import_fs5.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
4027
4298
|
return commitHash;
|
|
4028
4299
|
} catch (err) {
|
|
4029
4300
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
|
|
@@ -4061,9 +4332,9 @@ function applyUndo(hash, cwd) {
|
|
|
4061
4332
|
const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
4062
4333
|
const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
4063
4334
|
for (const file of [...tracked, ...untracked]) {
|
|
4064
|
-
const fullPath =
|
|
4065
|
-
if (!snapshotFiles.has(file) &&
|
|
4066
|
-
|
|
4335
|
+
const fullPath = import_path7.default.join(dir, file);
|
|
4336
|
+
if (!snapshotFiles.has(file) && import_fs5.default.existsSync(fullPath)) {
|
|
4337
|
+
import_fs5.default.unlinkSync(fullPath);
|
|
4067
4338
|
}
|
|
4068
4339
|
}
|
|
4069
4340
|
return true;
|
|
@@ -4075,7 +4346,7 @@ function applyUndo(hash, cwd) {
|
|
|
4075
4346
|
// src/cli.ts
|
|
4076
4347
|
var import_prompts3 = require("@inquirer/prompts");
|
|
4077
4348
|
var { version } = JSON.parse(
|
|
4078
|
-
|
|
4349
|
+
import_fs6.default.readFileSync(import_path8.default.join(__dirname, "../package.json"), "utf-8")
|
|
4079
4350
|
);
|
|
4080
4351
|
function parseDuration(str) {
|
|
4081
4352
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -4107,6 +4378,15 @@ INSTRUCTIONS:
|
|
|
4107
4378
|
- If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
|
|
4108
4379
|
}
|
|
4109
4380
|
const label = blockedByLabel.toLowerCase();
|
|
4381
|
+
if (label.includes("dlp") || label.includes("secret detected") || label.includes("credential review")) {
|
|
4382
|
+
return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
|
|
4383
|
+
CRITICAL INSTRUCTION: Do NOT retry this action.
|
|
4384
|
+
REQUIRED ACTIONS:
|
|
4385
|
+
1. Remove the hardcoded credential from your command or code.
|
|
4386
|
+
2. Use an environment variable or a dedicated secrets manager instead.
|
|
4387
|
+
3. Treat the leaked credential as compromised and rotate it immediately.
|
|
4388
|
+
Do NOT attempt to bypass this check or pass the credential through another tool.`;
|
|
4389
|
+
}
|
|
4110
4390
|
if (label.includes("sql safety") && label.includes("delete without where")) {
|
|
4111
4391
|
return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
|
|
4112
4392
|
INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
|
|
@@ -4268,14 +4548,14 @@ async function runProxy(targetCommand) {
|
|
|
4268
4548
|
}
|
|
4269
4549
|
program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
|
|
4270
4550
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
4271
|
-
const credPath =
|
|
4272
|
-
if (!
|
|
4273
|
-
|
|
4551
|
+
const credPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "credentials.json");
|
|
4552
|
+
if (!import_fs6.default.existsSync(import_path8.default.dirname(credPath)))
|
|
4553
|
+
import_fs6.default.mkdirSync(import_path8.default.dirname(credPath), { recursive: true });
|
|
4274
4554
|
const profileName = options.profile || "default";
|
|
4275
4555
|
let existingCreds = {};
|
|
4276
4556
|
try {
|
|
4277
|
-
if (
|
|
4278
|
-
const raw = JSON.parse(
|
|
4557
|
+
if (import_fs6.default.existsSync(credPath)) {
|
|
4558
|
+
const raw = JSON.parse(import_fs6.default.readFileSync(credPath, "utf-8"));
|
|
4279
4559
|
if (raw.apiKey) {
|
|
4280
4560
|
existingCreds = {
|
|
4281
4561
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -4287,13 +4567,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4287
4567
|
} catch {
|
|
4288
4568
|
}
|
|
4289
4569
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
4290
|
-
|
|
4570
|
+
import_fs6.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
4291
4571
|
if (profileName === "default") {
|
|
4292
|
-
const configPath =
|
|
4572
|
+
const configPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
|
|
4293
4573
|
let config = {};
|
|
4294
4574
|
try {
|
|
4295
|
-
if (
|
|
4296
|
-
config = JSON.parse(
|
|
4575
|
+
if (import_fs6.default.existsSync(configPath))
|
|
4576
|
+
config = JSON.parse(import_fs6.default.readFileSync(configPath, "utf-8"));
|
|
4297
4577
|
} catch {
|
|
4298
4578
|
}
|
|
4299
4579
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -4308,9 +4588,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4308
4588
|
approvers.cloud = false;
|
|
4309
4589
|
}
|
|
4310
4590
|
s.approvers = approvers;
|
|
4311
|
-
if (!
|
|
4312
|
-
|
|
4313
|
-
|
|
4591
|
+
if (!import_fs6.default.existsSync(import_path8.default.dirname(configPath)))
|
|
4592
|
+
import_fs6.default.mkdirSync(import_path8.default.dirname(configPath), { recursive: true });
|
|
4593
|
+
import_fs6.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
4314
4594
|
}
|
|
4315
4595
|
if (options.profile && profileName !== "default") {
|
|
4316
4596
|
console.log(import_chalk5.default.green(`\u2705 Profile "${profileName}" saved`));
|
|
@@ -4349,7 +4629,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
4349
4629
|
process.exit(1);
|
|
4350
4630
|
});
|
|
4351
4631
|
program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
4352
|
-
const homeDir2 =
|
|
4632
|
+
const homeDir2 = import_os6.default.homedir();
|
|
4353
4633
|
let failures = 0;
|
|
4354
4634
|
function pass(msg) {
|
|
4355
4635
|
console.log(import_chalk5.default.green(" \u2705 ") + msg);
|
|
@@ -4395,10 +4675,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4395
4675
|
);
|
|
4396
4676
|
}
|
|
4397
4677
|
section("Configuration");
|
|
4398
|
-
const globalConfigPath =
|
|
4399
|
-
if (
|
|
4678
|
+
const globalConfigPath = import_path8.default.join(homeDir2, ".node9", "config.json");
|
|
4679
|
+
if (import_fs6.default.existsSync(globalConfigPath)) {
|
|
4400
4680
|
try {
|
|
4401
|
-
JSON.parse(
|
|
4681
|
+
JSON.parse(import_fs6.default.readFileSync(globalConfigPath, "utf-8"));
|
|
4402
4682
|
pass("~/.node9/config.json found and valid");
|
|
4403
4683
|
} catch {
|
|
4404
4684
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -4406,17 +4686,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4406
4686
|
} else {
|
|
4407
4687
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
4408
4688
|
}
|
|
4409
|
-
const projectConfigPath =
|
|
4410
|
-
if (
|
|
4689
|
+
const projectConfigPath = import_path8.default.join(process.cwd(), "node9.config.json");
|
|
4690
|
+
if (import_fs6.default.existsSync(projectConfigPath)) {
|
|
4411
4691
|
try {
|
|
4412
|
-
JSON.parse(
|
|
4692
|
+
JSON.parse(import_fs6.default.readFileSync(projectConfigPath, "utf-8"));
|
|
4413
4693
|
pass("node9.config.json found and valid (project)");
|
|
4414
4694
|
} catch {
|
|
4415
4695
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
4416
4696
|
}
|
|
4417
4697
|
}
|
|
4418
|
-
const credsPath =
|
|
4419
|
-
if (
|
|
4698
|
+
const credsPath = import_path8.default.join(homeDir2, ".node9", "credentials.json");
|
|
4699
|
+
if (import_fs6.default.existsSync(credsPath)) {
|
|
4420
4700
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
4421
4701
|
} else {
|
|
4422
4702
|
warn(
|
|
@@ -4425,10 +4705,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4425
4705
|
);
|
|
4426
4706
|
}
|
|
4427
4707
|
section("Agent Hooks");
|
|
4428
|
-
const claudeSettingsPath =
|
|
4429
|
-
if (
|
|
4708
|
+
const claudeSettingsPath = import_path8.default.join(homeDir2, ".claude", "settings.json");
|
|
4709
|
+
if (import_fs6.default.existsSync(claudeSettingsPath)) {
|
|
4430
4710
|
try {
|
|
4431
|
-
const cs = JSON.parse(
|
|
4711
|
+
const cs = JSON.parse(import_fs6.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
4432
4712
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
4433
4713
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4434
4714
|
);
|
|
@@ -4441,10 +4721,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4441
4721
|
} else {
|
|
4442
4722
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
4443
4723
|
}
|
|
4444
|
-
const geminiSettingsPath =
|
|
4445
|
-
if (
|
|
4724
|
+
const geminiSettingsPath = import_path8.default.join(homeDir2, ".gemini", "settings.json");
|
|
4725
|
+
if (import_fs6.default.existsSync(geminiSettingsPath)) {
|
|
4446
4726
|
try {
|
|
4447
|
-
const gs = JSON.parse(
|
|
4727
|
+
const gs = JSON.parse(import_fs6.default.readFileSync(geminiSettingsPath, "utf-8"));
|
|
4448
4728
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
4449
4729
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4450
4730
|
);
|
|
@@ -4457,10 +4737,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4457
4737
|
} else {
|
|
4458
4738
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
4459
4739
|
}
|
|
4460
|
-
const cursorHooksPath =
|
|
4461
|
-
if (
|
|
4740
|
+
const cursorHooksPath = import_path8.default.join(homeDir2, ".cursor", "hooks.json");
|
|
4741
|
+
if (import_fs6.default.existsSync(cursorHooksPath)) {
|
|
4462
4742
|
try {
|
|
4463
|
-
const cur = JSON.parse(
|
|
4743
|
+
const cur = JSON.parse(import_fs6.default.readFileSync(cursorHooksPath, "utf-8"));
|
|
4464
4744
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
4465
4745
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
4466
4746
|
);
|
|
@@ -4562,8 +4842,8 @@ program.command("explain").description(
|
|
|
4562
4842
|
console.log("");
|
|
4563
4843
|
});
|
|
4564
4844
|
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
|
|
4565
|
-
const configPath =
|
|
4566
|
-
if (
|
|
4845
|
+
const configPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
|
|
4846
|
+
if (import_fs6.default.existsSync(configPath) && !options.force) {
|
|
4567
4847
|
console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
4568
4848
|
console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
|
|
4569
4849
|
return;
|
|
@@ -4577,9 +4857,9 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
4577
4857
|
mode: safeMode
|
|
4578
4858
|
}
|
|
4579
4859
|
};
|
|
4580
|
-
const dir =
|
|
4581
|
-
if (!
|
|
4582
|
-
|
|
4860
|
+
const dir = import_path8.default.dirname(configPath);
|
|
4861
|
+
if (!import_fs6.default.existsSync(dir)) import_fs6.default.mkdirSync(dir, { recursive: true });
|
|
4862
|
+
import_fs6.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
4583
4863
|
console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
|
|
4584
4864
|
console.log(import_chalk5.default.cyan(` Mode set to: ${safeMode}`));
|
|
4585
4865
|
console.log(
|
|
@@ -4597,14 +4877,14 @@ function formatRelativeTime(timestamp) {
|
|
|
4597
4877
|
return new Date(timestamp).toLocaleDateString();
|
|
4598
4878
|
}
|
|
4599
4879
|
program.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
|
|
4600
|
-
const logPath =
|
|
4601
|
-
if (!
|
|
4880
|
+
const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "audit.log");
|
|
4881
|
+
if (!import_fs6.default.existsSync(logPath)) {
|
|
4602
4882
|
console.log(
|
|
4603
4883
|
import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
4604
4884
|
);
|
|
4605
4885
|
return;
|
|
4606
4886
|
}
|
|
4607
|
-
const raw =
|
|
4887
|
+
const raw = import_fs6.default.readFileSync(logPath, "utf-8");
|
|
4608
4888
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
4609
4889
|
let entries = lines.flatMap((line) => {
|
|
4610
4890
|
try {
|
|
@@ -4687,13 +4967,13 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4687
4967
|
console.log("");
|
|
4688
4968
|
const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
|
|
4689
4969
|
console.log(` Mode: ${modeLabel}`);
|
|
4690
|
-
const projectConfig =
|
|
4691
|
-
const globalConfig =
|
|
4970
|
+
const projectConfig = import_path8.default.join(process.cwd(), "node9.config.json");
|
|
4971
|
+
const globalConfig = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
|
|
4692
4972
|
console.log(
|
|
4693
|
-
` Local: ${
|
|
4973
|
+
` Local: ${import_fs6.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
|
|
4694
4974
|
);
|
|
4695
4975
|
console.log(
|
|
4696
|
-
` Global: ${
|
|
4976
|
+
` Global: ${import_fs6.default.existsSync(globalConfig) ? import_chalk5.default.green("Active (~/.node9/config.json)") : import_chalk5.default.gray("Not present")}`
|
|
4697
4977
|
);
|
|
4698
4978
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
4699
4979
|
console.log(
|
|
@@ -4756,9 +5036,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
4756
5036
|
} catch (err) {
|
|
4757
5037
|
const tempConfig = getConfig();
|
|
4758
5038
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
4759
|
-
const logPath =
|
|
5039
|
+
const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
|
|
4760
5040
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4761
|
-
|
|
5041
|
+
import_fs6.default.appendFileSync(
|
|
4762
5042
|
logPath,
|
|
4763
5043
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
4764
5044
|
RAW: ${raw}
|
|
@@ -4776,10 +5056,10 @@ RAW: ${raw}
|
|
|
4776
5056
|
}
|
|
4777
5057
|
const config = getConfig();
|
|
4778
5058
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
4779
|
-
const logPath =
|
|
4780
|
-
if (!
|
|
4781
|
-
|
|
4782
|
-
|
|
5059
|
+
const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
|
|
5060
|
+
if (!import_fs6.default.existsSync(import_path8.default.dirname(logPath)))
|
|
5061
|
+
import_fs6.default.mkdirSync(import_path8.default.dirname(logPath), { recursive: true });
|
|
5062
|
+
import_fs6.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
4783
5063
|
`);
|
|
4784
5064
|
}
|
|
4785
5065
|
const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
|
|
@@ -4790,8 +5070,14 @@ RAW: ${raw}
|
|
|
4790
5070
|
const sendBlock = (msg, result2) => {
|
|
4791
5071
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
4792
5072
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
4793
|
-
|
|
5073
|
+
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
5074
|
+
console.error(import_chalk5.default.bgRed.white.bold(`
|
|
5075
|
+
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
5076
|
+
console.error(import_chalk5.default.red.bold(` A sensitive secret was found in the tool arguments!`));
|
|
5077
|
+
} else {
|
|
5078
|
+
console.error(import_chalk5.default.red(`
|
|
4794
5079
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
5080
|
+
}
|
|
4795
5081
|
console.error(import_chalk5.default.gray(` Triggered by: ${blockedByContext}`));
|
|
4796
5082
|
if (result2?.changeHint) console.error(import_chalk5.default.cyan(` To change: ${result2.changeHint}`));
|
|
4797
5083
|
console.error("");
|
|
@@ -4850,9 +5136,9 @@ RAW: ${raw}
|
|
|
4850
5136
|
});
|
|
4851
5137
|
} catch (err) {
|
|
4852
5138
|
if (process.env.NODE9_DEBUG === "1") {
|
|
4853
|
-
const logPath =
|
|
5139
|
+
const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
|
|
4854
5140
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4855
|
-
|
|
5141
|
+
import_fs6.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
4856
5142
|
`);
|
|
4857
5143
|
}
|
|
4858
5144
|
process.exit(0);
|
|
@@ -4897,10 +5183,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
4897
5183
|
decision: "allowed",
|
|
4898
5184
|
source: "post-hook"
|
|
4899
5185
|
};
|
|
4900
|
-
const logPath =
|
|
4901
|
-
if (!
|
|
4902
|
-
|
|
4903
|
-
|
|
5186
|
+
const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "audit.log");
|
|
5187
|
+
if (!import_fs6.default.existsSync(import_path8.default.dirname(logPath)))
|
|
5188
|
+
import_fs6.default.mkdirSync(import_path8.default.dirname(logPath), { recursive: true });
|
|
5189
|
+
import_fs6.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4904
5190
|
const config = getConfig();
|
|
4905
5191
|
if (shouldSnapshot(tool, {}, config)) {
|
|
4906
5192
|
await createShadowSnapshot();
|
|
@@ -5082,13 +5368,103 @@ program.command("undo").description(
|
|
|
5082
5368
|
console.log(import_chalk5.default.gray("\nCancelled.\n"));
|
|
5083
5369
|
}
|
|
5084
5370
|
});
|
|
5371
|
+
var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
|
|
5372
|
+
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
5373
|
+
const name = resolveShieldName(service);
|
|
5374
|
+
if (!name) {
|
|
5375
|
+
console.error(import_chalk5.default.red(`
|
|
5376
|
+
\u274C Unknown shield: "${service}"
|
|
5377
|
+
`));
|
|
5378
|
+
console.log(`Run ${import_chalk5.default.cyan("node9 shield list")} to see available shields.
|
|
5379
|
+
`);
|
|
5380
|
+
process.exit(1);
|
|
5381
|
+
}
|
|
5382
|
+
const shield = getShield(name);
|
|
5383
|
+
const active = readActiveShields();
|
|
5384
|
+
if (active.includes(name)) {
|
|
5385
|
+
console.log(import_chalk5.default.yellow(`
|
|
5386
|
+
\u2139\uFE0F Shield "${name}" is already active.
|
|
5387
|
+
`));
|
|
5388
|
+
return;
|
|
5389
|
+
}
|
|
5390
|
+
writeActiveShields([...active, name]);
|
|
5391
|
+
console.log(import_chalk5.default.green(`
|
|
5392
|
+
\u{1F6E1}\uFE0F Shield "${name}" enabled.`));
|
|
5393
|
+
console.log(import_chalk5.default.gray(` ${shield.smartRules.length} smart rules now active.`));
|
|
5394
|
+
if (shield.dangerousWords.length > 0)
|
|
5395
|
+
console.log(import_chalk5.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
|
|
5396
|
+
if (name === "filesystem") {
|
|
5397
|
+
console.log(
|
|
5398
|
+
import_chalk5.default.yellow(
|
|
5399
|
+
`
|
|
5400
|
+
\u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
|
|
5401
|
+
Tools like unlink, find -delete, or language-level file ops are not intercepted.`
|
|
5402
|
+
)
|
|
5403
|
+
);
|
|
5404
|
+
}
|
|
5405
|
+
console.log("");
|
|
5406
|
+
});
|
|
5407
|
+
shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
|
|
5408
|
+
const name = resolveShieldName(service);
|
|
5409
|
+
if (!name) {
|
|
5410
|
+
console.error(import_chalk5.default.red(`
|
|
5411
|
+
\u274C Unknown shield: "${service}"
|
|
5412
|
+
`));
|
|
5413
|
+
console.log(`Run ${import_chalk5.default.cyan("node9 shield list")} to see available shields.
|
|
5414
|
+
`);
|
|
5415
|
+
process.exit(1);
|
|
5416
|
+
}
|
|
5417
|
+
const active = readActiveShields();
|
|
5418
|
+
if (!active.includes(name)) {
|
|
5419
|
+
console.log(import_chalk5.default.yellow(`
|
|
5420
|
+
\u2139\uFE0F Shield "${name}" is not active.
|
|
5421
|
+
`));
|
|
5422
|
+
return;
|
|
5423
|
+
}
|
|
5424
|
+
writeActiveShields(active.filter((s) => s !== name));
|
|
5425
|
+
console.log(import_chalk5.default.green(`
|
|
5426
|
+
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
5427
|
+
`));
|
|
5428
|
+
});
|
|
5429
|
+
shieldCmd.command("list").description("Show all available shields").action(() => {
|
|
5430
|
+
const active = new Set(readActiveShields());
|
|
5431
|
+
console.log(import_chalk5.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
5432
|
+
for (const shield of listShields()) {
|
|
5433
|
+
const status = active.has(shield.name) ? import_chalk5.default.green("\u25CF enabled") : import_chalk5.default.gray("\u25CB disabled");
|
|
5434
|
+
console.log(` ${status} ${import_chalk5.default.cyan(shield.name.padEnd(12))} ${shield.description}`);
|
|
5435
|
+
if (shield.aliases.length > 0)
|
|
5436
|
+
console.log(import_chalk5.default.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
5437
|
+
}
|
|
5438
|
+
console.log("");
|
|
5439
|
+
});
|
|
5440
|
+
shieldCmd.command("status").description("Show which shields are currently active").action(() => {
|
|
5441
|
+
const active = readActiveShields();
|
|
5442
|
+
if (active.length === 0) {
|
|
5443
|
+
console.log(import_chalk5.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
|
|
5444
|
+
console.log(`Run ${import_chalk5.default.cyan("node9 shield list")} to see available shields.
|
|
5445
|
+
`);
|
|
5446
|
+
return;
|
|
5447
|
+
}
|
|
5448
|
+
console.log(import_chalk5.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
5449
|
+
for (const name of active) {
|
|
5450
|
+
const shield = getShield(name);
|
|
5451
|
+
if (!shield) continue;
|
|
5452
|
+
console.log(` ${import_chalk5.default.green("\u25CF")} ${import_chalk5.default.cyan(name)}`);
|
|
5453
|
+
console.log(
|
|
5454
|
+
import_chalk5.default.gray(
|
|
5455
|
+
` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
|
|
5456
|
+
)
|
|
5457
|
+
);
|
|
5458
|
+
}
|
|
5459
|
+
console.log("");
|
|
5460
|
+
});
|
|
5085
5461
|
process.on("unhandledRejection", (reason) => {
|
|
5086
5462
|
const isCheckHook = process.argv[2] === "check";
|
|
5087
5463
|
if (isCheckHook) {
|
|
5088
5464
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
5089
|
-
const logPath =
|
|
5465
|
+
const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
|
|
5090
5466
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
5091
|
-
|
|
5467
|
+
import_fs6.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
5092
5468
|
`);
|
|
5093
5469
|
}
|
|
5094
5470
|
process.exit(0);
|