@node9/proxy 1.11.2 → 1.11.4

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
@@ -96,7 +96,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
96
96
  }
97
97
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
98
98
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
99
- const testRun = isTestCall(toolName, args) ? { testRun: true } : {};
99
+ const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
100
100
  appendToLog(LOCAL_AUDIT_LOG, {
101
101
  ts: (/* @__PURE__ */ new Date()).toISOString(),
102
102
  tool: toolName,
@@ -417,7 +417,7 @@ var DEFAULT_CONFIG = {
417
417
  // 120-second auto-deny timeout
418
418
  flightRecorder: true,
419
419
  auditHashArgs: true,
420
- approvers: { native: true, browser: true, cloud: false, terminal: true },
420
+ approvers: { native: true, browser: false, cloud: false, terminal: true },
421
421
  cloudSyncIntervalHours: 5
422
422
  },
423
423
  policy: {
@@ -524,7 +524,7 @@ var DEFAULT_CONFIG = {
524
524
  },
525
525
  // ── Git safety ────────────────────────────────────────────────────────
526
526
  {
527
- name: "block-force-push",
527
+ name: "review-force-push",
528
528
  tool: "bash",
529
529
  conditions: [
530
530
  {
@@ -537,8 +537,8 @@ var DEFAULT_CONFIG = {
537
537
  }
538
538
  ],
539
539
  conditionMode: "all",
540
- verdict: "block",
541
- reason: "Force push overwrites remote history and cannot be undone",
540
+ verdict: "review",
541
+ reason: "Force push rewrites remote history \u2014 confirm this is intentional",
542
542
  description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
543
543
  },
544
544
  {
@@ -548,14 +548,16 @@ var DEFAULT_CONFIG = {
548
548
  {
549
549
  field: "command",
550
550
  op: "matches",
551
- value: "\\bgit\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
551
+ // Anchor git as a shell command so node -e / python -c scripts containing
552
+ // "git reset --hard" as a string don't false-positive.
553
+ value: "(^|&&|\\|\\||;)\\s*git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
552
554
  flags: "i"
553
555
  },
554
556
  {
555
557
  field: "command",
556
558
  op: "notMatches",
557
- // Exclude recovery ops these resolve a conflict, not start a destructive action.
558
- value: "\\bgit\\s+rebase\\s+--(abort|continue|skip)\\b",
559
+ // Exclude recovery ops and routine branch-surgery (--onto) these are not destructive.
560
+ value: "\\bgit\\s+rebase\\s+--(abort|continue|skip|onto)\\b",
559
561
  flags: "i"
560
562
  }
561
563
  ],
@@ -954,37 +956,314 @@ function getCompiledRegex(pattern, flags = "") {
954
956
  // src/policy/index.ts
955
957
  import path8 from "path";
956
958
  import pm from "picomatch";
957
- import { parse } from "sh-syntax";
959
+ import mvdanSh from "mvdan-sh";
958
960
 
959
961
  // src/dlp.ts
960
962
  import fs4 from "fs";
961
963
  import path4 from "path";
964
+ var DLP_STOPWORDS = [
965
+ "example",
966
+ "placeholder",
967
+ "changeme",
968
+ "your_key",
969
+ "your_token",
970
+ "your_secret",
971
+ "replace_me",
972
+ "insert_key",
973
+ "put_your",
974
+ "fake",
975
+ "dummy",
976
+ "sample",
977
+ "xxxxxxxx",
978
+ "aaaaaa",
979
+ "bbbbbb",
980
+ "00000000",
981
+ "${",
982
+ "{{",
983
+ "%{",
984
+ "<your",
985
+ "test_key",
986
+ "test_token"
987
+ ];
962
988
  var DLP_PATTERNS = [
963
- { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
964
- { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
965
- // Slack bot tokens: xoxb- + variable segment. Real tokens are ~50–80 chars;
966
- // lower bound 20 avoids false negatives on partial tokens, upper 100 caps scan cost.
967
- { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/, severity: "block" },
968
- { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
969
- { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
989
+ // ── AWS ───────────────────────────────────────────────────────────────────
970
990
  {
971
- name: "Private Key (PEM)",
972
- regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
973
- severity: "block"
991
+ name: "AWS Access Key ID",
992
+ regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/,
993
+ severity: "block",
994
+ keywords: ["akia", "asia", "abia", "acca", "a3t"]
995
+ },
996
+ // ── GitHub ────────────────────────────────────────────────────────────────
997
+ {
998
+ name: "GitHub Token",
999
+ regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/,
1000
+ severity: "block",
1001
+ keywords: ["ghp_", "gho_", "ghu_", "ghs_"]
1002
+ },
1003
+ {
1004
+ name: "GitHub Fine-Grained PAT",
1005
+ regex: /\bgithub_pat_\w{82}\b/,
1006
+ severity: "block",
1007
+ keywords: ["github_pat_"]
1008
+ },
1009
+ // ── Slack ─────────────────────────────────────────────────────────────────
1010
+ {
1011
+ name: "Slack Bot Token",
1012
+ // Real tokens are ~50–80 chars; lower bound 20 avoids false negatives on partial tokens
1013
+ regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/,
1014
+ severity: "block",
1015
+ keywords: ["xoxb-"]
1016
+ },
1017
+ // ── Anthropic ─────────────────────────────────────────────────────────────
1018
+ // Listed before OpenAI — Anthropic keys start with sk-ant- which would also
1019
+ // match the broader OpenAI sk- pattern; more specific rules must come first.
1020
+ {
1021
+ name: "Anthropic API Key",
1022
+ regex: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA\b/,
1023
+ severity: "block",
1024
+ keywords: ["sk-ant-api03"]
1025
+ },
1026
+ {
1027
+ name: "Anthropic Admin Key",
1028
+ regex: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA\b/,
1029
+ severity: "block",
1030
+ keywords: ["sk-ant-admin01"]
1031
+ },
1032
+ // ── OpenAI ────────────────────────────────────────────────────────────────
1033
+ {
1034
+ name: "OpenAI API Key",
1035
+ regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/,
1036
+ severity: "block",
1037
+ keywords: ["sk-"]
1038
+ },
1039
+ // ── Stripe ────────────────────────────────────────────────────────────────
1040
+ {
1041
+ name: "Stripe Secret Key",
1042
+ regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/,
1043
+ severity: "block",
1044
+ keywords: ["sk_live_", "sk_test_"]
1045
+ },
1046
+ // ── GCP ───────────────────────────────────────────────────────────────────
1047
+ {
1048
+ name: "GCP API Key",
1049
+ regex: /\bAIza[0-9A-Za-z_-]{35}\b/,
1050
+ severity: "block",
1051
+ keywords: ["aiza"]
974
1052
  },
975
- // GCP service account JSON (detects the type field that uniquely identifies it)
976
1053
  {
977
1054
  name: "GCP Service Account",
978
1055
  regex: /"type"\s*:\s*"service_account"/,
979
- severity: "block"
1056
+ severity: "block",
1057
+ keywords: ["service_account"]
1058
+ },
1059
+ // ── Azure ─────────────────────────────────────────────────────────────────
1060
+ // Pattern: 3 alphanum chars + digit + Q~ + 31-34 alphanum chars
1061
+ {
1062
+ name: "Azure AD Client Secret",
1063
+ regex: /(?:^|[\s>=:(,])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\s<),])/,
1064
+ severity: "block",
1065
+ keywords: ["q~"]
1066
+ },
1067
+ // ── Databricks ────────────────────────────────────────────────────────────
1068
+ {
1069
+ name: "Databricks API Token",
1070
+ regex: /\bdapi[a-f0-9]{32}(?:-\d)?\b/,
1071
+ severity: "block",
1072
+ keywords: ["dapi"]
1073
+ },
1074
+ // ── DigitalOcean ──────────────────────────────────────────────────────────
1075
+ {
1076
+ name: "DigitalOcean PAT",
1077
+ regex: /\bdop_v1_[a-f0-9]{64}\b/,
1078
+ severity: "block",
1079
+ keywords: ["dop_v1_"]
1080
+ },
1081
+ {
1082
+ name: "DigitalOcean Access Token",
1083
+ regex: /\bdoo_v1_[a-f0-9]{64}\b/,
1084
+ severity: "block",
1085
+ keywords: ["doo_v1_"]
1086
+ },
1087
+ // ── Doppler ───────────────────────────────────────────────────────────────
1088
+ {
1089
+ name: "Doppler Token",
1090
+ regex: /\bdp\.pt\.[a-z0-9]{43}\b/i,
1091
+ severity: "block",
1092
+ keywords: ["dp.pt."]
1093
+ },
1094
+ // ── HashiCorp Vault ───────────────────────────────────────────────────────
1095
+ {
1096
+ name: "HashiCorp Vault Service Token",
1097
+ regex: /\bhvs\.[\w-]{90,120}\b/,
1098
+ severity: "block",
1099
+ keywords: ["hvs."]
980
1100
  },
981
- // NPM auth token in .npmrc format
1101
+ {
1102
+ name: "HashiCorp Vault Batch Token",
1103
+ regex: /\bhvb\.[\w-]{138,300}\b/,
1104
+ severity: "block",
1105
+ keywords: ["hvb."]
1106
+ },
1107
+ // ── Hugging Face ──────────────────────────────────────────────────────────
1108
+ { name: "HuggingFace Token", regex: /\bhf_[A-Za-z]{34}\b/, severity: "block", keywords: ["hf_"] },
1109
+ // ── Postman ───────────────────────────────────────────────────────────────
1110
+ {
1111
+ name: "Postman API Token",
1112
+ regex: /\bPMAK-[a-f0-9]{24}-[a-f0-9]{34}\b/i,
1113
+ severity: "block",
1114
+ keywords: ["pmak-"]
1115
+ },
1116
+ // ── Pulumi ────────────────────────────────────────────────────────────────
1117
+ {
1118
+ name: "Pulumi Access Token",
1119
+ regex: /\bpul-[a-f0-9]{40}\b/,
1120
+ severity: "block",
1121
+ keywords: ["pul-"]
1122
+ },
1123
+ // ── SendGrid ──────────────────────────────────────────────────────────────
1124
+ {
1125
+ name: "SendGrid API Key",
1126
+ regex: /\bSG\.[a-zA-Z0-9=_.-]{66}\b/,
1127
+ severity: "block",
1128
+ keywords: ["sg."]
1129
+ },
1130
+ // ── Private keys (PEM) ────────────────────────────────────────────────────
1131
+ {
1132
+ name: "Private Key (PEM)",
1133
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
1134
+ severity: "block",
1135
+ keywords: ["-----begin"]
1136
+ },
1137
+ // ── NPM ───────────────────────────────────────────────────────────────────
982
1138
  {
983
1139
  name: "NPM Auth Token",
984
- regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
985
- severity: "block"
1140
+ regex: /_authToken\s*=\s*[A-Za-z0-9_-]{20,}/,
1141
+ severity: "block",
1142
+ keywords: ["_authtoken"]
1143
+ },
1144
+ // ── JWT ───────────────────────────────────────────────────────────────────
1145
+ // review (not block): JWTs appear legitimately in API calls; flag for human approval
1146
+ {
1147
+ name: "JWT",
1148
+ regex: /\bey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/_-]{17,}\.[a-zA-Z0-9\/_-]{10,}={0,2}\b/,
1149
+ severity: "review",
1150
+ keywords: ["eyj"]
1151
+ },
1152
+ // ── Stripe (extended — adds restricted key rk_ prefix) ──────────────────
1153
+ {
1154
+ name: "Stripe Restricted Key",
1155
+ regex: /\brk_(?:live|test|prod)_[0-9a-zA-Z]{10,99}\b/,
1156
+ severity: "block",
1157
+ keywords: ["rk_live_", "rk_test_", "rk_prod_"]
1158
+ },
1159
+ // ── Slack (app token) ─────────────────────────────────────────────────────
1160
+ {
1161
+ name: "Slack App Token",
1162
+ regex: /\bxapp-\d-[A-Z0-9]+-\d+-[a-f0-9]+\b/,
1163
+ severity: "block",
1164
+ keywords: ["xapp-"]
1165
+ },
1166
+ // ── GitLab ────────────────────────────────────────────────────────────────
1167
+ { name: "GitLab PAT", regex: /\bglpat-[\w-]{20}\b/, severity: "block", keywords: ["glpat-"] },
1168
+ {
1169
+ name: "GitLab Deploy Token",
1170
+ regex: /\bgldt-[0-9a-zA-Z_-]{20}\b/,
1171
+ severity: "block",
1172
+ keywords: ["gldt-"]
1173
+ },
1174
+ {
1175
+ name: "GitLab CI Job Token",
1176
+ regex: /\bglcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}\b/,
1177
+ severity: "block",
1178
+ keywords: ["glcbt-"]
1179
+ },
1180
+ // ── npm (publish token) ───────────────────────────────────────────────────
1181
+ {
1182
+ name: "npm Access Token",
1183
+ regex: /\bnpm_[a-zA-Z0-9]{36}\b/,
1184
+ severity: "block",
1185
+ keywords: ["npm_"]
1186
+ },
1187
+ // ── Shopify ───────────────────────────────────────────────────────────────
1188
+ {
1189
+ name: "Shopify Access Token",
1190
+ regex: /\bshpat_[a-fA-F0-9]{32}\b/,
1191
+ severity: "block",
1192
+ keywords: ["shpat_"]
1193
+ },
1194
+ {
1195
+ name: "Shopify Custom Access Token",
1196
+ regex: /\bshpca_[a-fA-F0-9]{32}\b/,
1197
+ severity: "block",
1198
+ keywords: ["shpca_"]
1199
+ },
1200
+ {
1201
+ name: "Shopify Private App Token",
1202
+ regex: /\bshppa_[a-fA-F0-9]{32}\b/,
1203
+ severity: "block",
1204
+ keywords: ["shppa_"]
1205
+ },
1206
+ {
1207
+ name: "Shopify Shared Secret",
1208
+ regex: /\bshpss_[a-fA-F0-9]{32}\b/,
1209
+ severity: "block",
1210
+ keywords: ["shpss_"]
1211
+ },
1212
+ // ── Linear ────────────────────────────────────────────────────────────────
1213
+ {
1214
+ name: "Linear API Key",
1215
+ regex: /\blin_api_[a-zA-Z0-9]{40}\b/,
1216
+ severity: "block",
1217
+ keywords: ["lin_api_"]
986
1218
  },
987
- { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i, severity: "review" }
1219
+ // ── PlanetScale ───────────────────────────────────────────────────────────
1220
+ {
1221
+ name: "PlanetScale API Token",
1222
+ regex: /\bpscale_tkn_[\w.-]{32,64}\b/,
1223
+ severity: "block",
1224
+ keywords: ["pscale_tkn_"]
1225
+ },
1226
+ {
1227
+ name: "PlanetScale Password",
1228
+ regex: /\bpscale_pw_[\w.-]{32,64}\b/,
1229
+ severity: "block",
1230
+ keywords: ["pscale_pw_"]
1231
+ },
1232
+ // ── Sentry ────────────────────────────────────────────────────────────────
1233
+ {
1234
+ name: "Sentry User Token",
1235
+ regex: /\bsntryu_[a-f0-9]{64}\b/,
1236
+ severity: "block",
1237
+ keywords: ["sntryu_"]
1238
+ },
1239
+ // ── Grafana ───────────────────────────────────────────────────────────────
1240
+ {
1241
+ name: "Grafana Service Account Token",
1242
+ regex: /\bglsa_[a-zA-Z0-9]{32}_[a-f0-9]{8}\b/,
1243
+ severity: "block",
1244
+ keywords: ["glsa_"]
1245
+ },
1246
+ // ── Heroku ────────────────────────────────────────────────────────────────
1247
+ {
1248
+ name: "Heroku API Key",
1249
+ regex: /\bHRKU-AA[0-9a-zA-Z_-]{58}\b/,
1250
+ severity: "block",
1251
+ keywords: ["hrku-aa"]
1252
+ },
1253
+ // ── PyPI ──────────────────────────────────────────────────────────────────
1254
+ {
1255
+ name: "PyPI Upload Token",
1256
+ regex: /\bpypi-[A-Za-z0-9_-]{50,}\b/,
1257
+ severity: "block",
1258
+ keywords: ["pypi-"]
1259
+ },
1260
+ // ── Bearer Token ─────────────────────────────────────────────────────────
1261
+ {
1262
+ name: "Bearer Token",
1263
+ regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i,
1264
+ severity: "review",
1265
+ keywords: ["bearer"]
1266
+ }
988
1267
  ];
989
1268
  var SENSITIVE_PATH_PATTERNS = [
990
1269
  /[/\\]\.ssh[/\\]/i,
@@ -1073,8 +1352,14 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
1073
1352
  }
1074
1353
  if (typeof args === "string") {
1075
1354
  const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
1355
+ const textLower = text.toLowerCase();
1076
1356
  for (const pattern of DLP_PATTERNS) {
1357
+ if (pattern.keywords && !pattern.keywords.some((kw) => textLower.includes(kw.toLowerCase()))) {
1358
+ continue;
1359
+ }
1077
1360
  if (pattern.regex.test(text)) {
1361
+ const matchedValue = (text.match(pattern.regex)?.[0] ?? "").toLowerCase();
1362
+ if (DLP_STOPWORDS.some((sw) => matchedValue.includes(sw))) continue;
1078
1363
  return {
1079
1364
  patternName: pattern.name,
1080
1365
  fieldPath,
@@ -1574,17 +1859,111 @@ function getNestedValue(obj, path15) {
1574
1859
  if (!obj || typeof obj !== "object") return null;
1575
1860
  return path15.split(".").reduce((prev, curr) => prev?.[curr], obj);
1576
1861
  }
1577
- function stripStringArguments(cmd) {
1578
- let result = cmd;
1579
- result = result.replace(
1580
- /\b(node|python3?|ruby|perl|php|deno)\s+(-[ecr]|eval)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/gi,
1581
- '$1 $2 ""'
1582
- );
1583
- result = result.replace(
1584
- /\s(-m|--message|--body|--title|--description)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
1585
- ' $1 ""'
1586
- );
1587
- return result;
1862
+ var { syntax } = mvdanSh;
1863
+ var sharedParser = syntax.NewParser();
1864
+ var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
1865
+ "-m",
1866
+ "--message",
1867
+ "--body",
1868
+ "--title",
1869
+ "--description",
1870
+ "--comment",
1871
+ "--subject",
1872
+ "--summary"
1873
+ ]);
1874
+ function normalizeCommandForPolicy(command) {
1875
+ try {
1876
+ const f = sharedParser.Parse(command, "cmd");
1877
+ const strips = [];
1878
+ syntax.Walk(f, (node) => {
1879
+ if (!node) return false;
1880
+ const n = node;
1881
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1882
+ const args = n.Args || [];
1883
+ for (let i = 0; i < args.length - 1; i++) {
1884
+ const argParts = args[i].Parts || [];
1885
+ if (argParts.length !== 1 || syntax.NodeType(argParts[0]) !== "Lit") continue;
1886
+ const flagVal = argParts[0].Value || "";
1887
+ if (!MESSAGE_FLAGS.has(flagVal.toLowerCase())) continue;
1888
+ const next = args[i + 1];
1889
+ const nextParts = next.Parts || [];
1890
+ if (nextParts.length !== 1) continue;
1891
+ const quotedNode = nextParts[0];
1892
+ const nt = syntax.NodeType(quotedNode);
1893
+ if (nt === "SglQuoted") {
1894
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
1895
+ } else if (nt === "DblQuoted") {
1896
+ const innerParts = quotedNode.Parts || [];
1897
+ const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
1898
+ if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
1899
+ }
1900
+ }
1901
+ return true;
1902
+ });
1903
+ if (strips.length === 0) return command;
1904
+ strips.sort((a, b) => b[0] - a[0]);
1905
+ let result = command;
1906
+ for (const [start, end] of strips) {
1907
+ result = result.slice(0, start) + '""' + result.slice(end);
1908
+ }
1909
+ return result;
1910
+ } catch {
1911
+ return command;
1912
+ }
1913
+ }
1914
+ var SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
1915
+ var DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
1916
+ function scanArgsForDynamicExec(args, startIdx) {
1917
+ let hasCmdSubst = false;
1918
+ let hasParamExp = false;
1919
+ let hasCurl = false;
1920
+ for (let i = startIdx; i < args.length; i++) {
1921
+ syntax.Walk(args[i], (inner) => {
1922
+ if (!inner) return false;
1923
+ const inn = inner;
1924
+ const it = syntax.NodeType(inn);
1925
+ if (it === "CmdSubst") hasCmdSubst = true;
1926
+ if (it === "ParamExp") hasParamExp = true;
1927
+ if (it === "Lit" && DOWNLOAD_CMDS.has(inn.Value?.toLowerCase())) hasCurl = true;
1928
+ return true;
1929
+ });
1930
+ }
1931
+ if (hasCmdSubst && hasCurl) return "block";
1932
+ if (hasCmdSubst || hasParamExp) return "review";
1933
+ return null;
1934
+ }
1935
+ function detectDangerousShellExec(command) {
1936
+ try {
1937
+ const f = sharedParser.Parse(command, "cmd");
1938
+ let result = null;
1939
+ syntax.Walk(f, (node) => {
1940
+ if (!node || result === "block") return false;
1941
+ const n = node;
1942
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1943
+ const args = n.Args || [];
1944
+ if (args.length === 0) return true;
1945
+ const firstParts = args[0].Parts || [];
1946
+ if (firstParts.length !== 1 || syntax.NodeType(firstParts[0]) !== "Lit") return true;
1947
+ const cmdName = firstParts[0].Value?.toLowerCase() ?? "";
1948
+ if (cmdName === "eval") {
1949
+ const v = scanArgsForDynamicExec(args, 1);
1950
+ if (v === "block" || v === "review" && result === null) result = v;
1951
+ } else if (SHELL_INTERPRETERS.has(cmdName)) {
1952
+ for (let i = 1; i < args.length - 1; i++) {
1953
+ const flagParts = args[i].Parts || [];
1954
+ if (flagParts.length !== 1 || syntax.NodeType(flagParts[0]) !== "Lit" || flagParts[0].Value !== "-c")
1955
+ continue;
1956
+ const v = scanArgsForDynamicExec(args, i + 1);
1957
+ if (v === "block" || v === "review" && result === null) result = v;
1958
+ break;
1959
+ }
1960
+ }
1961
+ return true;
1962
+ });
1963
+ return result;
1964
+ } catch {
1965
+ return null;
1966
+ }
1588
1967
  }
1589
1968
  function evaluateSmartConditions(args, rule) {
1590
1969
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -1592,7 +1971,7 @@ function evaluateSmartConditions(args, rule) {
1592
1971
  const results = rule.conditions.map((cond) => {
1593
1972
  const rawVal = getNestedValue(args, cond.field);
1594
1973
  const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1595
- const val = cond.field === "command" && normalized !== null ? stripStringArguments(normalized) : normalized;
1974
+ const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
1596
1975
  switch (cond.op) {
1597
1976
  case "exists":
1598
1977
  return val !== null && val !== "";
@@ -1641,52 +2020,35 @@ function isSqlTool(toolName, toolInspection) {
1641
2020
  return fieldName === "sql" || fieldName === "query";
1642
2021
  }
1643
2022
  var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
1644
- async function analyzeShellCommand(command) {
2023
+ function analyzeShellCommand(command) {
1645
2024
  const actions = [];
1646
2025
  const paths = [];
1647
2026
  const allTokens = [];
1648
2027
  const addToken = (token) => {
1649
2028
  const lower = token.toLowerCase();
1650
2029
  allTokens.push(lower);
1651
- if (lower.includes("/")) {
1652
- const segments = lower.split("/").filter(Boolean);
1653
- allTokens.push(...segments);
1654
- }
1655
- if (lower.startsWith("-")) {
1656
- allTokens.push(lower.replace(/^-+/, ""));
1657
- }
2030
+ if (lower.includes("/")) allTokens.push(...lower.split("/").filter(Boolean));
2031
+ if (lower.startsWith("-")) allTokens.push(lower.replace(/^-+/, ""));
1658
2032
  };
1659
2033
  try {
1660
- const ast = await parse(command);
1661
- const walk = (node) => {
1662
- if (!node) return;
1663
- if (node.type === "CallExpr") {
1664
- const parts = (node.Args || []).map((arg) => {
1665
- return (arg.Parts || []).map((p) => p.Value || "").join("");
1666
- }).filter((s) => s.length > 0);
1667
- if (parts.length > 0) {
1668
- actions.push(parts[0].toLowerCase());
1669
- parts.forEach((p) => addToken(p));
1670
- parts.slice(1).forEach((p) => {
1671
- if (!p.startsWith("-")) paths.push(p);
1672
- });
1673
- }
1674
- }
1675
- for (const key in node) {
1676
- if (key === "Parent") continue;
1677
- const val = node[key];
1678
- if (Array.isArray(val)) {
1679
- val.forEach((child) => {
1680
- if (child && typeof child === "object" && "type" in child) {
1681
- walk(child);
1682
- }
1683
- });
1684
- } else if (val && typeof val === "object" && "type" in val) {
1685
- walk(val);
1686
- }
2034
+ const f = sharedParser.Parse(command, "cmd");
2035
+ syntax.Walk(f, (node) => {
2036
+ if (!node) return false;
2037
+ const n = node;
2038
+ if (syntax.NodeType(n) !== "CallExpr") return true;
2039
+ const wordValues = (n.Args || []).map((arg) => {
2040
+ return (arg.Parts || []).map((p) => (p.Value ?? "").replace(/\\(.)/g, "$1")).join("");
2041
+ }).filter((s) => s.length > 0);
2042
+ if (wordValues.length > 0) {
2043
+ const cmd = wordValues[0].toLowerCase();
2044
+ if (!actions.includes(cmd)) actions.push(cmd);
2045
+ wordValues.forEach((w) => addToken(w));
2046
+ wordValues.slice(1).forEach((w) => {
2047
+ if (!w.startsWith("-")) paths.push(w);
2048
+ });
1687
2049
  }
1688
- };
1689
- walk(ast);
2050
+ return true;
2051
+ });
1690
2052
  } catch {
1691
2053
  }
1692
2054
  if (allTokens.length === 0) {
@@ -1711,7 +2073,18 @@ async function analyzeShellCommand(command) {
1711
2073
  }
1712
2074
  async function evaluatePolicy(toolName, args, agent, cwd) {
1713
2075
  const config = getConfig();
1714
- if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
2076
+ const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
2077
+ if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
2078
+ const dlpMatch = args !== void 0 ? scanArgs(args) : null;
2079
+ if (dlpMatch) {
2080
+ return {
2081
+ decision: dlpMatch.severity,
2082
+ blockedByLabel: `DLP: ${dlpMatch.patternName}`,
2083
+ reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
2084
+ };
2085
+ }
2086
+ }
2087
+ if (wouldBeIgnored) return { decision: "allow" };
1715
2088
  if (config.policy.smartRules.length > 0) {
1716
2089
  const matchedRule = config.policy.smartRules.find(
1717
2090
  (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
@@ -1741,13 +2114,30 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1741
2114
  let pathTokens = [];
1742
2115
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
1743
2116
  if (shellCommand) {
1744
- const analyzed = await analyzeShellCommand(shellCommand);
2117
+ const analyzed = analyzeShellCommand(shellCommand);
1745
2118
  allTokens = analyzed.allTokens;
1746
2119
  pathTokens = analyzed.paths;
1747
2120
  const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
1748
2121
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
1749
2122
  return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
1750
2123
  }
2124
+ const evalVerdict = detectDangerousShellExec(shellCommand);
2125
+ if (evalVerdict === "block") {
2126
+ return {
2127
+ decision: "block",
2128
+ blockedByLabel: "Node9: Eval Remote Execution",
2129
+ reason: "eval of remote download (curl/wget) is a near-certain supply-chain attack",
2130
+ tier: 3
2131
+ };
2132
+ }
2133
+ if (evalVerdict === "review") {
2134
+ return {
2135
+ decision: "review",
2136
+ blockedByLabel: "Node9: Eval Dynamic Content",
2137
+ reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2138
+ tier: 3
2139
+ };
2140
+ }
1751
2141
  const pipeAnalysis = analyzePipeChain(shellCommand);
1752
2142
  if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1753
2143
  const sinks = pipeAnalysis.sinkTargets;