@node9/proxy 1.0.19 → 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.js CHANGED
@@ -37,13 +37,14 @@ module.exports = __toCommonJS(src_exports);
37
37
  // src/core.ts
38
38
  var import_chalk2 = __toESM(require("chalk"));
39
39
  var import_prompts = require("@inquirer/prompts");
40
- var import_fs2 = __toESM(require("fs"));
41
- var import_path4 = __toESM(require("path"));
40
+ var import_fs3 = __toESM(require("fs"));
41
+ var import_path5 = __toESM(require("path"));
42
42
  var import_os2 = __toESM(require("os"));
43
43
  var import_net = __toESM(require("net"));
44
44
  var import_crypto = require("crypto");
45
45
  var import_child_process2 = require("child_process");
46
46
  var import_picomatch = __toESM(require("picomatch"));
47
+ var import_safe_regex2 = __toESM(require("safe-regex2"));
47
48
  var import_sh_syntax = require("sh-syntax");
48
49
 
49
50
  // src/ui/native.ts
@@ -465,8 +466,8 @@ function sanitizeConfig(raw) {
465
466
  }
466
467
  }
467
468
  const lines = result.error.issues.map((issue) => {
468
- const path5 = issue.path.length > 0 ? issue.path.join(".") : "root";
469
- return ` \u2022 ${path5}: ${issue.message}`;
469
+ const path6 = issue.path.length > 0 ? issue.path.join(".") : "root";
470
+ return ` \u2022 ${path6}: ${issue.message}`;
470
471
  });
