@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.mjs
CHANGED
|
@@ -6,9 +6,9 @@ import { Command } from "commander";
|
|
|
6
6
|
// src/core.ts
|
|
7
7
|
import chalk2 from "chalk";
|
|
8
8
|
import { confirm } from "@inquirer/prompts";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
9
|
+
import fs2 from "fs";
|
|
10
|
+
import path4 from "path";
|
|
11
|
+
import os2 from "os";
|
|
12
12
|
import pm from "picomatch";
|
|
13
13
|
import { parse } from "sh-syntax";
|
|
14
14
|
|
|
@@ -338,25 +338,26 @@ import { z } from "zod";
|
|
|
338
338
|
var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
339
339
|
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
340
340
|
});
|
|
341
|
-
var validRegex = noNewlines.refine(
|
|
342
|
-
(s) => {
|
|
343
|
-
try {
|
|
344
|
-
new RegExp(s);
|
|
345
|
-
return true;
|
|
346
|
-
} catch {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
{ message: "Value must be a valid regular expression" }
|
|
351
|
-
);
|
|
352
341
|
var SmartConditionSchema = z.object({
|
|
353
342
|
field: z.string().min(1, "Condition field must not be empty"),
|
|
354
|
-
op: z.enum(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
343
|
+
op: z.enum(
|
|
344
|
+
[
|
|
345
|
+
"matches",
|
|
346
|
+
"notMatches",
|
|
347
|
+
"contains",
|
|
348
|
+
"notContains",
|
|
349
|
+
"exists",
|
|
350
|
+
"notExists",
|
|
351
|
+
"matchesGlob",
|
|
352
|
+
"notMatchesGlob"
|
|
353
|
+
],
|
|
354
|
+
{
|
|
355
|
+
errorMap: () => ({
|
|
356
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
),
|
|
360
|
+
value: z.string().optional(),
|
|
360
361
|
flags: z.string().optional()
|
|
361
362
|
});
|
|
362
363
|
var SmartRuleSchema = z.object({
|
|
@@ -369,11 +370,6 @@ var SmartRuleSchema = z.object({
|
|
|
369
370
|
}),
|
|
370
371
|
reason: z.string().optional()
|
|
371
372
|
});
|
|
372
|
-
var PolicyRuleSchema = z.object({
|
|
373
|
-
action: z.string().min(1),
|
|
374
|
-
allowPaths: z.array(z.string()).optional(),
|
|
375
|
-
blockPaths: z.array(z.string()).optional()
|
|
376
|
-
});
|
|
377
373
|
var ConfigFileSchema = z.object({
|
|
378
374
|
version: z.string().optional(),
|
|
379
375
|
settings: z.object({
|
|
@@ -398,12 +394,15 @@ var ConfigFileSchema = z.object({
|
|
|
398
394
|
dangerousWords: z.array(noNewlines).optional(),
|
|
399
395
|
ignoredTools: z.array(z.string()).optional(),
|
|
400
396
|
toolInspection: z.record(z.string()).optional(),
|
|
401
|
-
rules: z.array(PolicyRuleSchema).optional(),
|
|
402
397
|
smartRules: z.array(SmartRuleSchema).optional(),
|
|
403
398
|
snapshot: z.object({
|
|
404
399
|
tools: z.array(z.string()).optional(),
|
|
405
400
|
onlyPaths: z.array(z.string()).optional(),
|
|
406
401
|
ignorePaths: z.array(z.string()).optional()
|
|
402
|
+
}).optional(),
|
|
403
|
+
dlp: z.object({
|
|
404
|
+
enabled: z.boolean().optional(),
|
|
405
|
+
scanIgnoredTools: z.boolean().optional()
|
|
407
406
|
}).optional()
|
|
408
407
|
}).optional(),
|
|
409
408
|
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
@@ -425,8 +424,8 @@ function sanitizeConfig(raw) {
|
|
|
425
424
|
}
|
|
426
425
|
}
|
|
427
426
|
const lines = result.error.issues.map((issue) => {
|
|
428
|
-
const
|
|
429
|
-
return ` \u2022 ${
|
|
427
|
+
const path9 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
428
|
+
return ` \u2022 ${path9}: ${issue.message}`;
|
|
430
429
|
});
|
|
431
430
|
return {
|
|
432
431
|
sanitized,
|
|
@@ -435,18 +434,301 @@ ${lines.join("\n")}`
|
|
|
435
434
|
};
|
|
436
435
|
}
|
|
437
436
|
|
|
437
|
+
// src/shields.ts
|
|
438
|
+
import fs from "fs";
|
|
439
|
+
import path3 from "path";
|
|
440
|
+
import os from "os";
|
|
441
|
+
import crypto from "crypto";
|
|
442
|
+
var SHIELDS = {
|
|
443
|
+
postgres: {
|
|
444
|
+
name: "postgres",
|
|
445
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
446
|
+
aliases: ["pg", "postgresql"],
|
|
447
|
+
smartRules: [
|
|
448
|
+
{
|
|
449
|
+
name: "shield:postgres:block-drop-table",
|
|
450
|
+
tool: "*",
|
|
451
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
452
|
+
verdict: "block",
|
|
453
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: "shield:postgres:block-truncate",
|
|
457
|
+
tool: "*",
|
|
458
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
459
|
+
verdict: "block",
|
|
460
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: "shield:postgres:block-drop-column",
|
|
464
|
+
tool: "*",
|
|
465
|
+
conditions: [
|
|
466
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
467
|
+
],
|
|
468
|
+
verdict: "block",
|
|
469
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: "shield:postgres:review-grant-revoke",
|
|
473
|
+
tool: "*",
|
|
474
|
+
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
475
|
+
verdict: "review",
|
|
476
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
477
|
+
}
|
|
478
|
+
],
|
|
479
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
480
|
+
},
|
|
481
|
+
github: {
|
|
482
|
+
name: "github",
|
|
483
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
484
|
+
aliases: ["git"],
|
|
485
|
+
smartRules: [
|
|
486
|
+
{
|
|
487
|
+
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
488
|
+
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
489
|
+
name: "shield:github:review-delete-branch-remote",
|
|
490
|
+
tool: "bash",
|
|
491
|
+
conditions: [
|
|
492
|
+
{
|
|
493
|
+
field: "command",
|
|
494
|
+
op: "matches",
|
|
495
|
+
value: "git\\s+push\\s+.*--delete",
|
|
496
|
+
flags: "i"
|
|
497
|
+
}
|
|
498
|
+
],
|
|
499
|
+
verdict: "review",
|
|
500
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
name: "shield:github:block-delete-repo",
|
|
504
|
+
tool: "*",
|
|
505
|
+
conditions: [
|
|
506
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
507
|
+
],
|
|
508
|
+
verdict: "block",
|
|
509
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
510
|
+
}
|
|
511
|
+
],
|
|
512
|
+
dangerousWords: []
|
|
513
|
+
},
|
|
514
|
+
aws: {
|
|
515
|
+
name: "aws",
|
|
516
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
517
|
+
aliases: ["amazon"],
|
|
518
|
+
smartRules: [
|
|
519
|
+
{
|
|
520
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
521
|
+
tool: "*",
|
|
522
|
+
conditions: [
|
|
523
|
+
{
|
|
524
|
+
field: "command",
|
|
525
|
+
op: "matches",
|
|
526
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
527
|
+
flags: "i"
|
|
528
|
+
}
|
|
529
|
+
],
|
|
530
|
+
verdict: "block",
|
|
531
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: "shield:aws:review-iam-changes",
|
|
535
|
+
tool: "*",
|
|
536
|
+
conditions: [
|
|
537
|
+
{
|
|
538
|
+
field: "command",
|
|
539
|
+
op: "matches",
|
|
540
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
541
|
+
flags: "i"
|
|
542
|
+
}
|
|
543
|
+
],
|
|
544
|
+
verdict: "review",
|
|
545
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
name: "shield:aws:block-ec2-terminate",
|
|
549
|
+
tool: "*",
|
|
550
|
+
conditions: [
|
|
551
|
+
{
|
|
552
|
+
field: "command",
|
|
553
|
+
op: "matches",
|
|
554
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
555
|
+
flags: "i"
|
|
556
|
+
}
|
|
557
|
+
],
|
|
558
|
+
verdict: "block",
|
|
559
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
name: "shield:aws:review-rds-delete",
|
|
563
|
+
tool: "*",
|
|
564
|
+
conditions: [
|
|
565
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
566
|
+
],
|
|
567
|
+
verdict: "review",
|
|
568
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
569
|
+
}
|
|
570
|
+
],
|
|
571
|
+
dangerousWords: []
|
|
572
|
+
},
|
|
573
|
+
filesystem: {
|
|
574
|
+
name: "filesystem",
|
|
575
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
576
|
+
aliases: ["fs"],
|
|
577
|
+
smartRules: [
|
|
578
|
+
{
|
|
579
|
+
name: "shield:filesystem:review-chmod-777",
|
|
580
|
+
tool: "bash",
|
|
581
|
+
conditions: [
|
|
582
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
583
|
+
],
|
|
584
|
+
verdict: "review",
|
|
585
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: "shield:filesystem:review-write-etc",
|
|
589
|
+
tool: "bash",
|
|
590
|
+
conditions: [
|
|
591
|
+
{
|
|
592
|
+
field: "command",
|
|
593
|
+
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
594
|
+
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
595
|
+
op: "matches",
|
|
596
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
597
|
+
}
|
|
598
|
+
],
|
|
599
|
+
verdict: "review",
|
|
600
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
601
|
+
}
|
|
602
|
+
],
|
|
603
|
+
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
604
|
+
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
605
|
+
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
606
|
+
dangerousWords: ["wipefs"]
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
function resolveShieldName(input) {
|
|
610
|
+
const lower = input.toLowerCase();
|
|
611
|
+
if (SHIELDS[lower]) return lower;
|
|
612
|
+
for (const [name, def] of Object.entries(SHIELDS)) {
|
|
613
|
+
if (def.aliases.includes(lower)) return name;
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
function getShield(name) {
|
|
618
|
+
const resolved = resolveShieldName(name);
|
|
619
|
+
return resolved ? SHIELDS[resolved] : null;
|
|
620
|
+
}
|
|
621
|
+
function listShields() {
|
|
622
|
+
return Object.values(SHIELDS);
|
|
623
|
+
}
|
|
624
|
+
var SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
|
|
625
|
+
function readActiveShields() {
|
|
626
|
+
try {
|
|
627
|
+
const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
628
|
+
if (!raw.trim()) return [];
|
|
629
|
+
const parsed = JSON.parse(raw);
|
|
630
|
+
if (Array.isArray(parsed.active)) {
|
|
631
|
+
return parsed.active.filter(
|
|
632
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
} catch (err) {
|
|
636
|
+
if (err.code !== "ENOENT") {
|
|
637
|
+
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
638
|
+
`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return [];
|
|
642
|
+
}
|
|
643
|
+
function writeActiveShields(active) {
|
|
644
|
+
fs.mkdirSync(path3.dirname(SHIELDS_STATE_FILE), { recursive: true });
|
|
645
|
+
const tmp = `${SHIELDS_STATE_FILE}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
646
|
+
fs.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
|
|
647
|
+
fs.renameSync(tmp, SHIELDS_STATE_FILE);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/dlp.ts
|
|
651
|
+
var DLP_PATTERNS = [
|
|
652
|
+
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
653
|
+
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
654
|
+
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
|
|
655
|
+
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
656
|
+
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
657
|
+
{
|
|
658
|
+
name: "Private Key (PEM)",
|
|
659
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
660
|
+
severity: "block"
|
|
661
|
+
},
|
|
662
|
+
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
663
|
+
];
|
|
664
|
+
function maskSecret(raw, pattern) {
|
|
665
|
+
const match = raw.match(pattern);
|
|
666
|
+
if (!match) return "****";
|
|
667
|
+
const secret = match[0];
|
|
668
|
+
if (secret.length < 8) return "****";
|
|
669
|
+
const prefix = secret.slice(0, 4);
|
|
670
|
+
const suffix = secret.slice(-4);
|
|
671
|
+
const stars = "*".repeat(Math.min(secret.length - 8, 12));
|
|
672
|
+
return `${prefix}${stars}${suffix}`;
|
|
673
|
+
}
|
|
674
|
+
var MAX_DEPTH = 5;
|
|
675
|
+
var MAX_STRING_BYTES = 1e5;
|
|
676
|
+
var MAX_JSON_PARSE_BYTES = 1e4;
|
|
677
|
+
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
678
|
+
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
679
|
+
if (Array.isArray(args)) {
|
|
680
|
+
for (let i = 0; i < args.length; i++) {
|
|
681
|
+
const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
|
|
682
|
+
if (match) return match;
|
|
683
|
+
}
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
if (typeof args === "object") {
|
|
687
|
+
for (const [key, value] of Object.entries(args)) {
|
|
688
|
+
const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
|
|
689
|
+
if (match) return match;
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
if (typeof args === "string") {
|
|
694
|
+
const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
|
|
695
|
+
for (const pattern of DLP_PATTERNS) {
|
|
696
|
+
if (pattern.regex.test(text)) {
|
|
697
|
+
return {
|
|
698
|
+
patternName: pattern.name,
|
|
699
|
+
fieldPath,
|
|
700
|
+
redactedSample: maskSecret(text, pattern.regex),
|
|
701
|
+
severity: pattern.severity
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (text.length < MAX_JSON_PARSE_BYTES) {
|
|
706
|
+
const trimmed = text.trim();
|
|
707
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
708
|
+
try {
|
|
709
|
+
const parsed = JSON.parse(text);
|
|
710
|
+
const inner = scanArgs(parsed, depth + 1, fieldPath);
|
|
711
|
+
if (inner) return inner;
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
|
|
438
720
|
// src/core.ts
|
|
439
|
-
var PAUSED_FILE =
|
|
440
|
-
var TRUST_FILE =
|
|
441
|
-
var LOCAL_AUDIT_LOG =
|
|
442
|
-
var HOOK_DEBUG_LOG =
|
|
721
|
+
var PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
|
|
722
|
+
var TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
|
|
723
|
+
var LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
|
|
724
|
+
var HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
|
|
443
725
|
function checkPause() {
|
|
444
726
|
try {
|
|
445
|
-
if (!
|
|
446
|
-
const state = JSON.parse(
|
|
727
|
+
if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
|
|
728
|
+
const state = JSON.parse(fs2.readFileSync(PAUSED_FILE, "utf-8"));
|
|
447
729
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
448
730
|
try {
|
|
449
|
-
|
|
731
|
+
fs2.unlinkSync(PAUSED_FILE);
|
|
450
732
|
} catch {
|
|
451
733
|
}
|
|
452
734
|
return { paused: false };
|
|
@@ -457,11 +739,11 @@ function checkPause() {
|
|
|
457
739
|
}
|
|
458
740
|
}
|
|
459
741
|
function atomicWriteSync(filePath, data, options) {
|
|
460
|
-
const dir =
|
|
461
|
-
if (!
|
|
462
|
-
const tmpPath = `${filePath}.${
|
|
463
|
-
|
|
464
|
-
|
|
742
|
+
const dir = path4.dirname(filePath);
|
|
743
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
744
|
+
const tmpPath = `${filePath}.${os2.hostname()}.${process.pid}.tmp`;
|
|
745
|
+
fs2.writeFileSync(tmpPath, data, options);
|
|
746
|
+
fs2.renameSync(tmpPath, filePath);
|
|
465
747
|
}
|
|
466
748
|
function pauseNode9(durationMs, durationStr) {
|
|
467
749
|
const state = { expiry: Date.now() + durationMs, duration: durationStr };
|
|
@@ -469,18 +751,18 @@ function pauseNode9(durationMs, durationStr) {
|
|
|
469
751
|
}
|
|
470
752
|
function resumeNode9() {
|
|
471
753
|
try {
|
|
472
|
-
if (
|
|
754
|
+
if (fs2.existsSync(PAUSED_FILE)) fs2.unlinkSync(PAUSED_FILE);
|
|
473
755
|
} catch {
|
|
474
756
|
}
|
|
475
757
|
}
|
|
476
758
|
function getActiveTrustSession(toolName) {
|
|
477
759
|
try {
|
|
478
|
-
if (!
|
|
479
|
-
const trust = JSON.parse(
|
|
760
|
+
if (!fs2.existsSync(TRUST_FILE)) return false;
|
|
761
|
+
const trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
|
|
480
762
|
const now = Date.now();
|
|
481
763
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
482
764
|
if (active.length !== trust.entries.length) {
|
|
483
|
-
|
|
765
|
+
fs2.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
484
766
|
}
|
|
485
767
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
486
768
|
} catch {
|
|
@@ -491,8 +773,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
491
773
|
try {
|
|
492
774
|
let trust = { entries: [] };
|
|
493
775
|
try {
|
|
494
|
-
if (
|
|
495
|
-
trust = JSON.parse(
|
|
776
|
+
if (fs2.existsSync(TRUST_FILE)) {
|
|
777
|
+
trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
|
|
496
778
|
}
|
|
497
779
|
} catch {
|
|
498
780
|
}
|
|
@@ -508,9 +790,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
508
790
|
}
|
|
509
791
|
function appendToLog(logPath, entry) {
|
|
510
792
|
try {
|
|
511
|
-
const dir =
|
|
512
|
-
if (!
|
|
513
|
-
|
|
793
|
+
const dir = path4.dirname(logPath);
|
|
794
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
795
|
+
fs2.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
514
796
|
} catch {
|
|
515
797
|
}
|
|
516
798
|
}
|
|
@@ -522,7 +804,7 @@ function appendHookDebug(toolName, args, meta) {
|
|
|
522
804
|
args: safeArgs,
|
|
523
805
|
agent: meta?.agent,
|
|
524
806
|
mcpServer: meta?.mcpServer,
|
|
525
|
-
hostname:
|
|
807
|
+
hostname: os2.hostname(),
|
|
526
808
|
cwd: process.cwd()
|
|
527
809
|
});
|
|
528
810
|
}
|
|
@@ -536,7 +818,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
|
536
818
|
checkedBy,
|
|
537
819
|
agent: meta?.agent,
|
|
538
820
|
mcpServer: meta?.mcpServer,
|
|
539
|
-
hostname:
|
|
821
|
+
hostname: os2.hostname()
|
|
540
822
|
});
|
|
541
823
|
}
|
|
542
824
|
function tokenize(toolName) {
|
|
@@ -552,9 +834,9 @@ function matchesPattern(text, patterns) {
|
|
|
552
834
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
553
835
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
554
836
|
}
|
|
555
|
-
function getNestedValue(obj,
|
|
837
|
+
function getNestedValue(obj, path9) {
|
|
556
838
|
if (!obj || typeof obj !== "object") return null;
|
|
557
|
-
return
|
|
839
|
+
return path9.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
558
840
|
}
|
|
559
841
|
function shouldSnapshot(toolName, args, config) {
|
|
560
842
|
if (!config.settings.enableUndo) return false;
|
|
@@ -599,6 +881,10 @@ function evaluateSmartConditions(args, rule) {
|
|
|
599
881
|
return true;
|
|
600
882
|
}
|
|
601
883
|
}
|
|
884
|
+
case "matchesGlob":
|
|
885
|
+
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
886
|
+
case "notMatchesGlob":
|
|
887
|
+
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : true;
|
|
602
888
|
default:
|
|
603
889
|
return false;
|
|
604
890
|
}
|
|
@@ -762,25 +1048,27 @@ var DEFAULT_CONFIG = {
|
|
|
762
1048
|
onlyPaths: [],
|
|
763
1049
|
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
764
1050
|
},
|
|
765
|
-
rules: [
|
|
766
|
-
// Only use the legacy rules format for simple path-based rm control.
|
|
767
|
-
// All other command-level enforcement lives in smartRules below.
|
|
768
|
-
{
|
|
769
|
-
action: "rm",
|
|
770
|
-
allowPaths: [
|
|
771
|
-
"**/node_modules/**",
|
|
772
|
-
"dist/**",
|
|
773
|
-
"build/**",
|
|
774
|
-
".next/**",
|
|
775
|
-
"coverage/**",
|
|
776
|
-
".cache/**",
|
|
777
|
-
"tmp/**",
|
|
778
|
-
"temp/**",
|
|
779
|
-
".DS_Store"
|
|
780
|
-
]
|
|
781
|
-
}
|
|
782
|
-
],
|
|
783
1051
|
smartRules: [
|
|
1052
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
1053
|
+
{
|
|
1054
|
+
name: "block-rm-rf-home",
|
|
1055
|
+
tool: "bash",
|
|
1056
|
+
conditionMode: "all",
|
|
1057
|
+
conditions: [
|
|
1058
|
+
{
|
|
1059
|
+
field: "command",
|
|
1060
|
+
op: "matches",
|
|
1061
|
+
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
field: "command",
|
|
1065
|
+
op: "matches",
|
|
1066
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
1067
|
+
}
|
|
1068
|
+
],
|
|
1069
|
+
verdict: "block",
|
|
1070
|
+
reason: "Recursive delete of home directory is irreversible"
|
|
1071
|
+
},
|
|
784
1072
|
// ── SQL safety ────────────────────────────────────────────────────────
|
|
785
1073
|
{
|
|
786
1074
|
name: "no-delete-without-where",
|
|
@@ -871,19 +1159,45 @@ var DEFAULT_CONFIG = {
|
|
|
871
1159
|
verdict: "block",
|
|
872
1160
|
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
873
1161
|
}
|
|
874
|
-
]
|
|
1162
|
+
],
|
|
1163
|
+
dlp: { enabled: true, scanIgnoredTools: true }
|
|
875
1164
|
},
|
|
876
1165
|
environments: {}
|
|
877
1166
|
};
|
|
1167
|
+
var ADVISORY_SMART_RULES = [
|
|
1168
|
+
{
|
|
1169
|
+
name: "allow-rm-safe-paths",
|
|
1170
|
+
tool: "*",
|
|
1171
|
+
conditionMode: "all",
|
|
1172
|
+
conditions: [
|
|
1173
|
+
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
1174
|
+
{
|
|
1175
|
+
field: "command",
|
|
1176
|
+
op: "matches",
|
|
1177
|
+
// Matches known-safe build artifact paths in the command.
|
|
1178
|
+
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
1179
|
+
}
|
|
1180
|
+
],
|
|
1181
|
+
verdict: "allow",
|
|
1182
|
+
reason: "Deleting a known-safe build artifact path"
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
name: "review-rm",
|
|
1186
|
+
tool: "*",
|
|
1187
|
+
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
1188
|
+
verdict: "review",
|
|
1189
|
+
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
1190
|
+
}
|
|
1191
|
+
];
|
|
878
1192
|
var cachedConfig = null;
|
|
879
1193
|
function _resetConfigCache() {
|
|
880
1194
|
cachedConfig = null;
|
|
881
1195
|
}
|
|
882
1196
|
function getGlobalSettings() {
|
|
883
1197
|
try {
|
|
884
|
-
const globalConfigPath =
|
|
885
|
-
if (
|
|
886
|
-
const parsed = JSON.parse(
|
|
1198
|
+
const globalConfigPath = path4.join(os2.homedir(), ".node9", "config.json");
|
|
1199
|
+
if (fs2.existsSync(globalConfigPath)) {
|
|
1200
|
+
const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
|
|
887
1201
|
const settings = parsed.settings || {};
|
|
888
1202
|
return {
|
|
889
1203
|
mode: settings.mode || "standard",
|
|
@@ -905,9 +1219,9 @@ function getGlobalSettings() {
|
|
|
905
1219
|
}
|
|
906
1220
|
function getInternalToken() {
|
|
907
1221
|
try {
|
|
908
|
-
const pidFile =
|
|
909
|
-
if (!
|
|
910
|
-
const data = JSON.parse(
|
|
1222
|
+
const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
|
|
1223
|
+
if (!fs2.existsSync(pidFile)) return null;
|
|
1224
|
+
const data = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
|
|
911
1225
|
process.kill(data.pid, 0);
|
|
912
1226
|
return data.internalToken ?? null;
|
|
913
1227
|
} catch {
|
|
@@ -922,7 +1236,8 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
922
1236
|
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
923
1237
|
);
|
|
924
1238
|
if (matchedRule) {
|
|
925
|
-
if (matchedRule.verdict === "allow")
|
|
1239
|
+
if (matchedRule.verdict === "allow")
|
|
1240
|
+
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
926
1241
|
return {
|
|
927
1242
|
decision: matchedRule.verdict,
|
|
928
1243
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -933,13 +1248,11 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
933
1248
|
}
|
|
934
1249
|
}
|
|
935
1250
|
let allTokens = [];
|
|
936
|
-
let actionTokens = [];
|
|
937
1251
|
let pathTokens = [];
|
|
938
1252
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
939
1253
|
if (shellCommand) {
|
|
940
1254
|
const analyzed = await analyzeShellCommand(shellCommand);
|
|
941
1255
|
allTokens = analyzed.allTokens;
|
|
942
|
-
actionTokens = analyzed.actions;
|
|
943
1256
|
pathTokens = analyzed.paths;
|
|
944
1257
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
945
1258
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
@@ -947,11 +1260,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
947
1260
|
}
|
|
948
1261
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
949
1262
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
950
|
-
actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
951
1263
|
}
|
|
952
1264
|
} else {
|
|
953
1265
|
allTokens = tokenize(toolName);
|
|
954
|
-
actionTokens = [toolName];
|
|
955
1266
|
if (args && typeof args === "object") {
|
|
956
1267
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
957
1268
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
@@ -974,29 +1285,6 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
974
1285
|
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
975
1286
|
if (allInSandbox) return { decision: "allow" };
|
|
976
1287
|
}
|
|
977
|
-
for (const action of actionTokens) {
|
|
978
|
-
const rule = config.policy.rules.find(
|
|
979
|
-
(r) => r.action === action || matchesPattern(action, r.action)
|
|
980
|
-
);
|
|
981
|
-
if (rule) {
|
|
982
|
-
if (pathTokens.length > 0) {
|
|
983
|
-
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
984
|
-
if (anyBlocked)
|
|
985
|
-
return {
|
|
986
|
-
decision: "review",
|
|
987
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
|
|
988
|
-
tier: 5
|
|
989
|
-
};
|
|
990
|
-
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
991
|
-
if (allAllowed) return { decision: "allow" };
|
|
992
|
-
}
|
|
993
|
-
return {
|
|
994
|
-
decision: "review",
|
|
995
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
|
|
996
|
-
tier: 5
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
1288
|
let matchedDangerousWord;
|
|
1001
1289
|
const isDangerous = allTokens.some(
|
|
1002
1290
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
@@ -1048,9 +1336,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
1048
1336
|
}
|
|
1049
1337
|
async function explainPolicy(toolName, args) {
|
|
1050
1338
|
const steps = [];
|
|
1051
|
-
const globalPath =
|
|
1052
|
-
const projectPath =
|
|
1053
|
-
const credsPath =
|
|
1339
|
+
const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
|
|
1340
|
+
const projectPath = path4.join(process.cwd(), "node9.config.json");
|
|
1341
|
+
const credsPath = path4.join(os2.homedir(), ".node9", "credentials.json");
|
|
1054
1342
|
const waterfall = [
|
|
1055
1343
|
{
|
|
1056
1344
|
tier: 1,
|
|
@@ -1061,19 +1349,19 @@ async function explainPolicy(toolName, args) {
|
|
|
1061
1349
|
{
|
|
1062
1350
|
tier: 2,
|
|
1063
1351
|
label: "Cloud policy",
|
|
1064
|
-
status:
|
|
1065
|
-
note:
|
|
1352
|
+
status: fs2.existsSync(credsPath) ? "active" : "missing",
|
|
1353
|
+
note: fs2.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
|
|
1066
1354
|
},
|
|
1067
1355
|
{
|
|
1068
1356
|
tier: 3,
|
|
1069
1357
|
label: "Project config",
|
|
1070
|
-
status:
|
|
1358
|
+
status: fs2.existsSync(projectPath) ? "active" : "missing",
|
|
1071
1359
|
path: projectPath
|
|
1072
1360
|
},
|
|
1073
1361
|
{
|
|
1074
1362
|
tier: 4,
|
|
1075
1363
|
label: "Global config",
|
|
1076
|
-
status:
|
|
1364
|
+
status: fs2.existsSync(globalPath) ? "active" : "missing",
|
|
1077
1365
|
path: globalPath
|
|
1078
1366
|
},
|
|
1079
1367
|
{
|
|
@@ -1084,7 +1372,28 @@ async function explainPolicy(toolName, args) {
|
|
|
1084
1372
|
}
|
|
1085
1373
|
];
|
|
1086
1374
|
const config = getConfig();
|
|
1087
|
-
|
|
1375
|
+
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
1376
|
+
if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
|
|
1377
|
+
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
1378
|
+
if (dlpMatch) {
|
|
1379
|
+
steps.push({
|
|
1380
|
+
name: "DLP Content Scanner",
|
|
1381
|
+
outcome: dlpMatch.severity === "block" ? "block" : "review",
|
|
1382
|
+
detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
|
|
1383
|
+
isFinal: dlpMatch.severity === "block"
|
|
1384
|
+
});
|
|
1385
|
+
if (dlpMatch.severity === "block") {
|
|
1386
|
+
return { tool: toolName, args, waterfall, steps, decision: "block" };
|
|
1387
|
+
}
|
|
1388
|
+
} else {
|
|
1389
|
+
steps.push({
|
|
1390
|
+
name: "DLP Content Scanner",
|
|
1391
|
+
outcome: "checked",
|
|
1392
|
+
detail: "No sensitive credentials detected in args"
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (wouldBeIgnored) {
|
|
1088
1397
|
steps.push({
|
|
1089
1398
|
name: "Ignored tools",
|
|
1090
1399
|
outcome: "allow",
|
|
@@ -1137,13 +1446,11 @@ async function explainPolicy(toolName, args) {
|
|
|
1137
1446
|
steps.push({ name: "Smart rules", outcome: "skip", detail: "No smart rules configured" });
|
|
1138
1447
|
}
|
|
1139
1448
|
let allTokens = [];
|
|
1140
|
-
let actionTokens = [];
|
|
1141
1449
|
let pathTokens = [];
|
|
1142
1450
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
1143
1451
|
if (shellCommand) {
|
|
1144
1452
|
const analyzed = await analyzeShellCommand(shellCommand);
|
|
1145
1453
|
allTokens = analyzed.allTokens;
|
|
1146
|
-
actionTokens = analyzed.actions;
|
|
1147
1454
|
pathTokens = analyzed.paths;
|
|
1148
1455
|
const patterns = Object.keys(config.policy.toolInspection);
|
|
1149
1456
|
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
@@ -1177,7 +1484,6 @@ async function explainPolicy(toolName, args) {
|
|
|
1177
1484
|
});
|
|
1178
1485
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
1179
1486
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1180
|
-
actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1181
1487
|
steps.push({
|
|
1182
1488
|
name: "SQL token stripping",
|
|
1183
1489
|
outcome: "checked",
|
|
@@ -1186,7 +1492,6 @@ async function explainPolicy(toolName, args) {
|
|
|
1186
1492
|
}
|
|
1187
1493
|
} else {
|
|
1188
1494
|
allTokens = tokenize(toolName);
|
|
1189
|
-
actionTokens = [toolName];
|
|
1190
1495
|
let detail = `No toolInspection match for "${toolName}" \u2014 tokens: [${allTokens.join(", ")}]`;
|
|
1191
1496
|
if (args && typeof args === "object") {
|
|
1192
1497
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
@@ -1227,65 +1532,6 @@ async function explainPolicy(toolName, args) {
|
|
|
1227
1532
|
detail: pathTokens.length === 0 ? "No path tokens found in input" : "No sandbox paths configured"
|
|
1228
1533
|
});
|
|
1229
1534
|
}
|
|
1230
|
-
let ruleMatched = false;
|
|
1231
|
-
for (const action of actionTokens) {
|
|
1232
|
-
const rule = config.policy.rules.find(
|
|
1233
|
-
(r) => r.action === action || matchesPattern(action, r.action)
|
|
1234
|
-
);
|
|
1235
|
-
if (rule) {
|
|
1236
|
-
ruleMatched = true;
|
|
1237
|
-
if (pathTokens.length > 0) {
|
|
1238
|
-
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
1239
|
-
if (anyBlocked) {
|
|
1240
|
-
steps.push({
|
|
1241
|
-
name: "Policy rules",
|
|
1242
|
-
outcome: "review",
|
|
1243
|
-
detail: `Rule "${rule.action}" matched + path is in blockPaths`,
|
|
1244
|
-
isFinal: true
|
|
1245
|
-
});
|
|
1246
|
-
return {
|
|
1247
|
-
tool: toolName,
|
|
1248
|
-
args,
|
|
1249
|
-
waterfall,
|
|
1250
|
-
steps,
|
|
1251
|
-
decision: "review",
|
|
1252
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
|
|
1253
|
-
};
|
|
1254
|
-
}
|
|
1255
|
-
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
1256
|
-
if (allAllowed) {
|
|
1257
|
-
steps.push({
|
|
1258
|
-
name: "Policy rules",
|
|
1259
|
-
outcome: "allow",
|
|
1260
|
-
detail: `Rule "${rule.action}" matched + all paths are in allowPaths`,
|
|
1261
|
-
isFinal: true
|
|
1262
|
-
});
|
|
1263
|
-
return { tool: toolName, args, waterfall, steps, decision: "allow" };
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
steps.push({
|
|
1267
|
-
name: "Policy rules",
|
|
1268
|
-
outcome: "review",
|
|
1269
|
-
detail: `Rule "${rule.action}" matched \u2014 default block (no path exception)`,
|
|
1270
|
-
isFinal: true
|
|
1271
|
-
});
|
|
1272
|
-
return {
|
|
1273
|
-
tool: toolName,
|
|
1274
|
-
args,
|
|
1275
|
-
waterfall,
|
|
1276
|
-
steps,
|
|
1277
|
-
decision: "review",
|
|
1278
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
if (!ruleMatched) {
|
|
1283
|
-
steps.push({
|
|
1284
|
-
name: "Policy rules",
|
|
1285
|
-
outcome: "skip",
|
|
1286
|
-
detail: config.policy.rules.length === 0 ? "No rules configured" : `No rule matched [${actionTokens.join(", ")}]`
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
1535
|
let matchedDangerousWord;
|
|
1290
1536
|
const isDangerous = uniqueTokens.some(
|
|
1291
1537
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
@@ -1354,9 +1600,9 @@ var DAEMON_PORT = 7391;
|
|
|
1354
1600
|
var DAEMON_HOST = "127.0.0.1";
|
|
1355
1601
|
function isDaemonRunning() {
|
|
1356
1602
|
try {
|
|
1357
|
-
const pidFile =
|
|
1358
|
-
if (!
|
|
1359
|
-
const { pid, port } = JSON.parse(
|
|
1603
|
+
const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
|
|
1604
|
+
if (!fs2.existsSync(pidFile)) return false;
|
|
1605
|
+
const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
|
|
1360
1606
|
if (port !== DAEMON_PORT) return false;
|
|
1361
1607
|
process.kill(pid, 0);
|
|
1362
1608
|
return true;
|
|
@@ -1366,9 +1612,9 @@ function isDaemonRunning() {
|
|
|
1366
1612
|
}
|
|
1367
1613
|
function getPersistentDecision(toolName) {
|
|
1368
1614
|
try {
|
|
1369
|
-
const file =
|
|
1370
|
-
if (!
|
|
1371
|
-
const decisions = JSON.parse(
|
|
1615
|
+
const file = path4.join(os2.homedir(), ".node9", "decisions.json");
|
|
1616
|
+
if (!fs2.existsSync(file)) return null;
|
|
1617
|
+
const decisions = JSON.parse(fs2.readFileSync(file, "utf-8"));
|
|
1372
1618
|
const d = decisions[toolName];
|
|
1373
1619
|
if (d === "allow" || d === "deny") return d;
|
|
1374
1620
|
} catch {
|
|
@@ -1467,6 +1713,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1467
1713
|
let policyMatchedField;
|
|
1468
1714
|
let policyMatchedWord;
|
|
1469
1715
|
let riskMetadata;
|
|
1716
|
+
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
1717
|
+
const dlpMatch = scanArgs(args);
|
|
1718
|
+
if (dlpMatch) {
|
|
1719
|
+
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
1720
|
+
if (dlpMatch.severity === "block") {
|
|
1721
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
|
|
1722
|
+
return {
|
|
1723
|
+
approved: false,
|
|
1724
|
+
reason: dlpReason,
|
|
1725
|
+
blockedBy: "local-config",
|
|
1726
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1470
1732
|
if (config.settings.mode === "audit") {
|
|
1471
1733
|
if (!isIgnoredTool(toolName)) {
|
|
1472
1734
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
@@ -1706,7 +1968,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1706
1968
|
racePromises.push(
|
|
1707
1969
|
(async () => {
|
|
1708
1970
|
try {
|
|
1709
|
-
|
|
1971
|
+
if (explainableLabel.includes("DLP")) {
|
|
1972
|
+
console.log(chalk2.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
1973
|
+
console.log(
|
|
1974
|
+
chalk2.red.bold(` A sensitive secret was detected in the tool arguments!`)
|
|
1975
|
+
);
|
|
1976
|
+
} else {
|
|
1977
|
+
console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
1978
|
+
}
|
|
1710
1979
|
console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
|
|
1711
1980
|
console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
|
|
1712
1981
|
if (isRemoteLocked) {
|
|
@@ -1811,8 +2080,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1811
2080
|
}
|
|
1812
2081
|
function getConfig() {
|
|
1813
2082
|
if (cachedConfig) return cachedConfig;
|
|
1814
|
-
const globalPath =
|
|
1815
|
-
const projectPath =
|
|
2083
|
+
const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
|
|
2084
|
+
const projectPath = path4.join(process.cwd(), "node9.config.json");
|
|
1816
2085
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1817
2086
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1818
2087
|
const mergedSettings = {
|
|
@@ -1824,13 +2093,13 @@ function getConfig() {
|
|
|
1824
2093
|
dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
|
|
1825
2094
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
1826
2095
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
1827
|
-
rules: [...DEFAULT_CONFIG.policy.rules],
|
|
1828
2096
|
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
1829
2097
|
snapshot: {
|
|
1830
2098
|
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
1831
2099
|
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
1832
2100
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1833
|
-
}
|
|
2101
|
+
},
|
|
2102
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp }
|
|
1834
2103
|
};
|
|
1835
2104
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
1836
2105
|
const applyLayer = (source) => {
|
|
@@ -1850,7 +2119,6 @@ function getConfig() {
|
|
|
1850
2119
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
1851
2120
|
if (p.toolInspection)
|
|
1852
2121
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
1853
|
-
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
1854
2122
|
if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
|
|
1855
2123
|
if (p.snapshot) {
|
|
1856
2124
|
const s2 = p.snapshot;
|
|
@@ -1858,6 +2126,11 @@ function getConfig() {
|
|
|
1858
2126
|
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1859
2127
|
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1860
2128
|
}
|
|
2129
|
+
if (p.dlp) {
|
|
2130
|
+
const d = p.dlp;
|
|
2131
|
+
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
2132
|
+
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
2133
|
+
}
|
|
1861
2134
|
const envs = source.environments || {};
|
|
1862
2135
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
1863
2136
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -1872,6 +2145,19 @@ function getConfig() {
|
|
|
1872
2145
|
};
|
|
1873
2146
|
applyLayer(globalConfig);
|
|
1874
2147
|
applyLayer(projectConfig);
|
|
2148
|
+
for (const shieldName of readActiveShields()) {
|
|
2149
|
+
const shield = getShield(shieldName);
|
|
2150
|
+
if (!shield) continue;
|
|
2151
|
+
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2152
|
+
for (const rule of shield.smartRules) {
|
|
2153
|
+
if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2154
|
+
}
|
|
2155
|
+
for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
|
|
2156
|
+
}
|
|
2157
|
+
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2158
|
+
for (const rule of ADVISORY_SMART_RULES) {
|
|
2159
|
+
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2160
|
+
}
|
|
1875
2161
|
if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
|
|
1876
2162
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
1877
2163
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
@@ -1887,10 +2173,10 @@ function getConfig() {
|
|
|
1887
2173
|
return cachedConfig;
|
|
1888
2174
|
}
|
|
1889
2175
|
function tryLoadConfig(filePath) {
|
|
1890
|
-
if (!
|
|
2176
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
1891
2177
|
let raw;
|
|
1892
2178
|
try {
|
|
1893
|
-
raw = JSON.parse(
|
|
2179
|
+
raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
1894
2180
|
} catch (err) {
|
|
1895
2181
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1896
2182
|
process.stderr.write(
|
|
@@ -1952,9 +2238,9 @@ function getCredentials() {
|
|
|
1952
2238
|
};
|
|
1953
2239
|
}
|
|
1954
2240
|
try {
|
|
1955
|
-
const credPath =
|
|
1956
|
-
if (
|
|
1957
|
-
const creds = JSON.parse(
|
|
2241
|
+
const credPath = path4.join(os2.homedir(), ".node9", "credentials.json");
|
|
2242
|
+
if (fs2.existsSync(credPath)) {
|
|
2243
|
+
const creds = JSON.parse(fs2.readFileSync(credPath, "utf-8"));
|
|
1958
2244
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
1959
2245
|
const profile = creds[profileName];
|
|
1960
2246
|
if (profile?.apiKey) {
|
|
@@ -1985,9 +2271,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1985
2271
|
context: {
|
|
1986
2272
|
agent: meta?.agent,
|
|
1987
2273
|
mcpServer: meta?.mcpServer,
|
|
1988
|
-
hostname:
|
|
2274
|
+
hostname: os2.hostname(),
|
|
1989
2275
|
cwd: process.cwd(),
|
|
1990
|
-
platform:
|
|
2276
|
+
platform: os2.platform()
|
|
1991
2277
|
}
|
|
1992
2278
|
}),
|
|
1993
2279
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -2008,9 +2294,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
2008
2294
|
context: {
|
|
2009
2295
|
agent: meta?.agent,
|
|
2010
2296
|
mcpServer: meta?.mcpServer,
|
|
2011
|
-
hostname:
|
|
2297
|
+
hostname: os2.hostname(),
|
|
2012
2298
|
cwd: process.cwd(),
|
|
2013
|
-
platform:
|
|
2299
|
+
platform: os2.platform()
|
|
2014
2300
|
},
|
|
2015
2301
|
...riskMetadata && { riskMetadata }
|
|
2016
2302
|
}),
|
|
@@ -2069,9 +2355,9 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
2069
2355
|
}
|
|
2070
2356
|
|
|
2071
2357
|
// src/setup.ts
|
|
2072
|
-
import
|
|
2073
|
-
import
|
|
2074
|
-
import
|
|
2358
|
+
import fs3 from "fs";
|
|
2359
|
+
import path5 from "path";
|
|
2360
|
+
import os3 from "os";
|
|
2075
2361
|
import chalk3 from "chalk";
|
|
2076
2362
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
2077
2363
|
function printDaemonTip() {
|
|
@@ -2087,22 +2373,22 @@ function fullPathCommand(subcommand) {
|
|
|
2087
2373
|
}
|
|
2088
2374
|
function readJson(filePath) {
|
|
2089
2375
|
try {
|
|
2090
|
-
if (
|
|
2091
|
-
return JSON.parse(
|
|
2376
|
+
if (fs3.existsSync(filePath)) {
|
|
2377
|
+
return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
2092
2378
|
}
|
|
2093
2379
|
} catch {
|
|
2094
2380
|
}
|
|
2095
2381
|
return null;
|
|
2096
2382
|
}
|
|
2097
2383
|
function writeJson(filePath, data) {
|
|
2098
|
-
const dir =
|
|
2099
|
-
if (!
|
|
2100
|
-
|
|
2384
|
+
const dir = path5.dirname(filePath);
|
|
2385
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
2386
|
+
fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
2101
2387
|
}
|
|
2102
2388
|
async function setupClaude() {
|
|
2103
|
-
const homeDir2 =
|
|
2104
|
-
const mcpPath =
|
|
2105
|
-
const hooksPath =
|
|
2389
|
+
const homeDir2 = os3.homedir();
|
|
2390
|
+
const mcpPath = path5.join(homeDir2, ".claude.json");
|
|
2391
|
+
const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
|
|
2106
2392
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
2107
2393
|
const settings = readJson(hooksPath) ?? {};
|
|
2108
2394
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -2176,8 +2462,8 @@ async function setupClaude() {
|
|
|
2176
2462
|
}
|
|
2177
2463
|
}
|
|
2178
2464
|
async function setupGemini() {
|
|
2179
|
-
const homeDir2 =
|
|
2180
|
-
const settingsPath =
|
|
2465
|
+
const homeDir2 = os3.homedir();
|
|
2466
|
+
const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
|
|
2181
2467
|
const settings = readJson(settingsPath) ?? {};
|
|
2182
2468
|
const servers = settings.mcpServers ?? {};
|
|
2183
2469
|
let anythingChanged = false;
|
|
@@ -2259,36 +2545,11 @@ async function setupGemini() {
|
|
|
2259
2545
|
}
|
|
2260
2546
|
}
|
|
2261
2547
|
async function setupCursor() {
|
|
2262
|
-
const homeDir2 =
|
|
2263
|
-
const mcpPath =
|
|
2264
|
-
const hooksPath = path4.join(homeDir2, ".cursor", "hooks.json");
|
|
2548
|
+
const homeDir2 = os3.homedir();
|
|
2549
|
+
const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
|
|
2265
2550
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
2266
|
-
const hooksFile = readJson(hooksPath) ?? { version: 1 };
|
|
2267
2551
|
const servers = mcpConfig.mcpServers ?? {};
|
|
2268
2552
|
let anythingChanged = false;
|
|
2269
|
-
if (!hooksFile.hooks) hooksFile.hooks = {};
|
|
2270
|
-
const hasPreHook = hooksFile.hooks.preToolUse?.some(
|
|
2271
|
-
(h) => h.command === "node9" && h.args?.includes("check") || h.command?.includes("cli.js")
|
|
2272
|
-
);
|
|
2273
|
-
if (!hasPreHook) {
|
|
2274
|
-
if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
|
|
2275
|
-
hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
|
|
2276
|
-
console.log(chalk3.green(" \u2705 preToolUse hook added \u2192 node9 check"));
|
|
2277
|
-
anythingChanged = true;
|
|
2278
|
-
}
|
|
2279
|
-
const hasPostHook = hooksFile.hooks.postToolUse?.some(
|
|
2280
|
-
(h) => h.command === "node9" && h.args?.includes("log") || h.command?.includes("cli.js")
|
|
2281
|
-
);
|
|
2282
|
-
if (!hasPostHook) {
|
|
2283
|
-
if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
|
|
2284
|
-
hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
|
|
2285
|
-
console.log(chalk3.green(" \u2705 postToolUse hook added \u2192 node9 log"));
|
|
2286
|
-
anythingChanged = true;
|
|
2287
|
-
}
|
|
2288
|
-
if (anythingChanged) {
|
|
2289
|
-
writeJson(hooksPath, hooksFile);
|
|
2290
|
-
console.log("");
|
|
2291
|
-
}
|
|
2292
2553
|
const serversToWrap = [];
|
|
2293
2554
|
for (const [name, server] of Object.entries(servers)) {
|
|
2294
2555
|
if (!server.command || server.command === "node9") continue;
|
|
@@ -2317,13 +2578,23 @@ async function setupCursor() {
|
|
|
2317
2578
|
}
|
|
2318
2579
|
console.log("");
|
|
2319
2580
|
}
|
|
2581
|
+
console.log(
|
|
2582
|
+
chalk3.yellow(
|
|
2583
|
+
" \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
|
|
2584
|
+
)
|
|
2585
|
+
);
|
|
2586
|
+
console.log("");
|
|
2320
2587
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2321
|
-
console.log(
|
|
2588
|
+
console.log(
|
|
2589
|
+
chalk3.blue(
|
|
2590
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
|
|
2591
|
+
)
|
|
2592
|
+
);
|
|
2322
2593
|
printDaemonTip();
|
|
2323
2594
|
return;
|
|
2324
2595
|
}
|
|
2325
2596
|
if (anythingChanged) {
|
|
2326
|
-
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
|
|
2597
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
|
|
2327
2598
|
console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
|
|
2328
2599
|
printDaemonTip();
|
|
2329
2600
|
}
|
|
@@ -3373,34 +3644,34 @@ var UI_HTML_TEMPLATE = ui_default;
|
|
|
3373
3644
|
|
|
3374
3645
|
// src/daemon/index.ts
|
|
3375
3646
|
import http from "http";
|
|
3376
|
-
import
|
|
3377
|
-
import
|
|
3378
|
-
import
|
|
3647
|
+
import fs4 from "fs";
|
|
3648
|
+
import path6 from "path";
|
|
3649
|
+
import os4 from "os";
|
|
3379
3650
|
import { spawn as spawn2 } from "child_process";
|
|
3380
3651
|
import { randomUUID } from "crypto";
|
|
3381
3652
|
import chalk4 from "chalk";
|
|
3382
3653
|
var DAEMON_PORT2 = 7391;
|
|
3383
3654
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
3384
|
-
var homeDir =
|
|
3385
|
-
var DAEMON_PID_FILE =
|
|
3386
|
-
var DECISIONS_FILE =
|
|
3387
|
-
var GLOBAL_CONFIG_FILE =
|
|
3388
|
-
var CREDENTIALS_FILE =
|
|
3389
|
-
var AUDIT_LOG_FILE =
|
|
3390
|
-
var TRUST_FILE2 =
|
|
3655
|
+
var homeDir = os4.homedir();
|
|
3656
|
+
var DAEMON_PID_FILE = path6.join(homeDir, ".node9", "daemon.pid");
|
|
3657
|
+
var DECISIONS_FILE = path6.join(homeDir, ".node9", "decisions.json");
|
|
3658
|
+
var GLOBAL_CONFIG_FILE = path6.join(homeDir, ".node9", "config.json");
|
|
3659
|
+
var CREDENTIALS_FILE = path6.join(homeDir, ".node9", "credentials.json");
|
|
3660
|
+
var AUDIT_LOG_FILE = path6.join(homeDir, ".node9", "audit.log");
|
|
3661
|
+
var TRUST_FILE2 = path6.join(homeDir, ".node9", "trust.json");
|
|
3391
3662
|
function atomicWriteSync2(filePath, data, options) {
|
|
3392
|
-
const dir =
|
|
3393
|
-
if (!
|
|
3663
|
+
const dir = path6.dirname(filePath);
|
|
3664
|
+
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
3394
3665
|
const tmpPath = `${filePath}.${randomUUID()}.tmp`;
|
|
3395
|
-
|
|
3396
|
-
|
|
3666
|
+
fs4.writeFileSync(tmpPath, data, options);
|
|
3667
|
+
fs4.renameSync(tmpPath, filePath);
|
|
3397
3668
|
}
|
|
3398
3669
|
function writeTrustEntry(toolName, durationMs) {
|
|
3399
3670
|
try {
|
|
3400
3671
|
let trust = { entries: [] };
|
|
3401
3672
|
try {
|
|
3402
|
-
if (
|
|
3403
|
-
trust = JSON.parse(
|
|
3673
|
+
if (fs4.existsSync(TRUST_FILE2))
|
|
3674
|
+
trust = JSON.parse(fs4.readFileSync(TRUST_FILE2, "utf-8"));
|
|
3404
3675
|
} catch {
|
|
3405
3676
|
}
|
|
3406
3677
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -3433,16 +3704,16 @@ function appendAuditLog(data) {
|
|
|
3433
3704
|
decision: data.decision,
|
|
3434
3705
|
source: "daemon"
|
|
3435
3706
|
};
|
|
3436
|
-
const dir =
|
|
3437
|
-
if (!
|
|
3438
|
-
|
|
3707
|
+
const dir = path6.dirname(AUDIT_LOG_FILE);
|
|
3708
|
+
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
3709
|
+
fs4.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
3439
3710
|
} catch {
|
|
3440
3711
|
}
|
|
3441
3712
|
}
|
|
3442
3713
|
function getAuditHistory(limit = 20) {
|
|
3443
3714
|
try {
|
|
3444
|
-
if (!
|
|
3445
|
-
const lines =
|
|
3715
|
+
if (!fs4.existsSync(AUDIT_LOG_FILE)) return [];
|
|
3716
|
+
const lines = fs4.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
3446
3717
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
3447
3718
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
3448
3719
|
} catch {
|
|
@@ -3452,7 +3723,7 @@ function getAuditHistory(limit = 20) {
|
|
|
3452
3723
|
var AUTO_DENY_MS = 12e4;
|
|
3453
3724
|
function getOrgName() {
|
|
3454
3725
|
try {
|
|
3455
|
-
if (
|
|
3726
|
+
if (fs4.existsSync(CREDENTIALS_FILE)) {
|
|
3456
3727
|
return "Node9 Cloud";
|
|
3457
3728
|
}
|
|
3458
3729
|
} catch {
|
|
@@ -3461,13 +3732,13 @@ function getOrgName() {
|
|
|
3461
3732
|
}
|
|
3462
3733
|
var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
3463
3734
|
function hasStoredSlackKey() {
|
|
3464
|
-
return
|
|
3735
|
+
return fs4.existsSync(CREDENTIALS_FILE);
|
|
3465
3736
|
}
|
|
3466
3737
|
function writeGlobalSetting(key, value) {
|
|
3467
3738
|
let config = {};
|
|
3468
3739
|
try {
|
|
3469
|
-
if (
|
|
3470
|
-
config = JSON.parse(
|
|
3740
|
+
if (fs4.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
3741
|
+
config = JSON.parse(fs4.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
3471
3742
|
}
|
|
3472
3743
|
} catch {
|
|
3473
3744
|
}
|
|
@@ -3491,7 +3762,7 @@ function abandonPending() {
|
|
|
3491
3762
|
});
|
|
3492
3763
|
if (autoStarted) {
|
|
3493
3764
|
try {
|
|
3494
|
-
|
|
3765
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
3495
3766
|
} catch {
|
|
3496
3767
|
}
|
|
3497
3768
|
setTimeout(() => {
|
|
@@ -3529,8 +3800,8 @@ function readBody(req) {
|
|
|
3529
3800
|
}
|
|
3530
3801
|
function readPersistentDecisions() {
|
|
3531
3802
|
try {
|
|
3532
|
-
if (
|
|
3533
|
-
return JSON.parse(
|
|
3803
|
+
if (fs4.existsSync(DECISIONS_FILE)) {
|
|
3804
|
+
return JSON.parse(fs4.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
3534
3805
|
}
|
|
3535
3806
|
} catch {
|
|
3536
3807
|
}
|
|
@@ -3557,7 +3828,7 @@ function startDaemon() {
|
|
|
3557
3828
|
idleTimer = setTimeout(() => {
|
|
3558
3829
|
if (autoStarted) {
|
|
3559
3830
|
try {
|
|
3560
|
-
|
|
3831
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
3561
3832
|
} catch {
|
|
3562
3833
|
}
|
|
3563
3834
|
}
|
|
@@ -3875,14 +4146,14 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3875
4146
|
server.on("error", (e) => {
|
|
3876
4147
|
if (e.code === "EADDRINUSE") {
|
|
3877
4148
|
try {
|
|
3878
|
-
if (
|
|
3879
|
-
const { pid } = JSON.parse(
|
|
4149
|
+
if (fs4.existsSync(DAEMON_PID_FILE)) {
|
|
4150
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
3880
4151
|
process.kill(pid, 0);
|
|
3881
4152
|
return process.exit(0);
|
|
3882
4153
|
}
|
|
3883
4154
|
} catch {
|
|
3884
4155
|
try {
|
|
3885
|
-
|
|
4156
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
3886
4157
|
} catch {
|
|
3887
4158
|
}
|
|
3888
4159
|
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
@@ -3902,25 +4173,25 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3902
4173
|
});
|
|
3903
4174
|
}
|
|
3904
4175
|
function stopDaemon() {
|
|
3905
|
-
if (!
|
|
4176
|
+
if (!fs4.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
|
|
3906
4177
|
try {
|
|
3907
|
-
const { pid } = JSON.parse(
|
|
4178
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
3908
4179
|
process.kill(pid, "SIGTERM");
|
|
3909
4180
|
console.log(chalk4.green("\u2705 Stopped."));
|
|
3910
4181
|
} catch {
|
|
3911
4182
|
console.log(chalk4.gray("Cleaned up stale PID file."));
|
|
3912
4183
|
} finally {
|
|
3913
4184
|
try {
|
|
3914
|
-
|
|
4185
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
3915
4186
|
} catch {
|
|
3916
4187
|
}
|
|
3917
4188
|
}
|
|
3918
4189
|
}
|
|
3919
4190
|
function daemonStatus() {
|
|
3920
|
-
if (!
|
|
4191
|
+
if (!fs4.existsSync(DAEMON_PID_FILE))
|
|
3921
4192
|
return console.log(chalk4.yellow("Node9 daemon: not running"));
|
|
3922
4193
|
try {
|
|
3923
|
-
const { pid } = JSON.parse(
|
|
4194
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
3924
4195
|
process.kill(pid, 0);
|
|
3925
4196
|
console.log(chalk4.green("Node9 daemon: running"));
|
|
3926
4197
|
} catch {
|
|
@@ -3934,30 +4205,30 @@ import { parseCommandString } from "execa";
|
|
|
3934
4205
|
import { execa } from "execa";
|
|
3935
4206
|
import chalk5 from "chalk";
|
|
3936
4207
|
import readline from "readline";
|
|
3937
|
-
import
|
|
3938
|
-
import
|
|
3939
|
-
import
|
|
4208
|
+
import fs6 from "fs";
|
|
4209
|
+
import path8 from "path";
|
|
4210
|
+
import os6 from "os";
|
|
3940
4211
|
|
|
3941
4212
|
// src/undo.ts
|
|
3942
4213
|
import { spawnSync } from "child_process";
|
|
3943
|
-
import
|
|
3944
|
-
import
|
|
3945
|
-
import
|
|
3946
|
-
var SNAPSHOT_STACK_PATH =
|
|
3947
|
-
var UNDO_LATEST_PATH =
|
|
4214
|
+
import fs5 from "fs";
|
|
4215
|
+
import path7 from "path";
|
|
4216
|
+
import os5 from "os";
|
|
4217
|
+
var SNAPSHOT_STACK_PATH = path7.join(os5.homedir(), ".node9", "snapshots.json");
|
|
4218
|
+
var UNDO_LATEST_PATH = path7.join(os5.homedir(), ".node9", "undo_latest.txt");
|
|
3948
4219
|
var MAX_SNAPSHOTS = 10;
|
|
3949
4220
|
function readStack() {
|
|
3950
4221
|
try {
|
|
3951
|
-
if (
|
|
3952
|
-
return JSON.parse(
|
|
4222
|
+
if (fs5.existsSync(SNAPSHOT_STACK_PATH))
|
|
4223
|
+
return JSON.parse(fs5.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
3953
4224
|
} catch {
|
|
3954
4225
|
}
|
|
3955
4226
|
return [];
|
|
3956
4227
|
}
|
|
3957
4228
|
function writeStack(stack) {
|
|
3958
|
-
const dir =
|
|
3959
|
-
if (!
|
|
3960
|
-
|
|
4229
|
+
const dir = path7.dirname(SNAPSHOT_STACK_PATH);
|
|
4230
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
4231
|
+
fs5.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
3961
4232
|
}
|
|
3962
4233
|
function buildArgsSummary(tool, args) {
|
|
3963
4234
|
if (!args || typeof args !== "object") return "";
|
|
@@ -3973,13 +4244,13 @@ function buildArgsSummary(tool, args) {
|
|
|
3973
4244
|
async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
3974
4245
|
try {
|
|
3975
4246
|
const cwd = process.cwd();
|
|
3976
|
-
if (!
|
|
3977
|
-
const tempIndex =
|
|
4247
|
+
if (!fs5.existsSync(path7.join(cwd, ".git"))) return null;
|
|
4248
|
+
const tempIndex = path7.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
3978
4249
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
3979
4250
|
spawnSync("git", ["add", "-A"], { env });
|
|
3980
4251
|
const treeRes = spawnSync("git", ["write-tree"], { env });
|
|
3981
4252
|
const treeHash = treeRes.stdout.toString().trim();
|
|
3982
|
-
if (
|
|
4253
|
+
if (fs5.existsSync(tempIndex)) fs5.unlinkSync(tempIndex);
|
|
3983
4254
|
if (!treeHash || treeRes.status !== 0) return null;
|
|
3984
4255
|
const commitRes = spawnSync("git", [
|
|
3985
4256
|
"commit-tree",
|
|
@@ -4000,7 +4271,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
|
4000
4271
|
stack.push(entry);
|
|
4001
4272
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
4002
4273
|
writeStack(stack);
|
|
4003
|
-
|
|
4274
|
+
fs5.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
4004
4275
|
return commitHash;
|
|
4005
4276
|
} catch (err) {
|
|
4006
4277
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
|
|
@@ -4038,9 +4309,9 @@ function applyUndo(hash, cwd) {
|
|
|
4038
4309
|
const tracked = spawnSync("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
4039
4310
|
const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
4040
4311
|
for (const file of [...tracked, ...untracked]) {
|
|
4041
|
-
const fullPath =
|
|
4042
|
-
if (!snapshotFiles.has(file) &&
|
|
4043
|
-
|
|
4312
|
+
const fullPath = path7.join(dir, file);
|
|
4313
|
+
if (!snapshotFiles.has(file) && fs5.existsSync(fullPath)) {
|
|
4314
|
+
fs5.unlinkSync(fullPath);
|
|
4044
4315
|
}
|
|
4045
4316
|
}
|
|
4046
4317
|
return true;
|
|
@@ -4052,7 +4323,7 @@ function applyUndo(hash, cwd) {
|
|
|
4052
4323
|
// src/cli.ts
|
|
4053
4324
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
4054
4325
|
var { version } = JSON.parse(
|
|
4055
|
-
|
|
4326
|
+
fs6.readFileSync(path8.join(__dirname, "../package.json"), "utf-8")
|
|
4056
4327
|
);
|
|
4057
4328
|
function parseDuration(str) {
|
|
4058
4329
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -4084,6 +4355,15 @@ INSTRUCTIONS:
|
|
|
4084
4355
|
- If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
|
|
4085
4356
|
}
|
|
4086
4357
|
const label = blockedByLabel.toLowerCase();
|
|
4358
|
+
if (label.includes("dlp") || label.includes("secret detected") || label.includes("credential review")) {
|
|
4359
|
+
return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
|
|
4360
|
+
CRITICAL INSTRUCTION: Do NOT retry this action.
|
|
4361
|
+
REQUIRED ACTIONS:
|
|
4362
|
+
1. Remove the hardcoded credential from your command or code.
|
|
4363
|
+
2. Use an environment variable or a dedicated secrets manager instead.
|
|
4364
|
+
3. Treat the leaked credential as compromised and rotate it immediately.
|
|
4365
|
+
Do NOT attempt to bypass this check or pass the credential through another tool.`;
|
|
4366
|
+
}
|
|
4087
4367
|
if (label.includes("sql safety") && label.includes("delete without where")) {
|
|
4088
4368
|
return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
|
|
4089
4369
|
INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
|
|
@@ -4245,14 +4525,14 @@ async function runProxy(targetCommand) {
|
|
|
4245
4525
|
}
|
|
4246
4526
|
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) => {
|
|
4247
4527
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
4248
|
-
const credPath =
|
|
4249
|
-
if (!
|
|
4250
|
-
|
|
4528
|
+
const credPath = path8.join(os6.homedir(), ".node9", "credentials.json");
|
|
4529
|
+
if (!fs6.existsSync(path8.dirname(credPath)))
|
|
4530
|
+
fs6.mkdirSync(path8.dirname(credPath), { recursive: true });
|
|
4251
4531
|
const profileName = options.profile || "default";
|
|
4252
4532
|
let existingCreds = {};
|
|
4253
4533
|
try {
|
|
4254
|
-
if (
|
|
4255
|
-
const raw = JSON.parse(
|
|
4534
|
+
if (fs6.existsSync(credPath)) {
|
|
4535
|
+
const raw = JSON.parse(fs6.readFileSync(credPath, "utf-8"));
|
|
4256
4536
|
if (raw.apiKey) {
|
|
4257
4537
|
existingCreds = {
|
|
4258
4538
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -4264,13 +4544,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4264
4544
|
} catch {
|
|
4265
4545
|
}
|
|
4266
4546
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
4267
|
-
|
|
4547
|
+
fs6.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
4268
4548
|
if (profileName === "default") {
|
|
4269
|
-
const configPath =
|
|
4549
|
+
const configPath = path8.join(os6.homedir(), ".node9", "config.json");
|
|
4270
4550
|
let config = {};
|
|
4271
4551
|
try {
|
|
4272
|
-
if (
|
|
4273
|
-
config = JSON.parse(
|
|
4552
|
+
if (fs6.existsSync(configPath))
|
|
4553
|
+
config = JSON.parse(fs6.readFileSync(configPath, "utf-8"));
|
|
4274
4554
|
} catch {
|
|
4275
4555
|
}
|
|
4276
4556
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -4285,9 +4565,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4285
4565
|
approvers.cloud = false;
|
|
4286
4566
|
}
|
|
4287
4567
|
s.approvers = approvers;
|
|
4288
|
-
if (!
|
|
4289
|
-
|
|
4290
|
-
|
|
4568
|
+
if (!fs6.existsSync(path8.dirname(configPath)))
|
|
4569
|
+
fs6.mkdirSync(path8.dirname(configPath), { recursive: true });
|
|
4570
|
+
fs6.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
4291
4571
|
}
|
|
4292
4572
|
if (options.profile && profileName !== "default") {
|
|
4293
4573
|
console.log(chalk5.green(`\u2705 Profile "${profileName}" saved`));
|
|
@@ -4326,7 +4606,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
4326
4606
|
process.exit(1);
|
|
4327
4607
|
});
|
|
4328
4608
|
program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
4329
|
-
const homeDir2 =
|
|
4609
|
+
const homeDir2 = os6.homedir();
|
|
4330
4610
|
let failures = 0;
|
|
4331
4611
|
function pass(msg) {
|
|
4332
4612
|
console.log(chalk5.green(" \u2705 ") + msg);
|
|
@@ -4372,10 +4652,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4372
4652
|
);
|
|
4373
4653
|
}
|
|
4374
4654
|
section("Configuration");
|
|
4375
|
-
const globalConfigPath =
|
|
4376
|
-
if (
|
|
4655
|
+
const globalConfigPath = path8.join(homeDir2, ".node9", "config.json");
|
|
4656
|
+
if (fs6.existsSync(globalConfigPath)) {
|
|
4377
4657
|
try {
|
|
4378
|
-
JSON.parse(
|
|
4658
|
+
JSON.parse(fs6.readFileSync(globalConfigPath, "utf-8"));
|
|
4379
4659
|
pass("~/.node9/config.json found and valid");
|
|
4380
4660
|
} catch {
|
|
4381
4661
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -4383,17 +4663,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4383
4663
|
} else {
|
|
4384
4664
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
4385
4665
|
}
|
|
4386
|
-
const projectConfigPath =
|
|
4387
|
-
if (
|
|
4666
|
+
const projectConfigPath = path8.join(process.cwd(), "node9.config.json");
|
|
4667
|
+
if (fs6.existsSync(projectConfigPath)) {
|
|
4388
4668
|
try {
|
|
4389
|
-
JSON.parse(
|
|
4669
|
+
JSON.parse(fs6.readFileSync(projectConfigPath, "utf-8"));
|
|
4390
4670
|
pass("node9.config.json found and valid (project)");
|
|
4391
4671
|
} catch {
|
|
4392
4672
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
4393
4673
|
}
|
|
4394
4674
|
}
|
|
4395
|
-
const credsPath =
|
|
4396
|
-
if (
|
|
4675
|
+
const credsPath = path8.join(homeDir2, ".node9", "credentials.json");
|
|
4676
|
+
if (fs6.existsSync(credsPath)) {
|
|
4397
4677
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
4398
4678
|
} else {
|
|
4399
4679
|
warn(
|
|
@@ -4402,10 +4682,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4402
4682
|
);
|
|
4403
4683
|
}
|
|
4404
4684
|
section("Agent Hooks");
|
|
4405
|
-
const claudeSettingsPath =
|
|
4406
|
-
if (
|
|
4685
|
+
const claudeSettingsPath = path8.join(homeDir2, ".claude", "settings.json");
|
|
4686
|
+
if (fs6.existsSync(claudeSettingsPath)) {
|
|
4407
4687
|
try {
|
|
4408
|
-
const cs = JSON.parse(
|
|
4688
|
+
const cs = JSON.parse(fs6.readFileSync(claudeSettingsPath, "utf-8"));
|
|
4409
4689
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
4410
4690
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4411
4691
|
);
|
|
@@ -4418,10 +4698,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4418
4698
|
} else {
|
|
4419
4699
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
4420
4700
|
}
|
|
4421
|
-
const geminiSettingsPath =
|
|
4422
|
-
if (
|
|
4701
|
+
const geminiSettingsPath = path8.join(homeDir2, ".gemini", "settings.json");
|
|
4702
|
+
if (fs6.existsSync(geminiSettingsPath)) {
|
|
4423
4703
|
try {
|
|
4424
|
-
const gs = JSON.parse(
|
|
4704
|
+
const gs = JSON.parse(fs6.readFileSync(geminiSettingsPath, "utf-8"));
|
|
4425
4705
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
4426
4706
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4427
4707
|
);
|
|
@@ -4434,10 +4714,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4434
4714
|
} else {
|
|
4435
4715
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
4436
4716
|
}
|
|
4437
|
-
const cursorHooksPath =
|
|
4438
|
-
if (
|
|
4717
|
+
const cursorHooksPath = path8.join(homeDir2, ".cursor", "hooks.json");
|
|
4718
|
+
if (fs6.existsSync(cursorHooksPath)) {
|
|
4439
4719
|
try {
|
|
4440
|
-
const cur = JSON.parse(
|
|
4720
|
+
const cur = JSON.parse(fs6.readFileSync(cursorHooksPath, "utf-8"));
|
|
4441
4721
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
4442
4722
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
4443
4723
|
);
|
|
@@ -4539,8 +4819,8 @@ program.command("explain").description(
|
|
|
4539
4819
|
console.log("");
|
|
4540
4820
|
});
|
|
4541
4821
|
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) => {
|
|
4542
|
-
const configPath =
|
|
4543
|
-
if (
|
|
4822
|
+
const configPath = path8.join(os6.homedir(), ".node9", "config.json");
|
|
4823
|
+
if (fs6.existsSync(configPath) && !options.force) {
|
|
4544
4824
|
console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
4545
4825
|
console.log(chalk5.gray(` Run with --force to overwrite.`));
|
|
4546
4826
|
return;
|
|
@@ -4554,9 +4834,9 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
4554
4834
|
mode: safeMode
|
|
4555
4835
|
}
|
|
4556
4836
|
};
|
|
4557
|
-
const dir =
|
|
4558
|
-
if (!
|
|
4559
|
-
|
|
4837
|
+
const dir = path8.dirname(configPath);
|
|
4838
|
+
if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
|
|
4839
|
+
fs6.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
4560
4840
|
console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
|
|
4561
4841
|
console.log(chalk5.cyan(` Mode set to: ${safeMode}`));
|
|
4562
4842
|
console.log(
|
|
@@ -4574,14 +4854,14 @@ function formatRelativeTime(timestamp) {
|
|
|
4574
4854
|
return new Date(timestamp).toLocaleDateString();
|
|
4575
4855
|
}
|
|
4576
4856
|
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) => {
|
|
4577
|
-
const logPath =
|
|
4578
|
-
if (!
|
|
4857
|
+
const logPath = path8.join(os6.homedir(), ".node9", "audit.log");
|
|
4858
|
+
if (!fs6.existsSync(logPath)) {
|
|
4579
4859
|
console.log(
|
|
4580
4860
|
chalk5.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
4581
4861
|
);
|
|
4582
4862
|
return;
|
|
4583
4863
|
}
|
|
4584
|
-
const raw =
|
|
4864
|
+
const raw = fs6.readFileSync(logPath, "utf-8");
|
|
4585
4865
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
4586
4866
|
let entries = lines.flatMap((line) => {
|
|
4587
4867
|
try {
|
|
@@ -4664,13 +4944,13 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4664
4944
|
console.log("");
|
|
4665
4945
|
const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
|
|
4666
4946
|
console.log(` Mode: ${modeLabel}`);
|
|
4667
|
-
const projectConfig =
|
|
4668
|
-
const globalConfig =
|
|
4947
|
+
const projectConfig = path8.join(process.cwd(), "node9.config.json");
|
|
4948
|
+
const globalConfig = path8.join(os6.homedir(), ".node9", "config.json");
|
|
4669
4949
|
console.log(
|
|
4670
|
-
` Local: ${
|
|
4950
|
+
` Local: ${fs6.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
|
|
4671
4951
|
);
|
|
4672
4952
|
console.log(
|
|
4673
|
-
` Global: ${
|
|
4953
|
+
` Global: ${fs6.existsSync(globalConfig) ? chalk5.green("Active (~/.node9/config.json)") : chalk5.gray("Not present")}`
|
|
4674
4954
|
);
|
|
4675
4955
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
4676
4956
|
console.log(
|
|
@@ -4733,9 +5013,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
4733
5013
|
} catch (err) {
|
|
4734
5014
|
const tempConfig = getConfig();
|
|
4735
5015
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
4736
|
-
const logPath =
|
|
5016
|
+
const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
|
|
4737
5017
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4738
|
-
|
|
5018
|
+
fs6.appendFileSync(
|
|
4739
5019
|
logPath,
|
|
4740
5020
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
4741
5021
|
RAW: ${raw}
|
|
@@ -4753,10 +5033,10 @@ RAW: ${raw}
|
|
|
4753
5033
|
}
|
|
4754
5034
|
const config = getConfig();
|
|
4755
5035
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
4756
|
-
const logPath =
|
|
4757
|
-
if (!
|
|
4758
|
-
|
|
4759
|
-
|
|
5036
|
+
const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
|
|
5037
|
+
if (!fs6.existsSync(path8.dirname(logPath)))
|
|
5038
|
+
fs6.mkdirSync(path8.dirname(logPath), { recursive: true });
|
|
5039
|
+
fs6.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
4760
5040
|
`);
|
|
4761
5041
|
}
|
|
4762
5042
|
const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
|
|
@@ -4767,8 +5047,14 @@ RAW: ${raw}
|
|
|
4767
5047
|
const sendBlock = (msg, result2) => {
|
|
4768
5048
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
4769
5049
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
4770
|
-
|
|
5050
|
+
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
5051
|
+
console.error(chalk5.bgRed.white.bold(`
|
|
5052
|
+
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
5053
|
+
console.error(chalk5.red.bold(` A sensitive secret was found in the tool arguments!`));
|
|
5054
|
+
} else {
|
|
5055
|
+
console.error(chalk5.red(`
|
|
4771
5056
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
5057
|
+
}
|
|
4772
5058
|
console.error(chalk5.gray(` Triggered by: ${blockedByContext}`));
|
|
4773
5059
|
if (result2?.changeHint) console.error(chalk5.cyan(` To change: ${result2.changeHint}`));
|
|
4774
5060
|
console.error("");
|
|
@@ -4827,9 +5113,9 @@ RAW: ${raw}
|
|
|
4827
5113
|
});
|
|
4828
5114
|
} catch (err) {
|
|
4829
5115
|
if (process.env.NODE9_DEBUG === "1") {
|
|
4830
|
-
const logPath =
|
|
5116
|
+
const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
|
|
4831
5117
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4832
|
-
|
|
5118
|
+
fs6.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
4833
5119
|
`);
|
|
4834
5120
|
}
|
|
4835
5121
|
process.exit(0);
|
|
@@ -4874,10 +5160,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
4874
5160
|
decision: "allowed",
|
|
4875
5161
|
source: "post-hook"
|
|
4876
5162
|
};
|
|
4877
|
-
const logPath =
|
|
4878
|
-
if (!
|
|
4879
|
-
|
|
4880
|
-
|
|
5163
|
+
const logPath = path8.join(os6.homedir(), ".node9", "audit.log");
|
|
5164
|
+
if (!fs6.existsSync(path8.dirname(logPath)))
|
|
5165
|
+
fs6.mkdirSync(path8.dirname(logPath), { recursive: true });
|
|
5166
|
+
fs6.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4881
5167
|
const config = getConfig();
|
|
4882
5168
|
if (shouldSnapshot(tool, {}, config)) {
|
|
4883
5169
|
await createShadowSnapshot();
|
|
@@ -5059,13 +5345,103 @@ program.command("undo").description(
|
|
|
5059
5345
|
console.log(chalk5.gray("\nCancelled.\n"));
|
|
5060
5346
|
}
|
|
5061
5347
|
});
|
|
5348
|
+
var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
|
|
5349
|
+
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
5350
|
+
const name = resolveShieldName(service);
|
|
5351
|
+
if (!name) {
|
|
5352
|
+
console.error(chalk5.red(`
|
|
5353
|
+
\u274C Unknown shield: "${service}"
|
|
5354
|
+
`));
|
|
5355
|
+
console.log(`Run ${chalk5.cyan("node9 shield list")} to see available shields.
|
|
5356
|
+
`);
|
|
5357
|
+
process.exit(1);
|
|
5358
|
+
}
|
|
5359
|
+
const shield = getShield(name);
|
|
5360
|
+
const active = readActiveShields();
|
|
5361
|
+
if (active.includes(name)) {
|
|
5362
|
+
console.log(chalk5.yellow(`
|
|
5363
|
+
\u2139\uFE0F Shield "${name}" is already active.
|
|
5364
|
+
`));
|
|
5365
|
+
return;
|
|
5366
|
+
}
|
|
5367
|
+
writeActiveShields([...active, name]);
|
|
5368
|
+
console.log(chalk5.green(`
|
|
5369
|
+
\u{1F6E1}\uFE0F Shield "${name}" enabled.`));
|
|
5370
|
+
console.log(chalk5.gray(` ${shield.smartRules.length} smart rules now active.`));
|
|
5371
|
+
if (shield.dangerousWords.length > 0)
|
|
5372
|
+
console.log(chalk5.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
|
|
5373
|
+
if (name === "filesystem") {
|
|
5374
|
+
console.log(
|
|
5375
|
+
chalk5.yellow(
|
|
5376
|
+
`
|
|
5377
|
+
\u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
|
|
5378
|
+
Tools like unlink, find -delete, or language-level file ops are not intercepted.`
|
|
5379
|
+
)
|
|
5380
|
+
);
|
|
5381
|
+
}
|
|
5382
|
+
console.log("");
|
|
5383
|
+
});
|
|
5384
|
+
shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
|
|
5385
|
+
const name = resolveShieldName(service);
|
|
5386
|
+
if (!name) {
|
|
5387
|
+
console.error(chalk5.red(`
|
|
5388
|
+
\u274C Unknown shield: "${service}"
|
|
5389
|
+
`));
|
|
5390
|
+
console.log(`Run ${chalk5.cyan("node9 shield list")} to see available shields.
|
|
5391
|
+
`);
|
|
5392
|
+
process.exit(1);
|
|
5393
|
+
}
|
|
5394
|
+
const active = readActiveShields();
|
|
5395
|
+
if (!active.includes(name)) {
|
|
5396
|
+
console.log(chalk5.yellow(`
|
|
5397
|
+
\u2139\uFE0F Shield "${name}" is not active.
|
|
5398
|
+
`));
|
|
5399
|
+
return;
|
|
5400
|
+
}
|
|
5401
|
+
writeActiveShields(active.filter((s) => s !== name));
|
|
5402
|
+
console.log(chalk5.green(`
|
|
5403
|
+
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
5404
|
+
`));
|
|
5405
|
+
});
|
|
5406
|
+
shieldCmd.command("list").description("Show all available shields").action(() => {
|
|
5407
|
+
const active = new Set(readActiveShields());
|
|
5408
|
+
console.log(chalk5.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
5409
|
+
for (const shield of listShields()) {
|
|
5410
|
+
const status = active.has(shield.name) ? chalk5.green("\u25CF enabled") : chalk5.gray("\u25CB disabled");
|
|
5411
|
+
console.log(` ${status} ${chalk5.cyan(shield.name.padEnd(12))} ${shield.description}`);
|
|
5412
|
+
if (shield.aliases.length > 0)
|
|
5413
|
+
console.log(chalk5.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
5414
|
+
}
|
|
5415
|
+
console.log("");
|
|
5416
|
+
});
|
|
5417
|
+
shieldCmd.command("status").description("Show which shields are currently active").action(() => {
|
|
5418
|
+
const active = readActiveShields();
|
|
5419
|
+
if (active.length === 0) {
|
|
5420
|
+
console.log(chalk5.yellow("\n\u2139\uFE0F No shields are active.\n"));
|
|
5421
|
+
console.log(`Run ${chalk5.cyan("node9 shield list")} to see available shields.
|
|
5422
|
+
`);
|
|
5423
|
+
return;
|
|
5424
|
+
}
|
|
5425
|
+
console.log(chalk5.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
5426
|
+
for (const name of active) {
|
|
5427
|
+
const shield = getShield(name);
|
|
5428
|
+
if (!shield) continue;
|
|
5429
|
+
console.log(` ${chalk5.green("\u25CF")} ${chalk5.cyan(name)}`);
|
|
5430
|
+
console.log(
|
|
5431
|
+
chalk5.gray(
|
|
5432
|
+
` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
|
|
5433
|
+
)
|
|
5434
|
+
);
|
|
5435
|
+
}
|
|
5436
|
+
console.log("");
|
|
5437
|
+
});
|
|
5062
5438
|
process.on("unhandledRejection", (reason) => {
|
|
5063
5439
|
const isCheckHook = process.argv[2] === "check";
|
|
5064
5440
|
if (isCheckHook) {
|
|
5065
5441
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
5066
|
-
const logPath =
|
|
5442
|
+
const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
|
|
5067
5443
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
5068
|
-
|
|
5444
|
+
fs6.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
5069
5445
|
`);
|
|
5070
5446
|
}
|
|
5071
5447
|
process.exit(0);
|