@node9/proxy 1.11.3 → 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.js CHANGED
@@ -447,7 +447,7 @@ var DEFAULT_CONFIG = {
447
447
  // 120-second auto-deny timeout
448
448
  flightRecorder: true,
449
449
  auditHashArgs: true,
450
- approvers: { native: true, browser: true, cloud: false, terminal: true },
450
+ approvers: { native: true, browser: false, cloud: false, terminal: true },
451
451
  cloudSyncIntervalHours: 5
452
452
  },
453
453
  policy: {
@@ -554,7 +554,7 @@ var DEFAULT_CONFIG = {
554
554
  },
555
555
  // ── Git safety ────────────────────────────────────────────────────────
556
556
  {
557
- name: "block-force-push",
557
+ name: "review-force-push",
558
558
  tool: "bash",
559
559
  conditions: [
560
560
  {
@@ -567,8 +567,8 @@ var DEFAULT_CONFIG = {
567
567
  }
568
568
  ],
569
569
  conditionMode: "all",
570
- verdict: "block",
571
- reason: "Force push overwrites remote history and cannot be undone",
570
+ verdict: "review",
571
+ reason: "Force push rewrites remote history \u2014 confirm this is intentional",
572
572
  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."
573
573
  },
574
574
  {
@@ -578,14 +578,16 @@ var DEFAULT_CONFIG = {
578
578
  {
579
579
  field: "command",
580
580
  op: "matches",
581
- value: "\\bgit\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
581
+ // Anchor git as a shell command so node -e / python -c scripts containing
582
+ // "git reset --hard" as a string don't false-positive.
583
+ value: "(^|&&|\\|\\||;)\\s*git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
582
584
  flags: "i"
583
585
  },
584
586
  {
585
587
  field: "command",
586
588
  op: "notMatches",
587
- // Exclude recovery ops these resolve a conflict, not start a destructive action.
588
- value: "\\bgit\\s+rebase\\s+--(abort|continue|skip)\\b",
589
+ // Exclude recovery ops and routine branch-surgery (--onto) these are not destructive.
590
+ value: "\\bgit\\s+rebase\\s+--(abort|continue|skip|onto)\\b",
589
591
  flags: "i"
590
592
  }
591
593
  ],
@@ -984,37 +986,314 @@ function getCompiledRegex(pattern, flags = "") {
984
986
  // src/policy/index.ts
985
987
  var import_path8 = __toESM(require("path"));
986
988
  var import_picomatch = __toESM(require("picomatch"));
987
- var import_sh_syntax = require("sh-syntax");
989
+ var import_mvdan_sh = __toESM(require("mvdan-sh"));
988
990
 
989
991
  // src/dlp.ts
990
992
  var import_fs4 = __toESM(require("fs"));
991
993
  var import_path4 = __toESM(require("path"));
994
+ var DLP_STOPWORDS = [
995
+ "example",
996
+ "placeholder",
997
+ "changeme",
998
+ "your_key",
999
+ "your_token",
1000
+ "your_secret",
1001
+ "replace_me",
1002
+ "insert_key",
1003
+ "put_your",
1004
+ "fake",
1005
+ "dummy",
1006
+ "sample",
1007
+ "xxxxxxxx",
1008
+ "aaaaaa",
1009
+ "bbbbbb",
1010
+ "00000000",
1011
+ "${",
1012
+ "{{",
1013
+ "%{",
1014
+ "<your",
1015
+ "test_key",
1016
+ "test_token"
1017
+ ];
992
1018
  var DLP_PATTERNS = [
993
- { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
994
- { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
995
- // Slack bot tokens: xoxb- + variable segment. Real tokens are ~50–80 chars;
996
- // lower bound 20 avoids false negatives on partial tokens, upper 100 caps scan cost.
997
- { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/, severity: "block" },
998
- { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
999
- { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
1019
+ // ── AWS ───────────────────────────────────────────────────────────────────
1000
1020
  {
1001
- name: "Private Key (PEM)",
1002
- regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
1003
- severity: "block"
1021
+ name: "AWS Access Key ID",
1022
+ regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/,
1023
+ severity: "block",
1024
+ keywords: ["akia", "asia", "abia", "acca", "a3t"]
1025
+ },
1026
+ // ── GitHub ────────────────────────────────────────────────────────────────
1027
+ {
1028
+ name: "GitHub Token",
1029
+ regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/,
1030
+ severity: "block",
1031
+ keywords: ["ghp_", "gho_", "ghu_", "ghs_"]
1032
+ },
1033
+ {
1034
+ name: "GitHub Fine-Grained PAT",
1035
+ regex: /\bgithub_pat_\w{82}\b/,
1036
+ severity: "block",
1037
+ keywords: ["github_pat_"]
1038
+ },
1039
+ // ── Slack ─────────────────────────────────────────────────────────────────
1040
+ {
1041
+ name: "Slack Bot Token",
1042
+ // Real tokens are ~50–80 chars; lower bound 20 avoids false negatives on partial tokens
1043
+ regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/,
1044
+ severity: "block",
1045
+ keywords: ["xoxb-"]
1046
+ },
1047
+ // ── Anthropic ─────────────────────────────────────────────────────────────
1048
+ // Listed before OpenAI — Anthropic keys start with sk-ant- which would also
1049
+ // match the broader OpenAI sk- pattern; more specific rules must come first.
1050
+ {
1051
+ name: "Anthropic API Key",
1052
+ regex: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA\b/,
1053
+ severity: "block",
1054
+ keywords: ["sk-ant-api03"]
1055
+ },
1056
+ {
1057
+ name: "Anthropic Admin Key",
1058
+ regex: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA\b/,
1059
+ severity: "block",
1060
+ keywords: ["sk-ant-admin01"]
1061
+ },
1062
+ // ── OpenAI ────────────────────────────────────────────────────────────────
1063
+ {
1064
+ name: "OpenAI API Key",
1065
+ regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/,
1066
+ severity: "block",
1067
+ keywords: ["sk-"]
1068
+ },
1069
+ // ── Stripe ────────────────────────────────────────────────────────────────
1070
+ {
1071
+ name: "Stripe Secret Key",
1072
+ regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/,
1073
+ severity: "block",
1074
+ keywords: ["sk_live_", "sk_test_"]
1075
+ },
1076
+ // ── GCP ───────────────────────────────────────────────────────────────────
1077
+ {
1078
+ name: "GCP API Key",
1079
+ regex: /\bAIza[0-9A-Za-z_-]{35}\b/,
1080
+ severity: "block",
1081
+ keywords: ["aiza"]
1004
1082
  },
1005
- // GCP service account JSON (detects the type field that uniquely identifies it)
1006
1083
  {
1007
1084
  name: "GCP Service Account",
1008
1085
  regex: /"type"\s*:\s*"service_account"/,
1009
- severity: "block"
1086
+ severity: "block",
1087
+ keywords: ["service_account"]
1088
+ },
1089
+ // ── Azure ─────────────────────────────────────────────────────────────────
1090
+ // Pattern: 3 alphanum chars + digit + Q~ + 31-34 alphanum chars
1091
+ {
1092
+ name: "Azure AD Client Secret",
1093
+ regex: /(?:^|[\s>=:(,])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\s<),])/,
1094
+ severity: "block",
1095
+ keywords: ["q~"]
1096
+ },
1097
+ // ── Databricks ────────────────────────────────────────────────────────────
1098
+ {
1099
+ name: "Databricks API Token",
1100
+ regex: /\bdapi[a-f0-9]{32}(?:-\d)?\b/,
1101
+ severity: "block",
1102
+ keywords: ["dapi"]
1103
+ },
1104
+ // ── DigitalOcean ──────────────────────────────────────────────────────────
1105
+ {
1106
+ name: "DigitalOcean PAT",
1107
+ regex: /\bdop_v1_[a-f0-9]{64}\b/,
1108
+ severity: "block",
1109
+ keywords: ["dop_v1_"]
1110
+ },
1111
+ {
1112
+ name: "DigitalOcean Access Token",
1113
+ regex: /\bdoo_v1_[a-f0-9]{64}\b/,
1114
+ severity: "block",
1115
+ keywords: ["doo_v1_"]
1116
+ },
1117
+ // ── Doppler ───────────────────────────────────────────────────────────────
1118
+ {
1119
+ name: "Doppler Token",
1120
+ regex: /\bdp\.pt\.[a-z0-9]{43}\b/i,
1121
+ severity: "block",
1122
+ keywords: ["dp.pt."]
1123
+ },
1124
+ // ── HashiCorp Vault ───────────────────────────────────────────────────────
1125
+ {
1126
+ name: "HashiCorp Vault Service Token",
1127
+ regex: /\bhvs\.[\w-]{90,120}\b/,
1128
+ severity: "block",
1129
+ keywords: ["hvs."]
1010
1130
  },
1011
- // NPM auth token in .npmrc format
1131
+ {
1132
+ name: "HashiCorp Vault Batch Token",
1133
+ regex: /\bhvb\.[\w-]{138,300}\b/,
1134
+ severity: "block",
1135
+ keywords: ["hvb."]
1136
+ },
1137
+ // ── Hugging Face ──────────────────────────────────────────────────────────
1138
+ { name: "HuggingFace Token", regex: /\bhf_[A-Za-z]{34}\b/, severity: "block", keywords: ["hf_"] },
1139
+ // ── Postman ───────────────────────────────────────────────────────────────
1140
+ {
1141
+ name: "Postman API Token",
1142
+ regex: /\bPMAK-[a-f0-9]{24}-[a-f0-9]{34}\b/i,
1143
+ severity: "block",
1144
+ keywords: ["pmak-"]
1145
+ },
1146
+ // ── Pulumi ────────────────────────────────────────────────────────────────
1147
+ {
1148
+ name: "Pulumi Access Token",
1149
+ regex: /\bpul-[a-f0-9]{40}\b/,
1150
+ severity: "block",
1151
+ keywords: ["pul-"]
1152
+ },
1153
+ // ── SendGrid ──────────────────────────────────────────────────────────────
1154
+ {
1155
+ name: "SendGrid API Key",
1156
+ regex: /\bSG\.[a-zA-Z0-9=_.-]{66}\b/,
1157
+ severity: "block",
1158
+ keywords: ["sg."]
1159
+ },
1160
+ // ── Private keys (PEM) ────────────────────────────────────────────────────
1161
+ {
1162
+ name: "Private Key (PEM)",
1163
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
1164
+ severity: "block",
1165
+ keywords: ["-----begin"]
1166
+ },
1167
+ // ── NPM ───────────────────────────────────────────────────────────────────
1012
1168
  {
1013
1169
  name: "NPM Auth Token",
1014
- regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
1015
- severity: "block"
1170
+ regex: /_authToken\s*=\s*[A-Za-z0-9_-]{20,}/,
1171
+ severity: "block",
1172
+ keywords: ["_authtoken"]
1173
+ },
1174
+ // ── JWT ───────────────────────────────────────────────────────────────────
1175
+ // review (not block): JWTs appear legitimately in API calls; flag for human approval
1176
+ {
1177
+ name: "JWT",
1178
+ regex: /\bey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/_-]{17,}\.[a-zA-Z0-9\/_-]{10,}={0,2}\b/,
1179
+ severity: "review",
1180
+ keywords: ["eyj"]
1181
+ },
1182
+ // ── Stripe (extended — adds restricted key rk_ prefix) ──────────────────
1183
+ {
1184
+ name: "Stripe Restricted Key",
1185
+ regex: /\brk_(?:live|test|prod)_[0-9a-zA-Z]{10,99}\b/,
1186
+ severity: "block",
1187
+ keywords: ["rk_live_", "rk_test_", "rk_prod_"]
1188
+ },
1189
+ // ── Slack (app token) ─────────────────────────────────────────────────────
1190
+ {
1191
+ name: "Slack App Token",
1192
+ regex: /\bxapp-\d-[A-Z0-9]+-\d+-[a-f0-9]+\b/,
1193
+ severity: "block",
1194
+ keywords: ["xapp-"]
1195
+ },
1196
+ // ── GitLab ────────────────────────────────────────────────────────────────
1197
+ { name: "GitLab PAT", regex: /\bglpat-[\w-]{20}\b/, severity: "block", keywords: ["glpat-"] },
1198
+ {
1199
+ name: "GitLab Deploy Token",
1200
+ regex: /\bgldt-[0-9a-zA-Z_-]{20}\b/,
1201
+ severity: "block",
1202
+ keywords: ["gldt-"]
1203
+ },
1204
+ {
1205
+ name: "GitLab CI Job Token",
1206
+ regex: /\bglcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}\b/,
1207
+ severity: "block",
1208
+ keywords: ["glcbt-"]
1209
+ },
1210
+ // ── npm (publish token) ───────────────────────────────────────────────────
1211
+ {
1212
+ name: "npm Access Token",
1213
+ regex: /\bnpm_[a-zA-Z0-9]{36}\b/,
1214
+ severity: "block",
1215
+ keywords: ["npm_"]
1216
+ },
1217
+ // ── Shopify ───────────────────────────────────────────────────────────────
1218
+ {
1219
+ name: "Shopify Access Token",
1220
+ regex: /\bshpat_[a-fA-F0-9]{32}\b/,
1221
+ severity: "block",
1222
+ keywords: ["shpat_"]
1223
+ },
1224
+ {
1225
+ name: "Shopify Custom Access Token",
1226
+ regex: /\bshpca_[a-fA-F0-9]{32}\b/,
1227
+ severity: "block",
1228
+ keywords: ["shpca_"]
1229
+ },
1230
+ {
1231
+ name: "Shopify Private App Token",
1232
+ regex: /\bshppa_[a-fA-F0-9]{32}\b/,
1233
+ severity: "block",
1234
+ keywords: ["shppa_"]
1235
+ },
1236
+ {
1237
+ name: "Shopify Shared Secret",
1238
+ regex: /\bshpss_[a-fA-F0-9]{32}\b/,
1239
+ severity: "block",
1240
+ keywords: ["shpss_"]
1241
+ },
1242
+ // ── Linear ────────────────────────────────────────────────────────────────
1243
+ {
1244
+ name: "Linear API Key",
1245
+ regex: /\blin_api_[a-zA-Z0-9]{40}\b/,
1246
+ severity: "block",
1247
+ keywords: ["lin_api_"]
1016
1248
  },
1017
- { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i, severity: "review" }
1249
+ // ── PlanetScale ───────────────────────────────────────────────────────────
1250
+ {
1251
+ name: "PlanetScale API Token",
1252
+ regex: /\bpscale_tkn_[\w.-]{32,64}\b/,
1253
+ severity: "block",
1254
+ keywords: ["pscale_tkn_"]
1255
+ },
1256
+ {
1257
+ name: "PlanetScale Password",
1258
+ regex: /\bpscale_pw_[\w.-]{32,64}\b/,
1259
+ severity: "block",
1260
+ keywords: ["pscale_pw_"]
1261
+ },
1262
+ // ── Sentry ────────────────────────────────────────────────────────────────
1263
+ {
1264
+ name: "Sentry User Token",
1265
+ regex: /\bsntryu_[a-f0-9]{64}\b/,
1266
+ severity: "block",
1267
+ keywords: ["sntryu_"]
1268
+ },
1269
+ // ── Grafana ───────────────────────────────────────────────────────────────
1270
+ {
1271
+ name: "Grafana Service Account Token",
1272
+ regex: /\bglsa_[a-zA-Z0-9]{32}_[a-f0-9]{8}\b/,
1273
+ severity: "block",
1274
+ keywords: ["glsa_"]
1275
+ },
1276
+ // ── Heroku ────────────────────────────────────────────────────────────────
1277
+ {
1278
+ name: "Heroku API Key",
1279
+ regex: /\bHRKU-AA[0-9a-zA-Z_-]{58}\b/,
1280
+ severity: "block",
1281
+ keywords: ["hrku-aa"]
1282
+ },
1283
+ // ── PyPI ──────────────────────────────────────────────────────────────────
1284
+ {
1285
+ name: "PyPI Upload Token",
1286
+ regex: /\bpypi-[A-Za-z0-9_-]{50,}\b/,
1287
+ severity: "block",
1288
+ keywords: ["pypi-"]
1289
+ },
1290
+ // ── Bearer Token ─────────────────────────────────────────────────────────
1291
+ {
1292
+ name: "Bearer Token",
1293
+ regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i,
1294
+ severity: "review",
1295
+ keywords: ["bearer"]
1296
+ }
1018
1297
  ];
1019
1298
  var SENSITIVE_PATH_PATTERNS = [
1020
1299
  /[/\\]\.ssh[/\\]/i,
@@ -1103,8 +1382,14 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
1103
1382
  }
1104
1383
  if (typeof args === "string") {
1105
1384
  const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
1385
+ const textLower = text.toLowerCase();
1106
1386
  for (const pattern of DLP_PATTERNS) {
1387
+ if (pattern.keywords && !pattern.keywords.some((kw) => textLower.includes(kw.toLowerCase()))) {
1388
+ continue;
1389
+ }
1107
1390
  if (pattern.regex.test(text)) {
1391
+ const matchedValue = (text.match(pattern.regex)?.[0] ?? "").toLowerCase();
1392
+ if (DLP_STOPWORDS.some((sw) => matchedValue.includes(sw))) continue;
1108
1393
  return {
1109
1394
  patternName: pattern.name,
1110
1395
  fieldPath,
@@ -1604,17 +1889,111 @@ function getNestedValue(obj, path15) {
1604
1889
  if (!obj || typeof obj !== "object") return null;
1605
1890
  return path15.split(".").reduce((prev, curr) => prev?.[curr], obj);
1606
1891
  }
1607
- function stripStringArguments(cmd) {
1608
- let result = cmd;
1609
- result = result.replace(
1610
- /\b(node|python3?|ruby|perl|php|deno)\s+(-[ecr]|eval)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/gi,
1611
- '$1 $2 ""'
1612
- );
1613
- result = result.replace(
1614
- /\s(-m|--message|--body|--title|--description)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
1615
- ' $1 ""'
1616
- );
1617
- return result;
1892
+ var { syntax } = import_mvdan_sh.default;
1893
+ var sharedParser = syntax.NewParser();
1894
+ var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
1895
+ "-m",
1896
+ "--message",
1897
+ "--body",
1898
+ "--title",
1899
+ "--description",
1900
+ "--comment",
1901
+ "--subject",
1902
+ "--summary"
1903
+ ]);
1904
+ function normalizeCommandForPolicy(command) {
1905
+ try {
1906
+ const f = sharedParser.Parse(command, "cmd");
1907
+ const strips = [];
1908
+ syntax.Walk(f, (node) => {
1909
+ if (!node) return false;
1910
+ const n = node;
1911
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1912
+ const args = n.Args || [];
1913
+ for (let i = 0; i < args.length - 1; i++) {
1914
+ const argParts = args[i].Parts || [];
1915
+ if (argParts.length !== 1 || syntax.NodeType(argParts[0]) !== "Lit") continue;
1916
+ const flagVal = argParts[0].Value || "";
1917
+ if (!MESSAGE_FLAGS.has(flagVal.toLowerCase())) continue;
1918
+ const next = args[i + 1];
1919
+ const nextParts = next.Parts || [];
1920
+ if (nextParts.length !== 1) continue;
1921
+ const quotedNode = nextParts[0];
1922
+ const nt = syntax.NodeType(quotedNode);
1923
+ if (nt === "SglQuoted") {
1924
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
1925
+ } else if (nt === "DblQuoted") {
1926
+ const innerParts = quotedNode.Parts || [];
1927
+ const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
1928
+ if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
1929
+ }
1930
+ }
1931
+ return true;
1932
+ });
1933
+ if (strips.length === 0) return command;
1934
+ strips.sort((a, b) => b[0] - a[0]);
1935
+ let result = command;
1936
+ for (const [start, end] of strips) {
1937
+ result = result.slice(0, start) + '""' + result.slice(end);
1938
+ }
1939
+ return result;
1940
+ } catch {
1941
+ return command;
1942
+ }
1943
+ }
1944
+ var SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
1945
+ var DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
1946
+ function scanArgsForDynamicExec(args, startIdx) {
1947
+ let hasCmdSubst = false;
1948
+ let hasParamExp = false;
1949
+ let hasCurl = false;
1950
+ for (let i = startIdx; i < args.length; i++) {
1951
+ syntax.Walk(args[i], (inner) => {
1952
+ if (!inner) return false;
1953
+ const inn = inner;
1954
+ const it = syntax.NodeType(inn);
1955
+ if (it === "CmdSubst") hasCmdSubst = true;
1956
+ if (it === "ParamExp") hasParamExp = true;
1957
+ if (it === "Lit" && DOWNLOAD_CMDS.has(inn.Value?.toLowerCase())) hasCurl = true;
1958
+ return true;
1959
+ });
1960
+ }
1961
+ if (hasCmdSubst && hasCurl) return "block";
1962
+ if (hasCmdSubst || hasParamExp) return "review";
1963
+ return null;
1964
+ }
1965
+ function detectDangerousShellExec(command) {
1966
+ try {
1967
+ const f = sharedParser.Parse(command, "cmd");
1968
+ let result = null;
1969
+ syntax.Walk(f, (node) => {
1970
+ if (!node || result === "block") return false;
1971
+ const n = node;
1972
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1973
+ const args = n.Args || [];
1974
+ if (args.length === 0) return true;
1975
+ const firstParts = args[0].Parts || [];
1976
+ if (firstParts.length !== 1 || syntax.NodeType(firstParts[0]) !== "Lit") return true;
1977
+ const cmdName = firstParts[0].Value?.toLowerCase() ?? "";
1978
+ if (cmdName === "eval") {
1979
+ const v = scanArgsForDynamicExec(args, 1);
1980
+ if (v === "block" || v === "review" && result === null) result = v;
1981
+ } else if (SHELL_INTERPRETERS.has(cmdName)) {
1982
+ for (let i = 1; i < args.length - 1; i++) {
1983
+ const flagParts = args[i].Parts || [];
1984
+ if (flagParts.length !== 1 || syntax.NodeType(flagParts[0]) !== "Lit" || flagParts[0].Value !== "-c")
1985
+ continue;
1986
+ const v = scanArgsForDynamicExec(args, i + 1);
1987
+ if (v === "block" || v === "review" && result === null) result = v;
1988
+ break;
1989
+ }
1990
+ }
1991
+ return true;
1992
+ });
1993
+ return result;
1994
+ } catch {
1995
+ return null;
1996
+ }
1618
1997
  }
1619
1998
  function evaluateSmartConditions(args, rule) {
1620
1999
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -1622,7 +2001,7 @@ function evaluateSmartConditions(args, rule) {
1622
2001
  const results = rule.conditions.map((cond) => {
1623
2002
  const rawVal = getNestedValue(args, cond.field);
1624
2003
  const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1625
- const val = cond.field === "command" && normalized !== null ? stripStringArguments(normalized) : normalized;
2004
+ const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
1626
2005
  switch (cond.op) {
1627
2006
  case "exists":
1628
2007
  return val !== null && val !== "";
@@ -1671,52 +2050,35 @@ function isSqlTool(toolName, toolInspection) {
1671
2050
  return fieldName === "sql" || fieldName === "query";
1672
2051
  }
1673
2052
  var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
1674
- async function analyzeShellCommand(command) {
2053
+ function analyzeShellCommand(command) {
1675
2054
  const actions = [];
1676
2055
  const paths = [];
1677
2056
  const allTokens = [];
1678
2057
  const addToken = (token) => {
1679
2058
  const lower = token.toLowerCase();
1680
2059
  allTokens.push(lower);
1681
- if (lower.includes("/")) {
1682
- const segments = lower.split("/").filter(Boolean);
1683
- allTokens.push(...segments);
1684
- }
1685
- if (lower.startsWith("-")) {
1686
- allTokens.push(lower.replace(/^-+/, ""));
1687
- }
2060
+ if (lower.includes("/")) allTokens.push(...lower.split("/").filter(Boolean));
2061
+ if (lower.startsWith("-")) allTokens.push(lower.replace(/^-+/, ""));
1688
2062
  };
1689
2063
  try {
1690
- const ast = await (0, import_sh_syntax.parse)(command);
1691
- const walk = (node) => {
1692
- if (!node) return;
1693
- if (node.type === "CallExpr") {
1694
- const parts = (node.Args || []).map((arg) => {
1695
- return (arg.Parts || []).map((p) => p.Value || "").join("");
1696
- }).filter((s) => s.length > 0);
1697
- if (parts.length > 0) {
1698
- actions.push(parts[0].toLowerCase());
1699
- parts.forEach((p) => addToken(p));
1700
- parts.slice(1).forEach((p) => {
1701
- if (!p.startsWith("-")) paths.push(p);
1702
- });
1703
- }
1704
- }
1705
- for (const key in node) {
1706
- if (key === "Parent") continue;
1707
- const val = node[key];
1708
- if (Array.isArray(val)) {
1709
- val.forEach((child) => {
1710
- if (child && typeof child === "object" && "type" in child) {
1711
- walk(child);
1712
- }
1713
- });
1714
- } else if (val && typeof val === "object" && "type" in val) {
1715
- walk(val);
1716
- }
2064
+ const f = sharedParser.Parse(command, "cmd");
2065
+ syntax.Walk(f, (node) => {
2066
+ if (!node) return false;
2067
+ const n = node;
2068
+ if (syntax.NodeType(n) !== "CallExpr") return true;
2069
+ const wordValues = (n.Args || []).map((arg) => {
2070
+ return (arg.Parts || []).map((p) => (p.Value ?? "").replace(/\\(.)/g, "$1")).join("");
2071
+ }).filter((s) => s.length > 0);
2072
+ if (wordValues.length > 0) {
2073
+ const cmd = wordValues[0].toLowerCase();
2074
+ if (!actions.includes(cmd)) actions.push(cmd);
2075
+ wordValues.forEach((w) => addToken(w));
2076
+ wordValues.slice(1).forEach((w) => {
2077
+ if (!w.startsWith("-")) paths.push(w);
2078
+ });
1717
2079
  }
1718
- };
1719
- walk(ast);
2080
+ return true;
2081
+ });
1720
2082
  } catch {
1721
2083
  }
1722
2084
  if (allTokens.length === 0) {
@@ -1741,7 +2103,18 @@ async function analyzeShellCommand(command) {
1741
2103
  }
1742
2104
  async function evaluatePolicy(toolName, args, agent, cwd) {
1743
2105
  const config = getConfig();
1744
- if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
2106
+ const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
2107
+ if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
2108
+ const dlpMatch = args !== void 0 ? scanArgs(args) : null;
2109
+ if (dlpMatch) {
2110
+ return {
2111
+ decision: dlpMatch.severity,
2112
+ blockedByLabel: `DLP: ${dlpMatch.patternName}`,
2113
+ reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
2114
+ };
2115
+ }
2116
+ }
2117
+ if (wouldBeIgnored) return { decision: "allow" };
1745
2118
  if (config.policy.smartRules.length > 0) {
1746
2119
  const matchedRule = config.policy.smartRules.find(
1747
2120
  (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
@@ -1771,13 +2144,30 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1771
2144
  let pathTokens = [];
1772
2145
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
1773
2146
  if (shellCommand) {
1774
- const analyzed = await analyzeShellCommand(shellCommand);
2147
+ const analyzed = analyzeShellCommand(shellCommand);
1775
2148
  allTokens = analyzed.allTokens;
1776
2149
  pathTokens = analyzed.paths;
1777
2150
  const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
1778
2151
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
1779
2152
  return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
1780
2153
  }
2154
+ const evalVerdict = detectDangerousShellExec(shellCommand);
2155
+ if (evalVerdict === "block") {
2156
+ return {
2157
+ decision: "block",
2158
+ blockedByLabel: "Node9: Eval Remote Execution",
2159
+ reason: "eval of remote download (curl/wget) is a near-certain supply-chain attack",
2160
+ tier: 3
2161
+ };
2162
+ }
2163
+ if (evalVerdict === "review") {
2164
+ return {
2165
+ decision: "review",
2166
+ blockedByLabel: "Node9: Eval Dynamic Content",
2167
+ reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2168
+ tier: 3
2169
+ };
2170
+ }
1781
2171
  const pipeAnalysis = analyzePipeChain(shellCommand);
1782
2172
  if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1783
2173
  const sinks = pipeAnalysis.sinkTargets;