471
472
  return {
472
473
  sanitized,
@@ -679,6 +680,8 @@ function readActiveShields() {
679
680
  }
680
681
 
681
682
  // src/dlp.ts
683
+ var import_fs2 = __toESM(require("fs"));
684
+ var import_path4 = __toESM(require("path"));
682
685
  var DLP_PATTERNS = [
683
686
  { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
684
687
  { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
@@ -690,8 +693,76 @@ var DLP_PATTERNS = [
690
693
  regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
691
694
  severity: "block"
692
695
  },
696
+ // GCP service account JSON (detects the type field that uniquely identifies it)
697
+ {
698
+ name: "GCP Service Account",
699
+ regex: /"type"\s*:\s*"service_account"/,
700
+ severity: "block"
701
+ },
702
+ // NPM auth token in .npmrc format
703
+ {
704
+ name: "NPM Auth Token",
705
+ regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
706
+ severity: "block"
707
+ },
693
708
  { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
694
709
  ];
710
+ var SENSITIVE_PATH_PATTERNS = [
711
+ /[/\\]\.ssh[/\\]/i,
712
+ /[/\\]\.aws[/\\]/i,
713
+ /[/\\]\.config[/\\]gcloud[/\\]/i,
714
+ /[/\\]\.azure[/\\]/i,
715
+ /[/\\]\.kube[/\\]config$/i,
716
+ /[/\\]\.env($|\.)/i,
717
+ // .env, .env.local, .env.production — not .envoy
718
+ /[/\\]\.git-credentials$/i,
719
+ /[/\\]\.npmrc$/i,
720
+ /[/\\]\.docker[/\\]config\.json$/i,
721
+ /[/\\][^/\\]+\.pem$/i,
722
+ /[/\\][^/\\]+\.key$/i,
723
+ /[/\\][^/\\]+\.p12$/i,
724
+ /[/\\][^/\\]+\.pfx$/i,
725
+ /^\/etc\/passwd$/,
726
+ /^\/etc\/shadow$/,
727
+ /^\/etc\/sudoers$/,
728
+ /[/\\]credentials\.json$/i,
729
+ /[/\\]id_rsa$/i,
730
+ /[/\\]id_ed25519$/i,
731
+ /[/\\]id_ecdsa$/i
732
+ ];
733
+ function scanFilePath(filePath, cwd = process.cwd()) {
734
+ if (!filePath) return null;
735
+ let resolved;
736
+ try {
737
+ const absolute = import_path4.default.resolve(cwd, filePath);
738
+ resolved = import_fs2.default.realpathSync.native(absolute);
739
+ } catch (err) {
740
+ const code = err.code;
741
+ if (code === "ENOENT" || code === "ENOTDIR") {
742
+ resolved = import_path4.default.resolve(cwd, filePath);
743
+ } else {
744
+ return {
745
+ patternName: "Sensitive File Path",
746
+ fieldPath: "file_path",
747
+ redactedSample: filePath,
748
+ severity: "block"
749
+ };
750
+ }
751
+ }
752
+ const normalised = resolved.replace(/\\/g, "/");
753
+ for (const pattern of SENSITIVE_PATH_PATTERNS) {
754
+ if (pattern.test(normalised)) {
755
+ return {
756
+ patternName: "Sensitive File Path",
757
+ fieldPath: "file_path",
758
+ redactedSample: filePath,
759
+ // show original path in alert, not resolved
760
+ severity: "block"
761
+ };
762
+ }
763
+ }
764
+ return null;
765
+ }
695
766
  function maskSecret(raw, pattern) {
696
767
  const match = raw.match(pattern);
697
768
  if (!match) return "****";
@@ -749,17 +820,17 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
749
820
  }
750
821
 
751
822
  // src/core.ts
752
- var PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
753
- var TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
754
- var LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
755
- var HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
823
+ var PAUSED_FILE = import_path5.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
824
+ var TRUST_FILE = import_path5.default.join(import_os2.default.homedir(), ".node9", "trust.json");
825
+ var LOCAL_AUDIT_LOG = import_path5.default.join(import_os2.default.homedir(), ".node9", "audit.log");
826
+ var HOOK_DEBUG_LOG = import_path5.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
756
827
  function checkPause() {
757
828
  try {
758
- if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
759
- const state = JSON.parse(import_fs2.default.readFileSync(PAUSED_FILE, "utf-8"));
829
+ if (!import_fs3.default.existsSync(PAUSED_FILE)) return { paused: false };
830
+ const state = JSON.parse(import_fs3.default.readFileSync(PAUSED_FILE, "utf-8"));
760
831
  if (state.expiry > 0 && Date.now() >= state.expiry) {
761
832
  try {
762
- import_fs2.default.unlinkSync(PAUSED_FILE);
833
+ import_fs3.default.unlinkSync(PAUSED_FILE);
763
834
  } catch {
764
835
  }
765
836
  return { paused: false };
@@ -770,20 +841,93 @@ function checkPause() {
770
841
  }
771
842
  }
772
843
  function atomicWriteSync(filePath, data, options) {
773
- const dir = import_path4.default.dirname(filePath);
774
- if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
844
+ const dir = import_path5.default.dirname(filePath);
845
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
775
846
  const tmpPath = `${filePath}.${import_os2.default.hostname()}.${process.pid}.tmp`;
776
- import_fs2.default.writeFileSync(tmpPath, data, options);
777
- import_fs2.default.renameSync(tmpPath, filePath);
847
+ import_fs3.default.writeFileSync(tmpPath, data, options);
848
+ import_fs3.default.renameSync(tmpPath, filePath);
849
+ }
850
+ var MAX_REGEX_LENGTH = 100;
851
+ var REGEX_CACHE_MAX = 500;
852
+ var regexCache = /* @__PURE__ */ new Map();
853
+ function validateRegex(pattern) {
854
+ if (!pattern) return "Pattern is required";
855
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
856
+ let parens = 0, brackets = 0, isEscaped = false, inCharClass = false;
857
+ for (let i = 0; i < pattern.length; i++) {
858
+ const char = pattern[i];
859
+ if (isEscaped) {
860
+ isEscaped = false;
861
+ continue;
862
+ }
863
+ if (char === "\\") {
864
+ isEscaped = true;
865
+ continue;
866
+ }
867
+ if (char === "[" && !inCharClass) {
868
+ inCharClass = true;
869
+ brackets++;
870
+ continue;
871
+ }
872
+ if (char === "]" && inCharClass) {
873
+ inCharClass = false;
874
+ brackets--;
875
+ continue;
876
+ }
877
+ if (inCharClass) continue;
878
+ if (char === "(") parens++;
879
+ else if (char === ")") parens--;
880
+ }
881
+ if (parens !== 0) return "Unbalanced parentheses";
882
+ if (brackets !== 0) return "Unbalanced brackets";
883
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
884
+ if (!(0, import_safe_regex2.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
885
+ try {
886
+ new RegExp(pattern);
887
+ } catch (e) {
888
+ return `Invalid regex syntax: ${e.message}`;
889
+ }
890
+ return null;
891
+ }
892
+ function getCompiledRegex(pattern, flags = "") {
893
+ if (flags && !/^[gimsuy]+$/.test(flags)) {
894
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Invalid regex flags: "${flags}"`);
895
+ return null;
896
+ }
897
+ const key = `${pattern}\0${flags}`;
898
+ if (regexCache.has(key)) {
899
+ const cached = regexCache.get(key);
900
+ regexCache.delete(key);
901
+ regexCache.set(key, cached);
902
+ return cached;
903
+ }
904
+ const err = validateRegex(pattern);
905
+ if (err) {
906
+ if (process.env.NODE9_DEBUG === "1")
907
+ console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
908
+ return null;
909
+ }
910
+ try {
911
+ const re = new RegExp(pattern, flags);
912
+ if (regexCache.size >= REGEX_CACHE_MAX) {
913
+ const oldest = regexCache.keys().next().value;
914
+ if (oldest) regexCache.delete(oldest);
915
+ }
916
+ regexCache.set(key, re);
917
+ return re;
918
+ } catch (e) {
919
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Regex compile failed:`, e);
920
+ return null;
921
+ }
778
922
  }
779
923
  function getActiveTrustSession(toolName) {
780
924
  try {
781
- if (!import_fs2.default.existsSync(TRUST_FILE)) return false;
782
- const trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
925
+ if (!import_fs3.default.existsSync(TRUST_FILE)) return false;
926
+ const trust = JSON.parse(import_fs3.default.readFileSync(TRUST_FILE, "utf-8"));
783
927
  const now = Date.now();
784
928
  const active = trust.entries.filter((e) => e.expiry > now);
785
929
  if (active.length !== trust.entries.length) {
786
- import_fs2.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
930
+ import_fs3.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
787
931
  }
788
932
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
789
933
  } catch {
@@ -794,8 +938,8 @@ function writeTrustSession(toolName, durationMs) {
794
938
  try {
795
939
  let trust = { entries: [] };
796
940
  try {
797
- if (import_fs2.default.existsSync(TRUST_FILE)) {
798
- trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
941
+ if (import_fs3.default.existsSync(TRUST_FILE)) {
942
+ trust = JSON.parse(import_fs3.default.readFileSync(TRUST_FILE, "utf-8"));
799
943
  }
800
944
  } catch {
801
945
  }
@@ -811,9 +955,9 @@ function writeTrustSession(toolName, durationMs) {
811
955
  }
812
956
  function appendToLog(logPath, entry) {
813
957
  try {
814
- const dir = import_path4.default.dirname(logPath);
815
- if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
816
- import_fs2.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
958
+ const dir = import_path5.default.dirname(logPath);
959
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
960
+ import_fs3.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
817
961
  } catch {
818
962
  }
819
963
  }
@@ -855,9 +999,9 @@ function matchesPattern(text, patterns) {
855
999
  const withoutDotSlash = text.replace(/^\.\//, "");
856
1000
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
857
1001
  }
858
- function getNestedValue(obj, path5) {
1002
+ function getNestedValue(obj, path6) {
859
1003
  if (!obj || typeof obj !== "object") return null;
860
- return path5.split(".").reduce((prev, curr) => prev?.[curr], obj);
1004
+ return path6.split(".").reduce((prev, curr) => prev?.[curr], obj);
861
1005
  }
862
1006
  function evaluateSmartConditions(args, rule) {
863
1007
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -876,19 +1020,16 @@ function evaluateSmartConditions(args, rule) {
876
1020
  return val !== null && cond.value ? !val.includes(cond.value) : true;
877
1021
  case "matches": {
878
1022
  if (val === null || !cond.value) return false;
879
- try {
880
- return new RegExp(cond.value, cond.flags ?? "").test(val);
881
- } catch {
882
- return false;
883
- }
1023
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
1024
+ if (!reM) return false;
1025
+ return reM.test(val);
884
1026
  }
885
1027
  case "notMatches": {
886
- if (val === null || !cond.value) return true;
887
- try {
888
- return !new RegExp(cond.value, cond.flags ?? "").test(val);
889
- } catch {
890
- return true;
891
- }
1028
+ if (!cond.value) return false;
1029
+ if (val === null) return true;
1030
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
1031
+ if (!reN) return false;
1032
+ return !reN.test(val);
892
1033
  }
893
1034
  case "matchesGlob":
894
1035
  return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
@@ -1210,9 +1351,9 @@ var ADVISORY_SMART_RULES = [
1210
1351
  var cachedConfig = null;
1211
1352
  function getInternalToken() {
1212
1353
  try {
1213
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1214
- if (!import_fs2.default.existsSync(pidFile)) return null;
1215
- const data = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1354
+ const pidFile = import_path5.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1355
+ if (!import_fs3.default.existsSync(pidFile)) return null;
1356
+ const data = JSON.parse(import_fs3.default.readFileSync(pidFile, "utf-8"));
1216
1357
  process.kill(data.pid, 0);
1217
1358
  return data.internalToken ?? null;
1218
1359
  } catch {
@@ -1332,10 +1473,10 @@ function isIgnoredTool(toolName) {
1332
1473
  var DAEMON_PORT = 7391;
1333
1474
  var DAEMON_HOST = "127.0.0.1";
1334
1475
  function isDaemonRunning() {
1335
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1336
- if (import_fs2.default.existsSync(pidFile)) {
1476
+ const pidFile = import_path5.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1477
+ if (import_fs3.default.existsSync(pidFile)) {
1337
1478
  try {
1338
- const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1479
+ const { pid, port } = JSON.parse(import_fs3.default.readFileSync(pidFile, "utf-8"));
1339
1480
  if (port !== DAEMON_PORT) return false;
1340
1481
  process.kill(pid, 0);
1341
1482
  return true;
@@ -1355,9 +1496,9 @@ function isDaemonRunning() {
1355
1496
  }
1356
1497
  function getPersistentDecision(toolName) {
1357
1498
  try {
1358
- const file = import_path4.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
1359
- if (!import_fs2.default.existsSync(file)) return null;
1360
- const decisions = JSON.parse(import_fs2.default.readFileSync(file, "utf-8"));
1499
+ const file = import_path5.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
1500
+ if (!import_fs3.default.existsSync(file)) return null;
1501
+ const decisions = JSON.parse(import_fs3.default.readFileSync(file, "utf-8"));
1361
1502
  const d = decisions[toolName];
1362
1503
  if (d === "allow" || d === "deny") return d;
1363
1504
  } catch {
@@ -1439,7 +1580,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
1439
1580
  signal: AbortSignal.timeout(3e3)
1440
1581
  });
1441
1582
  }
1442
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path4.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
1583
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path5.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
1443
1584
  function notifyActivity(data) {
1444
1585
  return new Promise((resolve) => {
1445
1586
  try {
@@ -1501,7 +1642,9 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1501
1642
  let policyMatchedWord;
1502
1643
  let riskMetadata;
1503
1644
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1504
- const dlpMatch = scanArgs(args);
1645
+ const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1646
+ const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
1647
+ const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
1505
1648
  if (dlpMatch) {
1506
1649
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1507
1650
  if (dlpMatch.severity === "block") {
@@ -1875,8 +2018,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1875
2018
  }
1876
2019
  function getConfig() {
1877
2020
  if (cachedConfig) return cachedConfig;
1878
- const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
1879
- const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
2021
+ const globalPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "config.json");
2022
+ const projectPath = import_path5.default.join(process.cwd(), "node9.config.json");
1880
2023
  const globalConfig = tryLoadConfig(globalPath);
1881
2024
  const projectConfig = tryLoadConfig(projectPath);
1882
2025
  const mergedSettings = {
@@ -1971,10 +2114,10 @@ function getConfig() {
1971
2114
  return cachedConfig;
1972
2115
  }
1973
2116
  function tryLoadConfig(filePath) {
1974
- if (!import_fs2.default.existsSync(filePath)) return null;
2117
+ if (!import_fs3.default.existsSync(filePath)) return null;
1975
2118
  let raw;
1976
2119
  try {
1977
- raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
2120
+ raw = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1978
2121
  } catch (err) {
1979
2122
  const msg = err instanceof Error ? err.message : String(err);
1980
2123
  process.stderr.write(
@@ -2036,9 +2179,9 @@ function getCredentials() {
2036
2179
  };
2037
2180
  }
2038
2181
  try {
2039
- const credPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
2040
- if (import_fs2.default.existsSync(credPath)) {
2041
- const creds = JSON.parse(import_fs2.default.readFileSync(credPath, "utf-8"));
2182
+ const credPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
2183
+ if (import_fs3.default.existsSync(credPath)) {
2184
+ const creds = JSON.parse(import_fs3.default.readFileSync(credPath, "utf-8"));
2042
2185
  const profileName = process.env.NODE9_PROFILE || "default";
2043
2186
  const profile = creds[profileName];
2044
2187
  if (profile?.apiKey) {