@node9/proxy 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,9 +1,61 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/audit/hasher.ts
12
+ import { createHash } from "crypto";
13
+ function canonicalise(value) {
14
+ return _canonicalise(value, /* @__PURE__ */ new WeakSet());
15
+ }
16
+ function _canonicalise(value, seen) {
17
+ if (value === null || typeof value !== "object") return value;
18
+ if (value instanceof Date) return value.toISOString();
19
+ if (value instanceof RegExp) return value.toString();
20
+ if (Buffer.isBuffer(value)) return value.toString("base64");
21
+ if (seen.has(value)) return "[Circular]";
22
+ seen.add(value);
23
+ let result;
24
+ if (Array.isArray(value)) {
25
+ result = value.map((v) => _canonicalise(v, seen));
26
+ } else {
27
+ const obj = value;
28
+ result = Object.fromEntries(
29
+ Object.keys(obj).sort().map((k) => [k, _canonicalise(obj[k], seen)])
30
+ );
31
+ }
32
+ seen.delete(value);
33
+ return result;
34
+ }
35
+ function hashArgs(args) {
36
+ const canonical = JSON.stringify(canonicalise(args) ?? null);
37
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
38
+ }
39
+ var init_hasher = __esm({
40
+ "src/audit/hasher.ts"() {
41
+ "use strict";
42
+ }
43
+ });
44
+
1
45
  // src/audit/index.ts
46
+ var audit_exports = {};
47
+ __export(audit_exports, {
48
+ HOOK_DEBUG_LOG: () => HOOK_DEBUG_LOG,
49
+ LOCAL_AUDIT_LOG: () => LOCAL_AUDIT_LOG,
50
+ appendConfigAudit: () => appendConfigAudit,
51
+ appendHookDebug: () => appendHookDebug,
52
+ appendLocalAudit: () => appendLocalAudit,
53
+ appendToLog: () => appendToLog,
54
+ redactSecrets: () => redactSecrets
55
+ });
2
56
  import fs from "fs";
3
57
  import path from "path";
4
58
  import os from "os";
5
- var LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
6
- var HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
7
59
  function redactSecrets(text) {
8
60
  if (!text) return text;
9
61
  let redacted = text;
@@ -25,24 +77,24 @@ function appendToLog(logPath, entry) {
25
77
  } catch {
26
78
  }
27
79
  }
28
- function appendHookDebug(toolName, args, meta) {
29
- const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
80
+ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
81
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
30
82
  appendToLog(HOOK_DEBUG_LOG, {
31
83
  ts: (/* @__PURE__ */ new Date()).toISOString(),
32
84
  tool: toolName,
33
- args: safeArgs,
85
+ ...argsField,
34
86
  agent: meta?.agent,
35
87
  mcpServer: meta?.mcpServer,
36
88
  hostname: os.hostname(),
37
89
  cwd: process.cwd()
38
90
  });
39
91
  }
40
- function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
41
- const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
92
+ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
93
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
42
94
  appendToLog(LOCAL_AUDIT_LOG, {
43
95
  ts: (/* @__PURE__ */ new Date()).toISOString(),
44
96
  tool: toolName,
45
- args: safeArgs,
97
+ ...argsField,
46
98
  decision,
47
99
  checkedBy,
48
100
  agent: meta?.agent,
@@ -50,6 +102,25 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
50
102
  hostname: os.hostname()
51
103
  });
52
104
  }
105
+ function appendConfigAudit(entry) {
106
+ appendToLog(LOCAL_AUDIT_LOG, {
107
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
108
+ ...entry,
109
+ hostname: os.hostname()
110
+ });
111
+ }
112
+ var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
113
+ var init_audit = __esm({
114
+ "src/audit/index.ts"() {
115
+ "use strict";
116
+ init_hasher();
117
+ LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
118
+ HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
119
+ }
120
+ });
121
+
122
+ // src/core.ts
123
+ init_audit();
53
124
 
