@node9/proxy 1.0.19 → 1.1.1

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,10 +680,14 @@ 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" },
685
- { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
688
+ // Slack bot tokens: xoxb- + variable segment. Real tokens are ~50–80 chars;
689
+ // lower bound 20 avoids false negatives on partial tokens, upper 100 caps scan cost.
690
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/, severity: "block" },
686
691
  { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
687
692
  { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
688
693
  {
@@ -690,8 +695,76 @@ var DLP_PATTERNS = [
690
695
  regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
691
696
  severity: "block"
692
697
  },
698
+ // GCP service account JSON (detects the type field that uniquely identifies it)
699
+ {
700
+ name: "GCP Service Account",
701
+ regex: /"type"\s*:\s*"service_account"/,
702
+ severity: "block"
703
+ },
704
+ // NPM auth token in .npmrc format
705
+ {
706
+ name: "NPM Auth Token",
707
+ regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
708
+ severity: "block"
709
+ },
693
710
  { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
694
711
  ];
712
+ var SENSITIVE_PATH_PATTERNS = [
713
+ /[/\\]\.ssh[/\\]/i,
714
+ /[/\\]\.aws[/\\]/i,
715
+ /[/\\]\.config[/\\]gcloud[/\\]/i,
716
+ /[/\\]\.azure[/\\]/i,
717
+ /[/\\]\.kube[/\\]config$/i,
718
+ /[/\\]\.env($|\.)/i,
719
+ // .env, .env.local, .env.production — not .envoy
720
+ /[/\\]\.git-credentials$/i,
721
+ /[/\\]\.npmrc$/i,
722
+ /[/\\]\.docker[/\\]config\.json$/i,
723
+ /[/\\][^/\\]+\.pem$/i,
724
+ /[/\\][^/\\]+\.key$/i,
725
+ /[/\\][^/\\]+\.p12$/i,
726
+ /[/\\][^/\\]+\.pfx$/i,
727
+ /^\/etc\/passwd$/,
728
+ /^\/etc\/shadow$/,
729
+ /^\/etc\/sudoers$/,
730
+ /[/\\]credentials\.json$/i,
731
+ /[/\\]id_rsa$/i,
732
+ /[/\\]id_ed25519$/i,
733
+ /[/\\]id_ecdsa$/i
734
+ ];
735
+ function scanFilePath(filePath, cwd = process.cwd()) {
736
+ if (!filePath) return null;
737
+ let resolved;
738
+ try {
739
+ const absolute = import_path4.default.resolve(cwd, filePath);
740
+ resolved = import_fs2.default.realpathSync.native(absolute);
741
+ } catch (err) {
742
+ const code = err.code;
743
+ if (code === "ENOENT" || code === "ENOTDIR") {
744
+ resolved = import_path4.default.resolve(cwd, filePath);
745
+ } else {
746
+ return {
747
+ patternName: "Sensitive File Path",
748
+ fieldPath: "file_path",
749
+ redactedSample: filePath,
750
+ severity: "block"
751
+ };
752
+ }
753
+ }
754
+ const normalised = resolved.replace(/\\/g, "/");
755
+ for (const pattern of SENSITIVE_PATH_PATTERNS) {
756
+ if (pattern.test(normalised)) {
757
+ return {
758
+ patternName: "Sensitive File Path",
759
+ fieldPath: "file_path",
760
+ redactedSample: filePath,
761
+ // show original path in alert, not resolved
762
+ severity: "block"
763
+ };
764
+ }
765
+ }
766
+ return null;
767
+ }
695
768
  function maskSecret(raw, pattern) {
696
769
  const match = raw.match(pattern);
697
770
  if (!match) return "****";
@@ -749,17 +822,17 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
749
822
  }
750
823
 
751
824
  // 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");
825
+ var PAUSED_FILE = import_path5.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
826
+ var TRUST_FILE = import_path5.default.join(import_os2.default.homedir(), ".node9", "trust.json");
827
+ var LOCAL_AUDIT_LOG = import_path5.default.join(import_os2.default.homedir(), ".node9", "audit.log");
828
+ var HOOK_DEBUG_LOG = import_path5.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
756
829
  function checkPause() {
757
830
  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"));
831
+ if (!import_fs3.default.existsSync(PAUSED_FILE)) return { paused: false };
832
+ const state = JSON.parse(import_fs3.default.readFileSync(PAUSED_FILE, "utf-8"));
760
833
  if (state.expiry > 0 && Date.now() >= state.expiry) {
761
834
  try {
762
- import_fs2.default.unlinkSync(PAUSED_FILE);
835
+ import_fs3.default.unlinkSync(PAUSED_FILE);
763
836
  } catch {
764
837
  }
765
838
  return { paused: false };
@@ -770,20 +843,66 @@ function checkPause() {
770
843
  }
771
844
  }
772
845
  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 });
846
+ const dir = import_path5.default.dirname(filePath);
847
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
775
848
  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);
849
+ import_fs3.default.writeFileSync(tmpPath, data, options);
850
+ import_fs3.default.renameSync(tmpPath, filePath);
851
+ }
852
+ var MAX_REGEX_LENGTH = 100;
853
+ var REGEX_CACHE_MAX = 500;
854
+ var regexCache = /* @__PURE__ */ new Map();
855
+ function validateRegex(pattern) {
856
+ if (!pattern) return "Pattern is required";
857
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
858
+ try {
859
+ new RegExp(pattern);
860
+ } catch (e) {
861
+ return `Invalid regex syntax: ${e.message}`;
862
+ }
863
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
864
+ if (!(0, import_safe_regex2.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
865
+ return null;
866
+ }
867
+ function getCompiledRegex(pattern, flags = "") {
868
+ if (flags && !/^[gimsuy]+$/.test(flags)) {
869
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Invalid regex flags: "${flags}"`);
870
+ return null;
871
+ }
872
+ const key = `${pattern}\0${flags}`;
873
+ if (regexCache.has(key)) {
874
+ const cached = regexCache.get(key);
875
+ regexCache.delete(key);
876
+ regexCache.set(key, cached);
877
+ return cached;
878
+ }
879
+ const err = validateRegex(pattern);
880
+ if (err) {
881
+ if (process.env.NODE9_DEBUG === "1")
882
+ console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
883
+ return null;
884
+ }
885
+ try {
886
+ const re = new RegExp(pattern, flags);
887
+ if (regexCache.size >= REGEX_CACHE_MAX) {
888
+ const oldest = regexCache.keys().next().value;
889
+ if (oldest) regexCache.delete(oldest);
890
+ }
891
+ regexCache.set(key, re);
892
+ return re;
893
+ } catch (e) {
894
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Regex compile failed:`, e);
895
+ return null;
896
+ }
778
897
  }
779
898
  function getActiveTrustSession(toolName) {
780
899
  try {
781
- if (!import_fs2.default.existsSync(TRUST_FILE)) return false;
782
- const trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
900
+ if (!import_fs3.default.existsSync(TRUST_FILE)) return false;
901
+ const trust = JSON.parse(import_fs3.default.readFileSync(TRUST_FILE, "utf-8"));
783
902
  const now = Date.now();
784
903
  const active = trust.entries.filter((e) => e.expiry > now);
785
904
  if (active.length !== trust.entries.length) {
786
- import_fs2.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
905
+ import_fs3.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
787
906
  }
788
907
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
789
908
  } catch {
@@ -794,8 +913,8 @@ function writeTrustSession(toolName, durationMs) {
794
913
  try {
795
914
  let trust = { entries: [] };
796
915
  try {
797
- if (import_fs2.default.existsSync(TRUST_FILE)) {
798
- trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
916
+ if (import_fs3.default.existsSync(TRUST_FILE)) {
917
+ trust = JSON.parse(import_fs3.default.readFileSync(TRUST_FILE, "utf-8"));
799
918
  }
800
919
  } catch {
801
920
  }
@@ -811,9 +930,9 @@ function writeTrustSession(toolName, durationMs) {
811
930
  }
812
931
  function appendToLog(logPath, entry) {
813
932
  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");
933
+ const dir = import_path5.default.dirname(logPath);
934
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
935
+ import_fs3.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
817
936
  } catch {
818
937
  }
819
938
  }
@@ -855,9 +974,9 @@ function matchesPattern(text, patterns) {
855
974
  const withoutDotSlash = text.replace(/^\.\//, "");
856
975
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
857
976
  }
858
- function getNestedValue(obj, path5) {
977
+ function getNestedValue(obj, path6) {
859
978
  if (!obj || typeof obj !== "object") return null;
860
- return path5.split(".").reduce((prev, curr) => prev?.[curr], obj);
979
+ return path6.split(".").reduce((prev, curr) => prev?.[curr], obj);
861
980
  }
862
981
  function evaluateSmartConditions(args, rule) {
863
982
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -876,19 +995,16 @@ function evaluateSmartConditions(args, rule) {
876
995
  return val !== null && cond.value ? !val.includes(cond.value) : true;
877
996
  case "matches": {
878
997
  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
- }
998
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
999
+ if (!reM) return false;
1000
+ return reM.test(val);
884
1001
  }
885
1002
  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
- }
1003
+ if (!cond.value) return false;
1004
+ if (val === null) return true;
1005
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
1006
+ if (!reN) return false;
1007
+ return !reN.test(val);
892
1008
  }
893
1009
  case "matchesGlob":
894
1010
  return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
@@ -1210,9 +1326,9 @@ var ADVISORY_SMART_RULES = [
1210
1326
  var cachedConfig = null;
1211
1327
  function getInternalToken() {
1212
1328
  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"));
1329
+ const pidFile = import_path5.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1330
+ if (!import_fs3.default.existsSync(pidFile)) return null;
1331
+ const data = JSON.parse(import_fs3.default.readFileSync(pidFile, "utf-8"));
1216
1332
  process.kill(data.pid, 0);
1217
1333
  return data.internalToken ?? null;
1218
1334
  } catch {
@@ -1332,10 +1448,10 @@ function isIgnoredTool(toolName) {
1332
1448
  var DAEMON_PORT = 7391;
1333
1449
  var DAEMON_HOST = "127.0.0.1";
1334
1450
  function isDaemonRunning() {
1335
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1336
- if (import_fs2.default.existsSync(pidFile)) {
1451
+ const pidFile = import_path5.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1452
+ if (import_fs3.default.existsSync(pidFile)) {
1337
1453
  try {
1338
- const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1454
+ const { pid, port } = JSON.parse(import_fs3.default.readFileSync(pidFile, "utf-8"));
1339
1455
  if (port !== DAEMON_PORT) return false;
1340
1456
  process.kill(pid, 0);
1341
1457
  return true;
@@ -1355,9 +1471,9 @@ function isDaemonRunning() {
1355
1471
  }
1356
1472
  function getPersistentDecision(toolName) {
1357
1473
  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"));
1474
+ const file = import_path5.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
1475
+ if (!import_fs3.default.existsSync(file)) return null;
1476
+ const decisions = JSON.parse(import_fs3.default.readFileSync(file, "utf-8"));
1361
1477
  const d = decisions[toolName];
1362
1478
  if (d === "allow" || d === "deny") return d;
1363
1479
  } catch {
@@ -1439,7 +1555,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
1439
1555
  signal: AbortSignal.timeout(3e3)
1440
1556
  });
1441
1557
  }
1442
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path4.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
1558
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path5.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
1443
1559
  function notifyActivity(data) {
1444
1560
  return new Promise((resolve) => {
1445
1561
  try {
@@ -1501,7 +1617,9 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1501
1617
  let policyMatchedWord;
1502
1618
  let riskMetadata;
1503
1619
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1504
- const dlpMatch = scanArgs(args);
1620
+ const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1621
+ const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
1622
+ const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
1505
1623
  if (dlpMatch) {
1506
1624
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1507
1625
  if (dlpMatch.severity === "block") {
@@ -1873,10 +1991,10 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1873
1991
  }
1874
1992
  return finalResult;
1875
1993
  }
1876
- function getConfig() {
1877
- 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");
1994
+ function getConfig(cwd) {
1995
+ if (!cwd && cachedConfig) return cachedConfig;
1996
+ const globalPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "config.json");
1997
+ const projectPath = import_path5.default.join(cwd ?? process.cwd(), "node9.config.json");
1880
1998
  const globalConfig = tryLoadConfig(globalPath);
1881
1999
  const projectConfig = tryLoadConfig(projectPath);
1882
2000
  const mergedSettings = {
@@ -1963,18 +2081,19 @@ function getConfig() {
1963
2081
  mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
1964
2082
  mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
1965
2083
  mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
1966
- cachedConfig = {
2084
+ const result = {
1967
2085
  settings: mergedSettings,
1968
2086
  policy: mergedPolicy,
1969
2087
  environments: mergedEnvironments
1970
2088
  };
1971
- return cachedConfig;
2089
+ if (!cwd) cachedConfig = result;
2090
+ return result;
1972
2091
  }
1973
2092
  function tryLoadConfig(filePath) {
1974
- if (!import_fs2.default.existsSync(filePath)) return null;
2093
+ if (!import_fs3.default.existsSync(filePath)) return null;
1975
2094
  let raw;
1976
2095
  try {
1977
- raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
2096
+ raw = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1978
2097
  } catch (err) {
1979
2098
  const msg = err instanceof Error ? err.message : String(err);
1980
2099
  process.stderr.write(
@@ -2036,9 +2155,9 @@ function getCredentials() {
2036
2155
  };
2037
2156
  }
2038
2157
  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"));
2158
+ const credPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
2159
+ if (import_fs3.default.existsSync(credPath)) {
2160
+ const creds = JSON.parse(import_fs3.default.readFileSync(credPath, "utf-8"));
2042
2161
  const profileName = process.env.NODE9_PROFILE || "default";
2043
2162
  const profile = creds[profileName];
2044
2163
  if (profile?.apiKey) {