@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.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;
@@ -1115,7 +1256,7 @@ var DEFAULT_CONFIG = {
1115
1256
  {
1116
1257
  field: "command",
1117
1258
  op: "matches",
1118
- value: "git push.*(--force|--force-with-lease|-f\\b)",
1259
+ value: "^\\s*git\\b.*\\bpush\\b.*(--force|--force-with-lease|-f\\b)",
1119
1260
  flags: "i"
1120
1261
  }
1121
1262
  ],
@@ -1126,7 +1267,14 @@ var DEFAULT_CONFIG = {
1126
1267
  {
1127
1268
  name: "review-git-push",
1128
1269
  tool: "bash",
1129
- conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
1270
+ conditions: [
1271
+ {
1272
+ field: "command",
1273
+ op: "matches",
1274
+ value: "^\\s*git\\b.*\\bpush\\b(?!.*(-f\\b|--force|--force-with-lease))",
1275
+ flags: "i"
1276
+ }
1277
+ ],
1130
1278
  conditionMode: "all",
1131
1279
  verdict: "review",
1132
1280
  reason: "git push sends changes to a shared remote"
@@ -1138,7 +1286,7 @@ var DEFAULT_CONFIG = {
1138
1286
  {
1139
1287
  field: "command",
1140
1288
  op: "matches",
1141
- value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
1289
+ value: "^\\s*git\\b.*(reset\\s+--hard|clean\\s+-[fdxX]|\\brebase\\b|tag\\s+-d|branch\\s+-[dD])",
1142
1290
  flags: "i"
1143
1291
  }
1144
1292
  ],
@@ -1203,9 +1351,9 @@ var ADVISORY_SMART_RULES = [
1203
1351
  var cachedConfig = null;
1204
1352
  function getInternalToken() {
1205
1353
  try {
1206
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1207
- if (!import_fs2.default.existsSync(pidFile)) return null;
1208
- 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"));
1209
1357
  process.kill(data.pid, 0);
1210
1358
  return data.internalToken ?? null;
1211
1359
  } catch {
@@ -1325,10 +1473,10 @@ function isIgnoredTool(toolName) {
1325
1473
  var DAEMON_PORT = 7391;
1326
1474
  var DAEMON_HOST = "127.0.0.1";
1327
1475
  function isDaemonRunning() {
1328
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1329
- 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)) {
1330
1478
  try {
1331
- 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"));
1332
1480
  if (port !== DAEMON_PORT) return false;
1333
1481
  process.kill(pid, 0);
1334
1482
  return true;
@@ -1348,9 +1496,9 @@ function isDaemonRunning() {
1348
1496
  }
1349
1497
  function getPersistentDecision(toolName) {
1350
1498
  try {
1351
- const file = import_path4.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
1352
- if (!import_fs2.default.existsSync(file)) return null;
1353
- 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"));
1354
1502
  const d = decisions[toolName];
1355
1503
  if (d === "allow" || d === "deny") return d;
1356
1504
  } catch {
@@ -1432,7 +1580,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
1432
1580
  signal: AbortSignal.timeout(3e3)
1433
1581
  });
1434
1582
  }
1435
- 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");
1436
1584
  function notifyActivity(data) {
1437
1585
  return new Promise((resolve) => {
1438
1586
  try {
@@ -1494,7 +1642,9 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1494
1642
  let policyMatchedWord;
1495
1643
  let riskMetadata;
1496
1644
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1497
- 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);
1498
1648
  if (dlpMatch) {
1499
1649
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1500
1650
  if (dlpMatch.severity === "block") {
@@ -1868,8 +2018,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1868
2018
  }
1869
2019
  function getConfig() {
1870
2020
  if (cachedConfig) return cachedConfig;
1871
- const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
1872
- 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");
1873
2023
  const globalConfig = tryLoadConfig(globalPath);
1874
2024
  const projectConfig = tryLoadConfig(projectPath);
1875
2025
  const mergedSettings = {
@@ -1964,10 +2114,10 @@ function getConfig() {
1964
2114
  return cachedConfig;
1965
2115
  }
1966
2116
  function tryLoadConfig(filePath) {
1967
- if (!import_fs2.default.existsSync(filePath)) return null;
2117
+ if (!import_fs3.default.existsSync(filePath)) return null;
1968
2118
  let raw;
1969
2119
  try {
1970
- raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
2120
+ raw = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1971
2121
  } catch (err) {
1972
2122
  const msg = err instanceof Error ? err.message : String(err);
1973
2123
  process.stderr.write(
@@ -2029,9 +2179,9 @@ function getCredentials() {
2029
2179
  };
2030
2180
  }
2031
2181
  try {
2032
- const credPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
2033
- if (import_fs2.default.existsSync(credPath)) {
2034
- 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"));
2035
2185
  const profileName = process.env.NODE9_PROFILE || "default";
2036
2186
  const profile = creds[profileName];
2037
2187
  if (profile?.apiKey) {