@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.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:
|
|
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: "
|
|
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: "
|
|
541
|
-
reason: "Force push
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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: "
|
|
972
|
-
regex:
|
|
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
|
-
|
|
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_
|
|
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
|
-
|
|
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
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1661
|
-
|
|
1662
|
-
if (!node) return;
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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;
|