54
125
  // src/config/index.ts
55
126
  import fs3 from "fs";
@@ -118,7 +189,8 @@ var ConfigFileSchema = z.object({
118
189
  environment: z.string().optional(),
119
190
  slackEnabled: z.boolean().optional(),
120
191
  enableTrustSessions: z.boolean().optional(),
121
- allowGlobalPause: z.boolean().optional()
192
+ allowGlobalPause: z.boolean().optional(),
193
+ auditHashArgs: z.boolean().optional()
122
194
  }).optional(),
123
195
  policy: z.object({
124
196
  sandboxPaths: z.array(z.string()).optional(),
@@ -414,6 +486,7 @@ var DEFAULT_CONFIG = {
414
486
  approvalTimeoutMs: 12e4,
415
487
  // 120-second auto-deny timeout
416
488
  flightRecorder: true,
489
+ auditHashArgs: true,
417
490
  approvers: { native: true, browser: true, cloud: false, terminal: true }
418
491
  },
419
492
  policy: {
@@ -1981,6 +2054,45 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
1981
2054
  const { id, allowCount } = await res.json();
1982
2055
  return { id, allowCount: allowCount ?? 1 };
1983
2056
  }
2057
+ async function notifyTaint(filePath, source) {
2058
+ if (!isDaemonRunning()) return;
2059
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2060
+ try {
2061
+ await fetch(`${base}/taint`, {
2062
+ method: "POST",
2063
+ headers: { "Content-Type": "application/json" },
2064
+ body: JSON.stringify({ path: filePath, source }),
2065
+ signal: AbortSignal.timeout(1e3)
2066
+ });
2067
+ } catch {
2068
+ }
2069
+ }
2070
+ async function checkTaint(paths) {
2071
+ if (paths.length === 0) return { tainted: false };
2072
+ if (!isDaemonRunning()) return { tainted: false, daemonUnavailable: true };
2073
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2074
+ try {
2075
+ const res = await fetch(`${base}/taint/check`, {
2076
+ method: "POST",
2077
+ headers: { "Content-Type": "application/json" },
2078
+ body: JSON.stringify({ paths }),
2079
+ signal: AbortSignal.timeout(2e3)
2080
+ });
2081
+ return await res.json();
2082
+ } catch (err) {
2083
+ try {
2084
+ const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
2085
+ appendToLog2(HOOK_DEBUG_LOG2, {
2086
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2087
+ event: "checkTaint-error",
2088
+ error: String(err),
2089
+ paths
2090
+ });
2091
+ } catch {
2092
+ }
2093
+ return { tainted: false, daemonUnavailable: true };
2094
+ }
2095
+ }
1984
2096
  async function resolveViaDaemon(id, decision, internalToken, source) {
1985
2097
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1986
2098
  await fetch(`${base}/resolve/${id}`, {
@@ -2317,7 +2429,11 @@ end run`;
2317
2429
  });
2318
2430
  }
2319
2431
 
2432
+ // src/auth/orchestrator.ts
2433
+ init_audit();
2434
+
2320
2435
  // src/auth/cloud.ts
2436
+ init_audit();
2321
2437
  import fs9 from "fs";
2322
2438
  import os8 from "os";
2323
2439
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
@@ -2428,6 +2544,51 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
2428
2544
  }
2429
2545
 
2430
2546
  // src/auth/orchestrator.ts
2547
+ var WRITE_TOOLS = /* @__PURE__ */ new Set([
2548
+ "write",
2549
+ "write_file",
2550
+ "create_file",
2551
+ "edit",
2552
+ "multiedit",
2553
+ "str_replace_based_edit_tool",
2554
+ "replace",
2555
+ "notebook_edit",
2556
+ "notebookedit"
2557
+ ]);
2558
+ function isWriteTool(toolName) {
2559
+ const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
2560
+ return WRITE_TOOLS.has(t);
2561
+ }
2562
+ function extractFilePaths(toolName, args) {
2563
+ const paths = [];
2564
+ if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
2565
+ const a = args;
2566
+ for (const key of ["file_path", "path", "filename", "source", "src", "input"]) {
2567
+ if (typeof a[key] === "string" && a[key]) paths.push(a[key]);
2568
+ }
2569
+ const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
2570
+ if (cmd) {
2571
+ for (const m of cmd.matchAll(/(?:-T|--upload-file|--data(?:-binary)?)\s+@?(\S+)/g)) {
2572
+ paths.push(m[1]);
2573
+ }
2574
+ for (const m of cmd.matchAll(/\b(?:scp|rsync)\s+(?:-\S+\s+)*(\S+)\s+\S+@/g)) {
2575
+ paths.push(m[1]);
2576
+ }
2577
+ for (const m of cmd.matchAll(/<\s*(\S+)/g)) {
2578
+ paths.push(m[1]);
2579
+ }
2580
+ }
2581
+ return paths.filter(Boolean);
2582
+ }
2583
+ function isNetworkTool(toolName, args) {
2584
+ const t = toolName.toLowerCase();
2585
+ if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
2586
+ const a = args;
2587
+ const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
2588
+ return /\b(curl|wget|scp|rsync|nc|ncat|netcat|ssh)\b/.test(cmd);
2589
+ }
2590
+ return false;
2591
+ }
2431
2592
  var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os9.tmpdir(), "node9-activity.sock");
2432
2593
  function notifyActivity(data) {
2433
2594
  return new Promise((resolve) => {
@@ -2458,7 +2619,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
2458
2619
  id: actId,
2459
2620
  tool: toolName,
2460
2621
  ts: actTs,
2461
- status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
2622
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
2462
2623
  label: result.blockedByLabel
2463
2624
  });
2464
2625
  }
@@ -2472,6 +2633,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2472
2633
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
2473
2634
  const creds = getCredentials();
2474
2635
  const config = getConfig(options?.cwd);
2636
+ const hashAuditArgs = config.settings.auditHashArgs === true;
2475
2637
  const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
2476
2638
  const approvers = {
2477
2639
  ...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
@@ -2482,13 +2644,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2482
2644
  approvers.terminal = false;
2483
2645
  }
2484
2646
  if (config.settings.enableHookLogDebug && !isTestEnv2) {
2485
- appendHookDebug(toolName, args, meta);
2647
+ appendHookDebug(toolName, args, meta, hashAuditArgs);
2486
2648
  }
2487
2649
  const isManual = meta?.agent === "Terminal";
2488
2650
  let explainableLabel = "Local Config";
2489
2651
  let policyMatchedField;
2490
2652
  let policyMatchedWord;
2491
2653
  let riskMetadata;
2654
+ let taintWarning = null;
2655
+ if (isNetworkTool(toolName, args)) {
2656
+ const filePaths = extractFilePaths(toolName, args);
2657
+ if (filePaths.length > 0) {
2658
+ const taintResult = await checkTaint(filePaths);
2659
+ if (taintResult.tainted && taintResult.record) {
2660
+ const { path: taintedPath, source: taintSource } = taintResult.record;
2661
+ taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
2662
+ } else if (taintResult.daemonUnavailable) {
2663
+ taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
2664
+ }
2665
+ }
2666
+ }
2492
2667
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
2493
2668
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
2494
2669
  const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
@@ -2496,7 +2671,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2496
2671
  if (dlpMatch) {
2497
2672
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
2498
2673
  if (dlpMatch.severity === "block") {
2499
- if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
2674
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
2675
+ if (isWriteTool(toolName) && filePath) {
2676
+ await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
2677
+ }
2500
2678
  return {
2501
2679
  approved: false,
2502
2680
  reason: dlpReason,
@@ -2504,7 +2682,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2504
2682
  blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
2505
2683
  };
2506
2684
  }
2507
- if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
2685
+ if (!isManual)
2686
+ appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta, hashAuditArgs);
2508
2687
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
2509
2688
  }
2510
2689
  }
@@ -2512,7 +2691,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2512
2691
  if (!isIgnoredTool(toolName)) {
2513
2692
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
2514
2693
  if (policyResult.decision === "review") {
2515
- appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
2694
+ appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
2516
2695
  if (approvers.cloud && creds?.apiKey) {
2517
2696
  await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
2518
2697
  }
@@ -2520,22 +2699,23 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2520
2699
  }
2521
2700
  return { approved: true, checkedBy: "audit" };
2522
2701
  }
2523
- if (!isIgnoredTool(toolName)) {
2702
+ if (!taintWarning && !isIgnoredTool(toolName)) {
2524
2703
  if (getActiveTrustSession(toolName)) {
2525
2704
  if (approvers.cloud && creds?.apiKey)
2526
2705
  await auditLocalAllow(toolName, args, "trust", creds, meta);
2527
- if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
2706
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
2528
2707
  return { approved: true, checkedBy: "trust" };
2529
2708
  }
2530
2709
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
2531
2710
  if (policyResult.decision === "allow") {
2532
2711
  if (approvers.cloud && creds?.apiKey)
2533
2712
  auditLocalAllow(toolName, args, "local-policy", creds, meta);
2534
- if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
2713
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
2535
2714
  return { approved: true, checkedBy: "local-policy" };
2536
2715
  }
2537
2716
  if (policyResult.decision === "block") {
2538
- if (!isManual) appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta);
2717
+ if (!isManual)
2718
+ appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
2539
2719
  return {
2540
2720
  approved: false,
2541
2721
  reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
@@ -2554,15 +2734,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2554
2734
  policyMatchedWord,
2555
2735
  policyResult.ruleName
2556
2736
  );
2557
- const persistent = getPersistentDecision(toolName);
2737
+ const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
2558
2738
  if (persistent === "allow") {
2559
2739
  if (approvers.cloud && creds?.apiKey)
2560
2740
  await auditLocalAllow(toolName, args, "persistent", creds, meta);
2561
- if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
2741
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
2562
2742
  return { approved: true, checkedBy: "persistent" };
2563
2743
  }
2564
2744
  if (persistent === "deny") {
2565
- if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
2745
+ if (!isManual)
2746
+ appendLocalAudit(toolName, args, "deny", "persistent-deny", meta, hashAuditArgs);
2566
2747
  return {
2567
2748
  approved: false,
2568
2749
  reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
@@ -2570,10 +2751,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2570
2751
  blockedByLabel: "Persistent User Rule"
2571
2752
  };
2572
2753
  }
2573
- } else {
2574
- if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
2754
+ } else if (!taintWarning) {
2755
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
2575
2756
  return { approved: true };
2576
2757
  }
2758
+ if (taintWarning) {
2759
+ explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
2760
+ riskMetadata = computeRiskMetadata(
2761
+ args,
2762
+ 7,
2763
+ explainableLabel,
2764
+ void 0,
2765
+ void 0,
2766
+ taintWarning
2767
+ );
2768
+ }
2577
2769
  let cloudRequestId = null;
2578
2770
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
2579
2771
  if (cloudEnforced) {
@@ -2592,7 +2784,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2592
2784
  };
2593
2785
  }
2594
2786
  cloudRequestId = initResult.requestId || null;
2595
- explainableLabel = "Organization Policy (SaaS)";
2787
+ if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
2596
2788
  } catch {
2597
2789
  }
2598
2790
  }
@@ -2779,7 +2971,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
2779
2971
  args,
2780
2972
  finalResult.approved ? "allow" : "deny",
2781
2973
  finalResult.checkedBy || finalResult.blockedBy || "unknown",
2782
- meta
2974
+ meta,
2975
+ hashAuditArgs
2783
2976
  );
2784
2977
  }
2785
2978
  return finalResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",