@node9/proxy 1.0.18 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  // src/core.ts
2
2
  import chalk2 from "chalk";
3
3
  import { confirm } from "@inquirer/prompts";
4
- import fs2 from "fs";
5
- import path4 from "path";
4
+ import fs3 from "fs";
5
+ import path5 from "path";
6
6
  import os2 from "os";
7
7
  import net from "net";
8
8
  import { randomUUID } from "crypto";
9
9
  import { spawnSync } from "child_process";
10
10
  import pm from "picomatch";
11
+ import safeRegex from "safe-regex2";
11
12
  import { parse } from "sh-syntax";
12
13
 
13
14
  // src/ui/native.ts
@@ -429,8 +430,8 @@ function sanitizeConfig(raw) {
429
430
  }
430
431
  }
431
432
  const lines = result.error.issues.map((issue) => {
432
- const path5 = issue.path.length > 0 ? issue.path.join(".") : "root";
433
- return ` \u2022 ${path5}: ${issue.message}`;
433
+ const path6 = issue.path.length > 0 ? issue.path.join(".") : "root";
434
+ return ` \u2022 ${path6}: ${issue.message}`;
434
435
  });
435
436
  return {
436
437
  sanitized,
@@ -643,6 +644,8 @@ function readActiveShields() {
643
644
  }
644
645
 
645
646
  // src/dlp.ts
647
+ import fs2 from "fs";
648
+ import path4 from "path";
646
649
  var DLP_PATTERNS = [
647
650
  { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
648
651
  { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
@@ -654,8 +657,76 @@ var DLP_PATTERNS = [
654
657
  regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
655
658
  severity: "block"
656
659
  },
660
+ // GCP service account JSON (detects the type field that uniquely identifies it)
661
+ {
662
+ name: "GCP Service Account",
663
+ regex: /"type"\s*:\s*"service_account"/,
664
+ severity: "block"
665
+ },
666
+ // NPM auth token in .npmrc format
667
+ {
668
+ name: "NPM Auth Token",
669
+ regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
670
+ severity: "block"
671
+ },
657
672
  { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
658
673
  ];
674
+ var SENSITIVE_PATH_PATTERNS = [
675
+ /[/\\]\.ssh[/\\]/i,
676
+ /[/\\]\.aws[/\\]/i,
677
+ /[/\\]\.config[/\\]gcloud[/\\]/i,
678
+ /[/\\]\.azure[/\\]/i,
679
+ /[/\\]\.kube[/\\]config$/i,
680
+ /[/\\]\.env($|\.)/i,
681
+ // .env, .env.local, .env.production — not .envoy
682
+ /[/\\]\.git-credentials$/i,
683
+ /[/\\]\.npmrc$/i,
684
+ /[/\\]\.docker[/\\]config\.json$/i,
685
+ /[/\\][^/\\]+\.pem$/i,
686
+ /[/\\][^/\\]+\.key$/i,
687
+ /[/\\][^/\\]+\.p12$/i,
688
+ /[/\\][^/\\]+\.pfx$/i,
689
+ /^\/etc\/passwd$/,
690
+ /^\/etc\/shadow$/,
691
+ /^\/etc\/sudoers$/,
692
+ /[/\\]credentials\.json$/i,
693
+ /[/\\]id_rsa$/i,
694
+ /[/\\]id_ed25519$/i,
695
+ /[/\\]id_ecdsa$/i
696
+ ];
697
+ function scanFilePath(filePath, cwd = process.cwd()) {
698
+ if (!filePath) return null;
699
+ let resolved;
700
+ try {
701
+ const absolute = path4.resolve(cwd, filePath);
702
+ resolved = fs2.realpathSync.native(absolute);
703
+ } catch (err) {
704
+ const code = err.code;
705
+ if (code === "ENOENT" || code === "ENOTDIR") {
706
+ resolved = path4.resolve(cwd, filePath);
707
+ } else {
708
+ return {
709
+ patternName: "Sensitive File Path",
710
+ fieldPath: "file_path",
711
+ redactedSample: filePath,
712
+ severity: "block"
713
+ };
714
+ }
715
+ }
716
+ const normalised = resolved.replace(/\\/g, "/");
717
+ for (const pattern of SENSITIVE_PATH_PATTERNS) {
718
+ if (pattern.test(normalised)) {
719
+ return {
720
+ patternName: "Sensitive File Path",
721
+ fieldPath: "file_path",
722
+ redactedSample: filePath,
723
+ // show original path in alert, not resolved
724
+ severity: "block"
725
+ };
726
+ }
727
+ }
728
+ return null;
729
+ }
659
730
  function maskSecret(raw, pattern) {
660
731
  const match = raw.match(pattern);
661
732
  if (!match) return "****";
@@ -713,17 +784,17 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
713
784
  }
714
785
 
715
786
  // src/core.ts
716
- var PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
717
- var TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
718
- var LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
719
- var HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
787
+ var PAUSED_FILE = path5.join(os2.homedir(), ".node9", "PAUSED");
788
+ var TRUST_FILE = path5.join(os2.homedir(), ".node9", "trust.json");
789
+ var LOCAL_AUDIT_LOG = path5.join(os2.homedir(), ".node9", "audit.log");
790
+ var HOOK_DEBUG_LOG = path5.join(os2.homedir(), ".node9", "hook-debug.log");
720
791
  function checkPause() {
721
792
  try {
722
- if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
723
- const state = JSON.parse(fs2.readFileSync(PAUSED_FILE, "utf-8"));
793
+ if (!fs3.existsSync(PAUSED_FILE)) return { paused: false };
794
+ const state = JSON.parse(fs3.readFileSync(PAUSED_FILE, "utf-8"));
724
795
  if (state.expiry > 0 && Date.now() >= state.expiry) {
725
796
  try {
726
- fs2.unlinkSync(PAUSED_FILE);
797
+ fs3.unlinkSync(PAUSED_FILE);
727
798
  } catch {
728
799
  }
729
800
  return { paused: false };
@@ -734,20 +805,93 @@ function checkPause() {
734
805
  }
735
806
  }
736
807
  function atomicWriteSync(filePath, data, options) {
737
- const dir = path4.dirname(filePath);
738
- if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
808
+ const dir = path5.dirname(filePath);
809
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
739
810
  const tmpPath = `${filePath}.${os2.hostname()}.${process.pid}.tmp`;
740
- fs2.writeFileSync(tmpPath, data, options);
741
- fs2.renameSync(tmpPath, filePath);
811
+ fs3.writeFileSync(tmpPath, data, options);
812
+ fs3.renameSync(tmpPath, filePath);
813
+ }
814
+ var MAX_REGEX_LENGTH = 100;
815
+ var REGEX_CACHE_MAX = 500;
816
+ var regexCache = /* @__PURE__ */ new Map();
817
+ function validateRegex(pattern) {
818
+ if (!pattern) return "Pattern is required";
819
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
820
+ let parens = 0, brackets = 0, isEscaped = false, inCharClass = false;
821
+ for (let i = 0; i < pattern.length; i++) {
822
+ const char = pattern[i];
823
+ if (isEscaped) {
824
+ isEscaped = false;
825
+ continue;
826
+ }
827
+ if (char === "\\") {
828
+ isEscaped = true;
829
+ continue;
830
+ }
831
+ if (char === "[" && !inCharClass) {
832
+ inCharClass = true;
833
+ brackets++;
834
+ continue;
835
+ }
836
+ if (char === "]" && inCharClass) {
837
+ inCharClass = false;
838
+ brackets--;
839
+ continue;
840
+ }
841
+ if (inCharClass) continue;
842
+ if (char === "(") parens++;
843
+ else if (char === ")") parens--;
844
+ }
845
+ if (parens !== 0) return "Unbalanced parentheses";
846
+ if (brackets !== 0) return "Unbalanced brackets";
847
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
848
+ if (!safeRegex(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
849
+ try {
850
+ new RegExp(pattern);
851
+ } catch (e) {
852
+ return `Invalid regex syntax: ${e.message}`;
853
+ }
854
+ return null;
855
+ }
856
+ function getCompiledRegex(pattern, flags = "") {
857
+ if (flags && !/^[gimsuy]+$/.test(flags)) {
858
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Invalid regex flags: "${flags}"`);
859
+ return null;
860
+ }
861
+ const key = `${pattern}\0${flags}`;
862
+ if (regexCache.has(key)) {
863
+ const cached = regexCache.get(key);
864
+ regexCache.delete(key);
865
+ regexCache.set(key, cached);
866
+ return cached;
867
+ }
868
+ const err = validateRegex(pattern);
869
+ if (err) {
870
+ if (process.env.NODE9_DEBUG === "1")
871
+ console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
872
+ return null;
873
+ }
874
+ try {
875
+ const re = new RegExp(pattern, flags);
876
+ if (regexCache.size >= REGEX_CACHE_MAX) {
877
+ const oldest = regexCache.keys().next().value;
878
+ if (oldest) regexCache.delete(oldest);
879
+ }
880
+ regexCache.set(key, re);
881
+ return re;
882
+ } catch (e) {
883
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Regex compile failed:`, e);
884
+ return null;
885
+ }
742
886
  }
743
887
  function getActiveTrustSession(toolName) {
744
888
  try {
745
- if (!fs2.existsSync(TRUST_FILE)) return false;
746
- const trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
889
+ if (!fs3.existsSync(TRUST_FILE)) return false;
890
+ const trust = JSON.parse(fs3.readFileSync(TRUST_FILE, "utf-8"));
747
891
  const now = Date.now();
748
892
  const active = trust.entries.filter((e) => e.expiry > now);
749
893
  if (active.length !== trust.entries.length) {
750
- fs2.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
894
+ fs3.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
751
895
  }
752
896
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
753
897
  } catch {
@@ -758,8 +902,8 @@ function writeTrustSession(toolName, durationMs) {
758
902
  try {
759
903
  let trust = { entries: [] };
760
904
  try {
761
- if (fs2.existsSync(TRUST_FILE)) {
762
- trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
905
+ if (fs3.existsSync(TRUST_FILE)) {
906
+ trust = JSON.parse(fs3.readFileSync(TRUST_FILE, "utf-8"));
763
907
  }
764
908
  } catch {
765
909
  }
@@ -775,9 +919,9 @@ function writeTrustSession(toolName, durationMs) {
775
919
  }
776
920
  function appendToLog(logPath, entry) {
777
921
  try {
778
- const dir = path4.dirname(logPath);
779
- if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
780
- fs2.appendFileSync(logPath, JSON.stringify(entry) + "\n");
922
+ const dir = path5.dirname(logPath);
923
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
924
+ fs3.appendFileSync(logPath, JSON.stringify(entry) + "\n");
781
925
  } catch {
782
926
  }
783
927
  }
@@ -819,9 +963,9 @@ function matchesPattern(text, patterns) {
819
963
  const withoutDotSlash = text.replace(/^\.\//, "");
820
964
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
821
965
  }
822
- function getNestedValue(obj, path5) {
966
+ function getNestedValue(obj, path6) {
823
967
  if (!obj || typeof obj !== "object") return null;
824
- return path5.split(".").reduce((prev, curr) => prev?.[curr], obj);
968
+ return path6.split(".").reduce((prev, curr) => prev?.[curr], obj);
825
969
  }
826
970
  function evaluateSmartConditions(args, rule) {
827
971
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -840,19 +984,16 @@ function evaluateSmartConditions(args, rule) {
840
984
  return val !== null && cond.value ? !val.includes(cond.value) : true;
841
985
  case "matches": {
842
986
  if (val === null || !cond.value) return false;
843
- try {
844
- return new RegExp(cond.value, cond.flags ?? "").test(val);
845
- } catch {
846
- return false;
847
- }
987
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
988
+ if (!reM) return false;
989
+ return reM.test(val);
848
990
  }
849
991
  case "notMatches": {
850
- if (val === null || !cond.value) return true;
851
- try {
852
- return !new RegExp(cond.value, cond.flags ?? "").test(val);
853
- } catch {
854
- return true;
855
- }
992
+ if (!cond.value) return false;
993
+ if (val === null) return true;
994
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
995
+ if (!reN) return false;
996
+ return !reN.test(val);
856
997
  }
857
998
  case "matchesGlob":
858
999
  return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
@@ -1079,7 +1220,7 @@ var DEFAULT_CONFIG = {
1079
1220
  {
1080
1221
  field: "command",
1081
1222
  op: "matches",
1082
- value: "git push.*(--force|--force-with-lease|-f\\b)",
1223
+ value: "^\\s*git\\b.*\\bpush\\b.*(--force|--force-with-lease|-f\\b)",
1083
1224
  flags: "i"
1084
1225
  }
1085
1226
  ],
@@ -1090,7 +1231,14 @@ var DEFAULT_CONFIG = {
1090
1231
  {
1091
1232
  name: "review-git-push",
1092
1233
  tool: "bash",
1093
- conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
1234
+ conditions: [
1235
+ {
1236
+ field: "command",
1237
+ op: "matches",
1238
+ value: "^\\s*git\\b.*\\bpush\\b(?!.*(-f\\b|--force|--force-with-lease))",
1239
+ flags: "i"
1240
+ }
1241
+ ],
1094
1242
  conditionMode: "all",
1095
1243
  verdict: "review",
1096
1244
  reason: "git push sends changes to a shared remote"
@@ -1102,7 +1250,7 @@ var DEFAULT_CONFIG = {
1102
1250
  {
1103
1251
  field: "command",
1104
1252
  op: "matches",
1105
- value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
1253
+ value: "^\\s*git\\b.*(reset\\s+--hard|clean\\s+-[fdxX]|\\brebase\\b|tag\\s+-d|branch\\s+-[dD])",
1106
1254
  flags: "i"
1107
1255
  }
1108
1256
  ],
@@ -1167,9 +1315,9 @@ var ADVISORY_SMART_RULES = [
1167
1315
  var cachedConfig = null;
1168
1316
  function getInternalToken() {
1169
1317
  try {
1170
- const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1171
- if (!fs2.existsSync(pidFile)) return null;
1172
- const data = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1318
+ const pidFile = path5.join(os2.homedir(), ".node9", "daemon.pid");
1319
+ if (!fs3.existsSync(pidFile)) return null;
1320
+ const data = JSON.parse(fs3.readFileSync(pidFile, "utf-8"));
1173
1321
  process.kill(data.pid, 0);
1174
1322
  return data.internalToken ?? null;
1175
1323
  } catch {
@@ -1289,10 +1437,10 @@ function isIgnoredTool(toolName) {
1289
1437
  var DAEMON_PORT = 7391;
1290
1438
  var DAEMON_HOST = "127.0.0.1";
1291
1439
  function isDaemonRunning() {
1292
- const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1293
- if (fs2.existsSync(pidFile)) {
1440
+ const pidFile = path5.join(os2.homedir(), ".node9", "daemon.pid");
1441
+ if (fs3.existsSync(pidFile)) {
1294
1442
  try {
1295
- const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1443
+ const { pid, port } = JSON.parse(fs3.readFileSync(pidFile, "utf-8"));
1296
1444
  if (port !== DAEMON_PORT) return false;
1297
1445
  process.kill(pid, 0);
1298
1446
  return true;
@@ -1312,9 +1460,9 @@ function isDaemonRunning() {
1312
1460
  }
1313
1461
  function getPersistentDecision(toolName) {
1314
1462
  try {
1315
- const file = path4.join(os2.homedir(), ".node9", "decisions.json");
1316
- if (!fs2.existsSync(file)) return null;
1317
- const decisions = JSON.parse(fs2.readFileSync(file, "utf-8"));
1463
+ const file = path5.join(os2.homedir(), ".node9", "decisions.json");
1464
+ if (!fs3.existsSync(file)) return null;
1465
+ const decisions = JSON.parse(fs3.readFileSync(file, "utf-8"));
1318
1466
  const d = decisions[toolName];
1319
1467
  if (d === "allow" || d === "deny") return d;
1320
1468
  } catch {
@@ -1396,7 +1544,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
1396
1544
  signal: AbortSignal.timeout(3e3)
1397
1545
  });
1398
1546
  }
1399
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path4.join(os2.tmpdir(), "node9-activity.sock");
1547
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path5.join(os2.tmpdir(), "node9-activity.sock");
1400
1548
  function notifyActivity(data) {
1401
1549
  return new Promise((resolve) => {
1402
1550
  try {
@@ -1458,7 +1606,9 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1458
1606
  let policyMatchedWord;
1459
1607
  let riskMetadata;
1460
1608
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1461
- const dlpMatch = scanArgs(args);
1609
+ const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1610
+ const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
1611
+ const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
1462
1612
  if (dlpMatch) {
1463
1613
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1464
1614
  if (dlpMatch.severity === "block") {
@@ -1832,8 +1982,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1832
1982
  }
1833
1983
  function getConfig() {
1834
1984
  if (cachedConfig) return cachedConfig;
1835
- const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
1836
- const projectPath = path4.join(process.cwd(), "node9.config.json");
1985
+ const globalPath = path5.join(os2.homedir(), ".node9", "config.json");
1986
+ const projectPath = path5.join(process.cwd(), "node9.config.json");
1837
1987
  const globalConfig = tryLoadConfig(globalPath);
1838
1988
  const projectConfig = tryLoadConfig(projectPath);
1839
1989
  const mergedSettings = {
@@ -1928,10 +2078,10 @@ function getConfig() {
1928
2078
  return cachedConfig;
1929
2079
  }
1930
2080
  function tryLoadConfig(filePath) {
1931
- if (!fs2.existsSync(filePath)) return null;
2081
+ if (!fs3.existsSync(filePath)) return null;
1932
2082
  let raw;
1933
2083
  try {
1934
- raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
2084
+ raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
1935
2085
  } catch (err) {
1936
2086
  const msg = err instanceof Error ? err.message : String(err);
1937
2087
  process.stderr.write(
@@ -1993,9 +2143,9 @@ function getCredentials() {
1993
2143
  };
1994
2144
  }
1995
2145
  try {
1996
- const credPath = path4.join(os2.homedir(), ".node9", "credentials.json");
1997
- if (fs2.existsSync(credPath)) {
1998
- const creds = JSON.parse(fs2.readFileSync(credPath, "utf-8"));
2146
+ const credPath = path5.join(os2.homedir(), ".node9", "credentials.json");
2147
+ if (fs3.existsSync(credPath)) {
2148
+ const creds = JSON.parse(fs3.readFileSync(credPath, "utf-8"));
1999
2149
  const profileName = process.env.NODE9_PROFILE || "default";
2000
2150
  const profile = creds[profileName];
2001
2151
  if (profile?.apiKey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.0.18",
3
+ "version": "1.1.0",
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",
@@ -71,6 +71,7 @@
71
71
  "commander": "^14.0.3",
72
72
  "execa": "^9.6.1",
73
73
  "picomatch": "^4.0.3",
74
+ "safe-regex2": "^5.1.0",
74
75
  "sh-syntax": "^0.5.8",
75
76
  "zod": "^3.25.76"
76
77
  },