@node9/proxy 1.3.0 → 1.3.2
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 +13 -0
- package/dist/cli.js +759 -270
- package/dist/cli.mjs +746 -257
- package/dist/index.js +506 -53
- package/dist/index.mjs +506 -53
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -155,8 +155,8 @@ function sanitizeConfig(raw) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
const lines = result.error.issues.map((issue) => {
|
|
158
|
-
const
|
|
159
|
-
return ` \u2022 ${
|
|
158
|
+
const path13 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
159
|
+
return ` \u2022 ${path13}: ${issue.message}`;
|
|
160
160
|
});
|
|
161
161
|
return {
|
|
162
162
|
sanitized,
|
|
@@ -886,6 +886,7 @@ function getCompiledRegex(pattern, flags = "") {
|
|
|
886
886
|
}
|
|
887
887
|
|
|
888
888
|
// src/policy/index.ts
|
|
889
|
+
import path7 from "path";
|
|
889
890
|
import pm from "picomatch";
|
|
890
891
|
import { parse } from "sh-syntax";
|
|
891
892
|
|
|
@@ -1031,8 +1032,416 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1031
1032
|
return null;
|
|
1032
1033
|
}
|
|
1033
1034
|
|
|
1035
|
+
// src/utils/provenance.ts
|
|
1036
|
+
import fs5 from "fs";
|
|
1037
|
+
import path5 from "path";
|
|
1038
|
+
import os4 from "os";
|
|
1039
|
+
var SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
|
|
1040
|
+
var MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
|
|
1041
|
+
var USER_PREFIXES = [
|
|
1042
|
+
path5.join(os4.homedir(), "bin"),
|
|
1043
|
+
path5.join(os4.homedir(), ".local", "bin"),
|
|
1044
|
+
path5.join(os4.homedir(), ".cargo", "bin"),
|
|
1045
|
+
path5.join(os4.homedir(), ".npm-global", "bin"),
|
|
1046
|
+
path5.join(os4.homedir(), ".volta", "bin")
|
|
1047
|
+
];
|
|
1048
|
+
var SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
|
|
1049
|
+
function findInPath(cmd) {
|
|
1050
|
+
if (path5.posix.isAbsolute(cmd)) return cmd;
|
|
1051
|
+
const pathEnv = process.env.PATH ?? "";
|
|
1052
|
+
for (const dir of pathEnv.split(path5.delimiter)) {
|
|
1053
|
+
if (!dir) continue;
|
|
1054
|
+
const full = path5.join(dir, cmd);
|
|
1055
|
+
try {
|
|
1056
|
+
fs5.accessSync(full, fs5.constants.X_OK);
|
|
1057
|
+
return full;
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
function _classifyPath(resolved, cwd) {
|
|
1064
|
+
if (cwd && resolved.startsWith(cwd + "/")) {
|
|
1065
|
+
return { trustLevel: "user", reason: "binary in project directory" };
|
|
1066
|
+
}
|
|
1067
|
+
const osTmp = os4.tmpdir();
|
|
1068
|
+
const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
|
|
1069
|
+
if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
1070
|
+
return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
|
|
1071
|
+
}
|
|
1072
|
+
if (SYSTEM_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
1073
|
+
return { trustLevel: "system", reason: "" };
|
|
1074
|
+
}
|
|
1075
|
+
if (MANAGED_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
1076
|
+
return { trustLevel: "managed", reason: "" };
|
|
1077
|
+
}
|
|
1078
|
+
if (USER_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
1079
|
+
return { trustLevel: "user", reason: "" };
|
|
1080
|
+
}
|
|
1081
|
+
return { trustLevel: "unknown", reason: "binary in unrecognized location" };
|
|
1082
|
+
}
|
|
1083
|
+
function checkProvenance(cmd, cwd) {
|
|
1084
|
+
const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
|
|
1085
|
+
if (path5.posix.isAbsolute(bare)) {
|
|
1086
|
+
const early = _classifyPath(bare, cwd);
|
|
1087
|
+
if (early.trustLevel === "suspect") {
|
|
1088
|
+
return { resolvedPath: bare, ...early };
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
let resolved;
|
|
1092
|
+
try {
|
|
1093
|
+
const found = findInPath(bare);
|
|
1094
|
+
if (!found) {
|
|
1095
|
+
return {
|
|
1096
|
+
resolvedPath: cmd,
|
|
1097
|
+
trustLevel: "unknown",
|
|
1098
|
+
reason: "binary not found in PATH"
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
resolved = fs5.realpathSync(found);
|
|
1102
|
+
} catch {
|
|
1103
|
+
return {
|
|
1104
|
+
resolvedPath: cmd,
|
|
1105
|
+
trustLevel: "unknown",
|
|
1106
|
+
reason: "binary not found in PATH"
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
try {
|
|
1110
|
+
const stat = fs5.statSync(resolved);
|
|
1111
|
+
if (stat.mode & 2) {
|
|
1112
|
+
return {
|
|
1113
|
+
resolvedPath: resolved,
|
|
1114
|
+
trustLevel: "suspect",
|
|
1115
|
+
reason: "binary is world-writable"
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
} catch {
|
|
1119
|
+
return {
|
|
1120
|
+
resolvedPath: resolved,
|
|
1121
|
+
trustLevel: "unknown",
|
|
1122
|
+
reason: "could not stat binary"
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
const classify = _classifyPath(resolved, cwd);
|
|
1126
|
+
return { resolvedPath: resolved, ...classify };
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/policy/pipe-chain.ts
|
|
1130
|
+
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1131
|
+
"cat",
|
|
1132
|
+
"head",
|
|
1133
|
+
"tail",
|
|
1134
|
+
"grep",
|
|
1135
|
+
"awk",
|
|
1136
|
+
"sed",
|
|
1137
|
+
"cut",
|
|
1138
|
+
"sort",
|
|
1139
|
+
"tee",
|
|
1140
|
+
"less",
|
|
1141
|
+
"more",
|
|
1142
|
+
"strings",
|
|
1143
|
+
"xxd"
|
|
1144
|
+
]);
|
|
1145
|
+
var SINK_COMMANDS = /* @__PURE__ */ new Set([
|
|
1146
|
+
"curl",
|
|
1147
|
+
"wget",
|
|
1148
|
+
"nc",
|
|
1149
|
+
"ncat",
|
|
1150
|
+
"netcat",
|
|
1151
|
+
"ssh",
|
|
1152
|
+
"scp",
|
|
1153
|
+
"rsync",
|
|
1154
|
+
"socat",
|
|
1155
|
+
"ftp",
|
|
1156
|
+
"sftp",
|
|
1157
|
+
"telnet"
|
|
1158
|
+
]);
|
|
1159
|
+
var OBFUSCATORS = /* @__PURE__ */ new Set([
|
|
1160
|
+
"base64",
|
|
1161
|
+
"gzip",
|
|
1162
|
+
"gunzip",
|
|
1163
|
+
"bzip2",
|
|
1164
|
+
"xz",
|
|
1165
|
+
"zstd",
|
|
1166
|
+
"openssl",
|
|
1167
|
+
"gpg",
|
|
1168
|
+
"python",
|
|
1169
|
+
"python3",
|
|
1170
|
+
"perl",
|
|
1171
|
+
"ruby",
|
|
1172
|
+
"node"
|
|
1173
|
+
]);
|
|
1174
|
+
var SENSITIVE_PATTERNS = [
|
|
1175
|
+
/(?:^|\/)\.env(?:\.|$)/i,
|
|
1176
|
+
// .env, .env.local, .env.production
|
|
1177
|
+
/id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
|
|
1178
|
+
// SSH private keys
|
|
1179
|
+
/\.pem$|\.key$|\.p12$|\.pfx$/i,
|
|
1180
|
+
// certificate files
|
|
1181
|
+
/(?:^|\/)\.ssh\//i,
|
|
1182
|
+
// ~/.ssh/ directory
|
|
1183
|
+
/(?:^|\/)\.aws\/credentials/i,
|
|
1184
|
+
// AWS credentials
|
|
1185
|
+
/(?:^|\/)\.netrc$/i,
|
|
1186
|
+
// netrc (stores HTTP credentials)
|
|
1187
|
+
/(?:^|\/)(passwd|shadow|sudoers)$/i,
|
|
1188
|
+
// /etc/passwd, /etc/shadow
|
|
1189
|
+
/(?:^|\/)credentials(?:\.json)?$/i
|
|
1190
|
+
// generic credentials files
|
|
1191
|
+
];
|
|
1192
|
+
function isSensitivePath(p) {
|
|
1193
|
+
return SENSITIVE_PATTERNS.some((re) => re.test(p));
|
|
1194
|
+
}
|
|
1195
|
+
function splitOnPipe(cmd) {
|
|
1196
|
+
const segments = [];
|
|
1197
|
+
let current = "";
|
|
1198
|
+
let inSingle = false;
|
|
1199
|
+
let inDouble = false;
|
|
1200
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
1201
|
+
const ch = cmd[i];
|
|
1202
|
+
if (ch === "'" && !inDouble) {
|
|
1203
|
+
inSingle = !inSingle;
|
|
1204
|
+
current += ch;
|
|
1205
|
+
} else if (ch === '"' && !inSingle) {
|
|
1206
|
+
inDouble = !inDouble;
|
|
1207
|
+
current += ch;
|
|
1208
|
+
} else if (ch === "|" && !inSingle && !inDouble && cmd[i + 1] !== "|" && (i === 0 || cmd[i - 1] !== "|")) {
|
|
1209
|
+
segments.push(current.trim());
|
|
1210
|
+
current = "";
|
|
1211
|
+
} else {
|
|
1212
|
+
current += ch;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (current.trim()) segments.push(current.trim());
|
|
1216
|
+
return segments.filter(Boolean);
|
|
1217
|
+
}
|
|
1218
|
+
function positionalTokens(segment) {
|
|
1219
|
+
return segment.split(/\s+/).slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
|
|
1220
|
+
}
|
|
1221
|
+
function analyzePipeChain(command) {
|
|
1222
|
+
const segments = splitOnPipe(command);
|
|
1223
|
+
if (segments.length < 2) {
|
|
1224
|
+
return {
|
|
1225
|
+
isPipeline: false,
|
|
1226
|
+
hasSensitiveSource: false,
|
|
1227
|
+
hasExternalSink: false,
|
|
1228
|
+
hasObfuscation: false,
|
|
1229
|
+
sourceFiles: [],
|
|
1230
|
+
sinkTargets: [],
|
|
1231
|
+
risk: "none"
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
const sourceFiles = [];
|
|
1235
|
+
const sinkTargets = [];
|
|
1236
|
+
let hasSensitiveSource = false;
|
|
1237
|
+
let hasExternalSink = false;
|
|
1238
|
+
let hasObfuscation = false;
|
|
1239
|
+
for (const segment of segments) {
|
|
1240
|
+
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
1241
|
+
if (tokens.length === 0) continue;
|
|
1242
|
+
const binary = tokens[0].toLowerCase();
|
|
1243
|
+
const args = positionalTokens(segment);
|
|
1244
|
+
if (SOURCE_COMMANDS.has(binary)) {
|
|
1245
|
+
sourceFiles.push(...args);
|
|
1246
|
+
if (args.some(isSensitivePath)) hasSensitiveSource = true;
|
|
1247
|
+
}
|
|
1248
|
+
if (OBFUSCATORS.has(binary)) hasObfuscation = true;
|
|
1249
|
+
if (SINK_COMMANDS.has(binary)) {
|
|
1250
|
+
const targets = args.filter(
|
|
1251
|
+
(a) => a.includes(".") || a.includes("://") || /^\d+\.\d+/.test(a)
|
|
1252
|
+
);
|
|
1253
|
+
sinkTargets.push(...targets);
|
|
1254
|
+
if (targets.length > 0) hasExternalSink = true;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const fullCmd = command.toLowerCase();
|
|
1258
|
+
if (!hasSensitiveSource) {
|
|
1259
|
+
const redirMatch = fullCmd.match(/<\s*(\S+)/);
|
|
1260
|
+
if (redirMatch && isSensitivePath(redirMatch[1])) {
|
|
1261
|
+
hasSensitiveSource = true;
|
|
1262
|
+
sourceFiles.push(redirMatch[1]);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
const risk = hasSensitiveSource && hasExternalSink && hasObfuscation ? "critical" : hasSensitiveSource && hasExternalSink ? "high" : hasExternalSink ? "medium" : "none";
|
|
1266
|
+
return {
|
|
1267
|
+
isPipeline: true,
|
|
1268
|
+
hasSensitiveSource,
|
|
1269
|
+
hasExternalSink,
|
|
1270
|
+
hasObfuscation,
|
|
1271
|
+
sourceFiles,
|
|
1272
|
+
sinkTargets,
|
|
1273
|
+
risk
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/policy/flag-tables.ts
|
|
1278
|
+
import path6 from "path";
|
|
1279
|
+
var FLAGS_WITH_VALUES = {
|
|
1280
|
+
curl: /* @__PURE__ */ new Set([
|
|
1281
|
+
"-H",
|
|
1282
|
+
"--header",
|
|
1283
|
+
"-A",
|
|
1284
|
+
"--user-agent",
|
|
1285
|
+
"-e",
|
|
1286
|
+
"--referer",
|
|
1287
|
+
"-x",
|
|
1288
|
+
"--proxy",
|
|
1289
|
+
"-u",
|
|
1290
|
+
"--user",
|
|
1291
|
+
"-d",
|
|
1292
|
+
"--data",
|
|
1293
|
+
"--data-raw",
|
|
1294
|
+
"--data-binary",
|
|
1295
|
+
"-o",
|
|
1296
|
+
"--output",
|
|
1297
|
+
"-F",
|
|
1298
|
+
"--form",
|
|
1299
|
+
"--connect-to",
|
|
1300
|
+
"--resolve",
|
|
1301
|
+
"--cacert",
|
|
1302
|
+
"--cert",
|
|
1303
|
+
"--key",
|
|
1304
|
+
"-m",
|
|
1305
|
+
"--max-time"
|
|
1306
|
+
]),
|
|
1307
|
+
wget: /* @__PURE__ */ new Set([
|
|
1308
|
+
"-O",
|
|
1309
|
+
"--output-document",
|
|
1310
|
+
"-P",
|
|
1311
|
+
"--directory-prefix",
|
|
1312
|
+
"-U",
|
|
1313
|
+
"--user-agent",
|
|
1314
|
+
"-e",
|
|
1315
|
+
"--execute",
|
|
1316
|
+
"--proxy",
|
|
1317
|
+
"--ca-certificate"
|
|
1318
|
+
]),
|
|
1319
|
+
nc: /* @__PURE__ */ new Set(["-x", "-p", "-s", "-w", "-W", "-I", "-O"]),
|
|
1320
|
+
ncat: /* @__PURE__ */ new Set(["-x", "-p", "-s", "--proxy", "--proxy-auth", "-w", "--wait"]),
|
|
1321
|
+
netcat: /* @__PURE__ */ new Set(["-x", "-p", "-s", "-w"]),
|
|
1322
|
+
ssh: /* @__PURE__ */ new Set([
|
|
1323
|
+
"-i",
|
|
1324
|
+
"-l",
|
|
1325
|
+
"-p",
|
|
1326
|
+
"-o",
|
|
1327
|
+
"-E",
|
|
1328
|
+
"-F",
|
|
1329
|
+
"-J",
|
|
1330
|
+
"-L",
|
|
1331
|
+
"-R",
|
|
1332
|
+
"-W",
|
|
1333
|
+
"-b",
|
|
1334
|
+
"-c",
|
|
1335
|
+
"-D",
|
|
1336
|
+
"-e",
|
|
1337
|
+
"-I",
|
|
1338
|
+
"-S"
|
|
1339
|
+
]),
|
|
1340
|
+
scp: /* @__PURE__ */ new Set(["-i", "-o", "-P", "-S"]),
|
|
1341
|
+
rsync: /* @__PURE__ */ new Set(["-e", "--rsh", "--rsync-path", "--password-file", "--log-file"]),
|
|
1342
|
+
socat: /* @__PURE__ */ new Set([])
|
|
1343
|
+
// socat uses address syntax, not flags — no value-flags
|
|
1344
|
+
};
|
|
1345
|
+
function extractPositionalArgs(tokens, binary) {
|
|
1346
|
+
const binaryName = path6.basename(binary).replace(/\.exe$/i, "");
|
|
1347
|
+
const flagsWithValues = FLAGS_WITH_VALUES[binaryName] ?? /* @__PURE__ */ new Set();
|
|
1348
|
+
const positional = [];
|
|
1349
|
+
let skipNext = false;
|
|
1350
|
+
for (const token of tokens) {
|
|
1351
|
+
if (skipNext) {
|
|
1352
|
+
skipNext = false;
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
if (token.startsWith("--") && token.includes("=")) continue;
|
|
1356
|
+
if (token.startsWith("-") && token.length === 2 && flagsWithValues.has(token)) {
|
|
1357
|
+
skipNext = true;
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
if (token.startsWith("--") && flagsWithValues.has(token)) {
|
|
1361
|
+
skipNext = true;
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
const shortFlag = token.slice(0, 2);
|
|
1365
|
+
if (token.startsWith("-") && token.length > 2 && flagsWithValues.has(shortFlag)) continue;
|
|
1366
|
+
if (token.startsWith("-")) continue;
|
|
1367
|
+
if (token.startsWith("@")) continue;
|
|
1368
|
+
positional.push(token);
|
|
1369
|
+
}
|
|
1370
|
+
return positional;
|
|
1371
|
+
}
|
|
1372
|
+
function extractNetworkTargets(tokens, binary) {
|
|
1373
|
+
return extractPositionalArgs(tokens, binary).map((t) => t.includes("@") ? t.split("@")[1] : t).map((t) => {
|
|
1374
|
+
const colonIdx = t.indexOf(":");
|
|
1375
|
+
if (colonIdx === -1) return t;
|
|
1376
|
+
const afterColon = t.slice(colonIdx + 1);
|
|
1377
|
+
if (/^\d+$/.test(afterColon)) return t.slice(0, colonIdx);
|
|
1378
|
+
return t;
|
|
1379
|
+
}).filter(Boolean);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// src/policy/ssh-parser.ts
|
|
1383
|
+
function tokenize(cmd) {
|
|
1384
|
+
const tokens = [];
|
|
1385
|
+
let current = "";
|
|
1386
|
+
let inSingle = false;
|
|
1387
|
+
let inDouble = false;
|
|
1388
|
+
for (const ch of cmd) {
|
|
1389
|
+
if (ch === "'" && !inDouble) {
|
|
1390
|
+
inSingle = !inSingle;
|
|
1391
|
+
} else if (ch === '"' && !inSingle) {
|
|
1392
|
+
inDouble = !inDouble;
|
|
1393
|
+
} else if ((ch === " " || ch === " ") && !inSingle && !inDouble) {
|
|
1394
|
+
if (current) {
|
|
1395
|
+
tokens.push(current);
|
|
1396
|
+
current = "";
|
|
1397
|
+
}
|
|
1398
|
+
} else {
|
|
1399
|
+
current += ch;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
if (current) tokens.push(current);
|
|
1403
|
+
return tokens;
|
|
1404
|
+
}
|
|
1405
|
+
function parseHost(raw) {
|
|
1406
|
+
return raw.split("@").pop().split(":")[0];
|
|
1407
|
+
}
|
|
1408
|
+
function extractAllSshHosts(tokens) {
|
|
1409
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
1410
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1411
|
+
const t = tokens[i];
|
|
1412
|
+
if (t === "-J" && tokens[i + 1]) {
|
|
1413
|
+
for (const hop of tokens[++i].split(",")) {
|
|
1414
|
+
const h = parseHost(hop);
|
|
1415
|
+
if (h) hosts.add(h);
|
|
1416
|
+
}
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
if (t === "-o" && tokens[i + 1]?.toLowerCase().startsWith("proxyjump=")) {
|
|
1420
|
+
const val = tokens[++i].split("=").slice(1).join("=");
|
|
1421
|
+
for (const hop of val.split(",")) {
|
|
1422
|
+
const h = parseHost(hop);
|
|
1423
|
+
if (h) hosts.add(h);
|
|
1424
|
+
}
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
if (t === "-o" && tokens[i + 1]?.toLowerCase().startsWith("proxycommand=")) {
|
|
1428
|
+
const raw = tokens[++i].split("=").slice(1).join("=").replace(/^['"]|['"]$/g, "");
|
|
1429
|
+
const subTokens = tokenize(raw);
|
|
1430
|
+
const binary = subTokens[0] ?? "";
|
|
1431
|
+
extractNetworkTargets(subTokens.slice(1), binary).forEach((h) => hosts.add(h));
|
|
1432
|
+
extractAllSshHosts(subTokens.slice(1)).forEach((h) => hosts.add(h));
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
if (!t.startsWith("-")) {
|
|
1436
|
+
const h = parseHost(t);
|
|
1437
|
+
if (h) hosts.add(h);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return [...hosts].filter(Boolean);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1034
1443
|
// src/policy/index.ts
|
|
1035
|
-
function
|
|
1444
|
+
function tokenize2(toolName) {
|
|
1036
1445
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
1037
1446
|
}
|
|
1038
1447
|
function matchesPattern(text, patterns) {
|
|
@@ -1045,9 +1454,9 @@ function matchesPattern(text, patterns) {
|
|
|
1045
1454
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1046
1455
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1047
1456
|
}
|
|
1048
|
-
function getNestedValue(obj,
|
|
1457
|
+
function getNestedValue(obj, path13) {
|
|
1049
1458
|
if (!obj || typeof obj !== "object") return null;
|
|
1050
|
-
return
|
|
1459
|
+
return path13.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1051
1460
|
}
|
|
1052
1461
|
function evaluateSmartConditions(args, rule) {
|
|
1053
1462
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -1171,7 +1580,7 @@ async function analyzeShellCommand(command) {
|
|
|
1171
1580
|
}
|
|
1172
1581
|
return { actions, paths, allTokens };
|
|
1173
1582
|
}
|
|
1174
|
-
async function evaluatePolicy(toolName, args, agent) {
|
|
1583
|
+
async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
1175
1584
|
const config = getConfig();
|
|
1176
1585
|
if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
|
|
1177
1586
|
if (config.policy.smartRules.length > 0) {
|
|
@@ -1201,11 +1610,55 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
1201
1610
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
1202
1611
|
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
1203
1612
|
}
|
|
1613
|
+
const pipeAnalysis = analyzePipeChain(shellCommand);
|
|
1614
|
+
if (pipeAnalysis.isPipeline) {
|
|
1615
|
+
if (pipeAnalysis.risk === "critical") {
|
|
1616
|
+
return {
|
|
1617
|
+
decision: "block",
|
|
1618
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1619
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
|
|
1620
|
+
tier: 3
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
if (pipeAnalysis.risk === "high") {
|
|
1624
|
+
return {
|
|
1625
|
+
decision: "review",
|
|
1626
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1627
|
+
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
|
|
1628
|
+
tier: 3
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
const firstToken = analyzed.actions[0] ?? "";
|
|
1633
|
+
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
1634
|
+
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
1635
|
+
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
1636
|
+
allTokens.push(...sshHosts);
|
|
1637
|
+
}
|
|
1638
|
+
if (firstToken && path7.posix.isAbsolute(firstToken)) {
|
|
1639
|
+
const prov = checkProvenance(firstToken, cwd);
|
|
1640
|
+
if (prov.trustLevel === "suspect") {
|
|
1641
|
+
return {
|
|
1642
|
+
decision: config.settings.mode === "strict" ? "block" : "review",
|
|
1643
|
+
blockedByLabel: "Node9: Suspect Binary",
|
|
1644
|
+
reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
|
|
1645
|
+
tier: 3
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
if (prov.trustLevel === "unknown" && config.settings.mode === "strict") {
|
|
1649
|
+
return {
|
|
1650
|
+
decision: "review",
|
|
1651
|
+
blockedByLabel: "Node9: Unknown Binary (strict mode)",
|
|
1652
|
+
reason: `Binary "${firstToken}" \u2014 ${prov.reason}`,
|
|
1653
|
+
tier: 3
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1204
1657
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
1205
1658
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1206
1659
|
}
|
|
1207
1660
|
} else {
|
|
1208
|
-
allTokens =
|
|
1661
|
+
allTokens = tokenize2(toolName);
|
|
1209
1662
|
if (args && typeof args === "object") {
|
|
1210
1663
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
1211
1664
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
@@ -1283,18 +1736,18 @@ function isIgnoredTool(toolName) {
|
|
|
1283
1736
|
}
|
|
1284
1737
|
|
|
1285
1738
|
// src/auth/state.ts
|
|
1286
|
-
import
|
|
1287
|
-
import
|
|
1288
|
-
import
|
|
1289
|
-
var PAUSED_FILE =
|
|
1290
|
-
var TRUST_FILE =
|
|
1739
|
+
import fs6 from "fs";
|
|
1740
|
+
import path8 from "path";
|
|
1741
|
+
import os5 from "os";
|
|
1742
|
+
var PAUSED_FILE = path8.join(os5.homedir(), ".node9", "PAUSED");
|
|
1743
|
+
var TRUST_FILE = path8.join(os5.homedir(), ".node9", "trust.json");
|
|
1291
1744
|
function checkPause() {
|
|
1292
1745
|
try {
|
|
1293
|
-
if (!
|
|
1294
|
-
const state = JSON.parse(
|
|
1746
|
+
if (!fs6.existsSync(PAUSED_FILE)) return { paused: false };
|
|
1747
|
+
const state = JSON.parse(fs6.readFileSync(PAUSED_FILE, "utf-8"));
|
|
1295
1748
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
1296
1749
|
try {
|
|
1297
|
-
|
|
1750
|
+
fs6.unlinkSync(PAUSED_FILE);
|
|
1298
1751
|
} catch {
|
|
1299
1752
|
}
|
|
1300
1753
|
return { paused: false };
|
|
@@ -1305,20 +1758,20 @@ function checkPause() {
|
|
|
1305
1758
|
}
|
|
1306
1759
|
}
|
|
1307
1760
|
function atomicWriteSync(filePath, data, options) {
|
|
1308
|
-
const dir =
|
|
1309
|
-
if (!
|
|
1310
|
-
const tmpPath = `${filePath}.${
|
|
1311
|
-
|
|
1312
|
-
|
|
1761
|
+
const dir = path8.dirname(filePath);
|
|
1762
|
+
if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
|
|
1763
|
+
const tmpPath = `${filePath}.${os5.hostname()}.${process.pid}.tmp`;
|
|
1764
|
+
fs6.writeFileSync(tmpPath, data, options);
|
|
1765
|
+
fs6.renameSync(tmpPath, filePath);
|
|
1313
1766
|
}
|
|
1314
1767
|
function getActiveTrustSession(toolName) {
|
|
1315
1768
|
try {
|
|
1316
|
-
if (!
|
|
1317
|
-
const trust = JSON.parse(
|
|
1769
|
+
if (!fs6.existsSync(TRUST_FILE)) return false;
|
|
1770
|
+
const trust = JSON.parse(fs6.readFileSync(TRUST_FILE, "utf-8"));
|
|
1318
1771
|
const now = Date.now();
|
|
1319
1772
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
1320
1773
|
if (active.length !== trust.entries.length) {
|
|
1321
|
-
|
|
1774
|
+
fs6.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
1322
1775
|
}
|
|
1323
1776
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
1324
1777
|
} catch {
|
|
@@ -1329,8 +1782,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
1329
1782
|
try {
|
|
1330
1783
|
let trust = { entries: [] };
|
|
1331
1784
|
try {
|
|
1332
|
-
if (
|
|
1333
|
-
trust = JSON.parse(
|
|
1785
|
+
if (fs6.existsSync(TRUST_FILE)) {
|
|
1786
|
+
trust = JSON.parse(fs6.readFileSync(TRUST_FILE, "utf-8"));
|
|
1334
1787
|
}
|
|
1335
1788
|
} catch {
|
|
1336
1789
|
}
|
|
@@ -1346,9 +1799,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
1346
1799
|
}
|
|
1347
1800
|
function getPersistentDecision(toolName) {
|
|
1348
1801
|
try {
|
|
1349
|
-
const file =
|
|
1350
|
-
if (!
|
|
1351
|
-
const decisions = JSON.parse(
|
|
1802
|
+
const file = path8.join(os5.homedir(), ".node9", "decisions.json");
|
|
1803
|
+
if (!fs6.existsSync(file)) return null;
|
|
1804
|
+
const decisions = JSON.parse(fs6.readFileSync(file, "utf-8"));
|
|
1352
1805
|
const d = decisions[toolName];
|
|
1353
1806
|
if (d === "allow" || d === "deny") return d;
|
|
1354
1807
|
} catch {
|
|
@@ -1357,17 +1810,17 @@ function getPersistentDecision(toolName) {
|
|
|
1357
1810
|
}
|
|
1358
1811
|
|
|
1359
1812
|
// src/auth/daemon.ts
|
|
1360
|
-
import
|
|
1361
|
-
import
|
|
1362
|
-
import
|
|
1813
|
+
import fs7 from "fs";
|
|
1814
|
+
import path9 from "path";
|
|
1815
|
+
import os6 from "os";
|
|
1363
1816
|
import { spawnSync } from "child_process";
|
|
1364
1817
|
var DAEMON_PORT = 7391;
|
|
1365
1818
|
var DAEMON_HOST = "127.0.0.1";
|
|
1366
1819
|
function getInternalToken() {
|
|
1367
1820
|
try {
|
|
1368
|
-
const pidFile =
|
|
1369
|
-
if (!
|
|
1370
|
-
const data = JSON.parse(
|
|
1821
|
+
const pidFile = path9.join(os6.homedir(), ".node9", "daemon.pid");
|
|
1822
|
+
if (!fs7.existsSync(pidFile)) return null;
|
|
1823
|
+
const data = JSON.parse(fs7.readFileSync(pidFile, "utf-8"));
|
|
1371
1824
|
process.kill(data.pid, 0);
|
|
1372
1825
|
return data.internalToken ?? null;
|
|
1373
1826
|
} catch {
|
|
@@ -1375,10 +1828,10 @@ function getInternalToken() {
|
|
|
1375
1828
|
}
|
|
1376
1829
|
}
|
|
1377
1830
|
function isDaemonRunning() {
|
|
1378
|
-
const pidFile =
|
|
1379
|
-
if (
|
|
1831
|
+
const pidFile = path9.join(os6.homedir(), ".node9", "daemon.pid");
|
|
1832
|
+
if (fs7.existsSync(pidFile)) {
|
|
1380
1833
|
try {
|
|
1381
|
-
const { pid, port } = JSON.parse(
|
|
1834
|
+
const { pid, port } = JSON.parse(fs7.readFileSync(pidFile, "utf-8"));
|
|
1382
1835
|
if (port !== DAEMON_PORT) return false;
|
|
1383
1836
|
process.kill(pid, 0);
|
|
1384
1837
|
return true;
|
|
@@ -1474,16 +1927,16 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1474
1927
|
|
|
1475
1928
|
// src/auth/orchestrator.ts
|
|
1476
1929
|
import net from "net";
|
|
1477
|
-
import
|
|
1478
|
-
import
|
|
1930
|
+
import path12 from "path";
|
|
1931
|
+
import os8 from "os";
|
|
1479
1932
|
import { randomUUID } from "crypto";
|
|
1480
1933
|
|
|
1481
1934
|
// src/ui/native.ts
|
|
1482
1935
|
import { spawn } from "child_process";
|
|
1483
|
-
import
|
|
1936
|
+
import path11 from "path";
|
|
1484
1937
|
|
|
1485
1938
|
// src/context-sniper.ts
|
|
1486
|
-
import
|
|
1939
|
+
import path10 from "path";
|
|
1487
1940
|
function smartTruncate(str, maxLen = 500) {
|
|
1488
1941
|
if (str.length <= maxLen) return str;
|
|
1489
1942
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -1551,7 +2004,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
1551
2004
|
intent = "EDIT";
|
|
1552
2005
|
if (obj.file_path) {
|
|
1553
2006
|
editFilePath = String(obj.file_path);
|
|
1554
|
-
editFileName =
|
|
2007
|
+
editFileName = path10.basename(editFilePath);
|
|
1555
2008
|
}
|
|
1556
2009
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
1557
2010
|
contextSnippet = result.snippet;
|
|
@@ -1606,7 +2059,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
1606
2059
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1607
2060
|
const obj = parsed;
|
|
1608
2061
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
1609
|
-
const file = obj.file_path ?
|
|
2062
|
+
const file = obj.file_path ? path11.basename(String(obj.file_path)) : "file";
|
|
1610
2063
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
1611
2064
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
1612
2065
|
return {
|
|
@@ -1781,8 +2234,8 @@ end run`;
|
|
|
1781
2234
|
}
|
|
1782
2235
|
|
|
1783
2236
|
// src/auth/cloud.ts
|
|
1784
|
-
import
|
|
1785
|
-
import
|
|
2237
|
+
import fs8 from "fs";
|
|
2238
|
+
import os7 from "os";
|
|
1786
2239
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1787
2240
|
return fetch(`${creds.apiUrl}/audit`, {
|
|
1788
2241
|
method: "POST",
|
|
@@ -1794,9 +2247,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1794
2247
|
context: {
|
|
1795
2248
|
agent: meta?.agent,
|
|
1796
2249
|
mcpServer: meta?.mcpServer,
|
|
1797
|
-
hostname:
|
|
2250
|
+
hostname: os7.hostname(),
|
|
1798
2251
|
cwd: process.cwd(),
|
|
1799
|
-
platform:
|
|
2252
|
+
platform: os7.platform()
|
|
1800
2253
|
}
|
|
1801
2254
|
}),
|
|
1802
2255
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -1817,9 +2270,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
1817
2270
|
context: {
|
|
1818
2271
|
agent: meta?.agent,
|
|
1819
2272
|
mcpServer: meta?.mcpServer,
|
|
1820
|
-
hostname:
|
|
2273
|
+
hostname: os7.hostname(),
|
|
1821
2274
|
cwd: process.cwd(),
|
|
1822
|
-
platform:
|
|
2275
|
+
platform: os7.platform()
|
|
1823
2276
|
},
|
|
1824
2277
|
...riskMetadata && { riskMetadata }
|
|
1825
2278
|
}),
|
|
@@ -1875,14 +2328,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
1875
2328
|
});
|
|
1876
2329
|
clearTimeout(timer);
|
|
1877
2330
|
if (!res.ok) {
|
|
1878
|
-
|
|
2331
|
+
fs8.appendFileSync(
|
|
1879
2332
|
HOOK_DEBUG_LOG,
|
|
1880
2333
|
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
1881
2334
|
`
|
|
1882
2335
|
);
|
|
1883
2336
|
}
|
|
1884
2337
|
} catch (err) {
|
|
1885
|
-
|
|
2338
|
+
fs8.appendFileSync(
|
|
1886
2339
|
HOOK_DEBUG_LOG,
|
|
1887
2340
|
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
1888
2341
|
`
|
|
@@ -1891,7 +2344,7 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
1891
2344
|
}
|
|
1892
2345
|
|
|
1893
2346
|
// src/auth/orchestrator.ts
|
|
1894
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
2347
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path12.join(os8.tmpdir(), "node9-activity.sock");
|
|
1895
2348
|
function notifyActivity(data) {
|
|
1896
2349
|
return new Promise((resolve) => {
|
|
1897
2350
|
try {
|
|
@@ -1973,7 +2426,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
1973
2426
|
}
|
|
1974
2427
|
if (config.settings.mode === "audit") {
|
|
1975
2428
|
if (!isIgnoredTool(toolName)) {
|
|
1976
|
-
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
2429
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
1977
2430
|
if (policyResult.decision === "review") {
|
|
1978
2431
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
1979
2432
|
if (approvers.cloud && creds?.apiKey) {
|