@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/README.md +39 -31
- package/dist/cli.js +2029 -286
- package/dist/cli.mjs +2023 -280
- package/dist/index.js +466 -76
- package/dist/index.mjs +466 -76
- package/dist/shields/builtin/bash-safe.json +18 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -116,7 +116,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
116
116
|
}
|
|
117
117
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
118
118
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
119
|
-
const testRun = isTestCall(toolName, args) ? { testRun: true } : {};
|
|
119
|
+
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
120
120
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
121
121
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
122
122
|
tool: toolName,
|
|
@@ -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:
|
|
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: "
|
|
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: "
|
|
571
|
-
reason: "Force push
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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: "
|
|
1002
|
-
regex:
|
|
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
|
-
|
|
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_
|
|
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
|
-
|
|
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
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1691
|
-
|
|
1692
|
-
if (!node) return;
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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;
|