@node9/proxy 1.3.1 → 1.4.0
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/cli.js +979 -365
- package/dist/cli.mjs +965 -352
- package/dist/index.js +548 -53
- package/dist/index.mjs +548 -53
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -191,8 +191,8 @@ function sanitizeConfig(raw) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
const lines = result.error.issues.map((issue) => {
|
|
194
|
-
const
|
|
195
|
-
return ` \u2022 ${
|
|
194
|
+
const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
195
|
+
return ` \u2022 ${path14}: ${issue.message}`;
|
|
196
196
|
});
|
|
197
197
|
return {
|
|
198
198
|
sanitized,
|
|
@@ -922,6 +922,7 @@ function getCompiledRegex(pattern, flags = "") {
|
|
|
922
922
|
}
|
|
923
923
|
|
|
924
924
|
// src/policy/index.ts
|
|
925
|
+
var import_path8 = __toESM(require("path"));
|
|
925
926
|
var import_picomatch = __toESM(require("picomatch"));
|
|
926
927
|
var import_sh_syntax = require("sh-syntax");
|
|
927
928
|
|
|
@@ -1067,8 +1068,447 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1067
1068
|
return null;
|
|
1068
1069
|
}
|
|
1069
1070
|
|
|
1071
|
+
// src/utils/provenance.ts
|
|
1072
|
+
var import_fs5 = __toESM(require("fs"));
|
|
1073
|
+
var import_path5 = __toESM(require("path"));
|
|
1074
|
+
var import_os4 = __toESM(require("os"));
|
|
1075
|
+
var SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
|
|
1076
|
+
var MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
|
|
1077
|
+
var USER_PREFIXES = [
|
|
1078
|
+
import_path5.default.join(import_os4.default.homedir(), "bin"),
|
|
1079
|
+
import_path5.default.join(import_os4.default.homedir(), ".local", "bin"),
|
|
1080
|
+
import_path5.default.join(import_os4.default.homedir(), ".cargo", "bin"),
|
|
1081
|
+
import_path5.default.join(import_os4.default.homedir(), ".npm-global", "bin"),
|
|
1082
|
+
import_path5.default.join(import_os4.default.homedir(), ".volta", "bin")
|
|
1083
|
+
];
|
|
1084
|
+
var SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
|
|
1085
|
+
function findInPath(cmd) {
|
|
1086
|
+
if (import_path5.default.posix.isAbsolute(cmd)) return cmd;
|
|
1087
|
+
const pathEnv = process.env.PATH ?? "";
|
|
1088
|
+
for (const dir of pathEnv.split(import_path5.default.delimiter)) {
|
|
1089
|
+
if (!dir) continue;
|
|
1090
|
+
const full = import_path5.default.join(dir, cmd);
|
|
1091
|
+
try {
|
|
1092
|
+
import_fs5.default.accessSync(full, import_fs5.default.constants.X_OK);
|
|
1093
|
+
return full;
|
|
1094
|
+
} catch {
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
function _classifyPath(resolved, cwd) {
|
|
1100
|
+
if (cwd && resolved.startsWith(cwd + "/")) {
|
|
1101
|
+
return { trustLevel: "user", reason: "binary in project directory" };
|
|
1102
|
+
}
|
|
1103
|
+
const osTmp = import_os4.default.tmpdir();
|
|
1104
|
+
const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
|
|
1105
|
+
if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
1106
|
+
return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
|
|
1107
|
+
}
|
|
1108
|
+
if (SYSTEM_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
1109
|
+
return { trustLevel: "system", reason: "" };
|
|
1110
|
+
}
|
|
1111
|
+
if (MANAGED_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
1112
|
+
return { trustLevel: "managed", reason: "" };
|
|
1113
|
+
}
|
|
1114
|
+
if (USER_PREFIXES.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
1115
|
+
return { trustLevel: "user", reason: "" };
|
|
1116
|
+
}
|
|
1117
|
+
return { trustLevel: "unknown", reason: "binary in unrecognized location" };
|
|
1118
|
+
}
|
|
1119
|
+
function checkProvenance(cmd, cwd) {
|
|
1120
|
+
const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
|
|
1121
|
+
if (import_path5.default.posix.isAbsolute(bare)) {
|
|
1122
|
+
const early = _classifyPath(bare, cwd);
|
|
1123
|
+
if (early.trustLevel === "suspect") {
|
|
1124
|
+
return { resolvedPath: bare, ...early };
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
let resolved;
|
|
1128
|
+
try {
|
|
1129
|
+
const found = findInPath(bare);
|
|
1130
|
+
if (!found) {
|
|
1131
|
+
return {
|
|
1132
|
+
resolvedPath: cmd,
|
|
1133
|
+
trustLevel: "unknown",
|
|
1134
|
+
reason: "binary not found in PATH"
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
resolved = import_fs5.default.realpathSync(found);
|
|
1138
|
+
} catch {
|
|
1139
|
+
return {
|
|
1140
|
+
resolvedPath: cmd,
|
|
1141
|
+
trustLevel: "unknown",
|
|
1142
|
+
reason: "binary not found in PATH"
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
try {
|
|
1146
|
+
const stat = import_fs5.default.statSync(resolved);
|
|
1147
|
+
if (stat.mode & 2) {
|
|
1148
|
+
return {
|
|
1149
|
+
resolvedPath: resolved,
|
|
1150
|
+
trustLevel: "suspect",
|
|
1151
|
+
reason: "binary is world-writable"
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
} catch {
|
|
1155
|
+
return {
|
|
1156
|
+
resolvedPath: resolved,
|
|
1157
|
+
trustLevel: "unknown",
|
|
1158
|
+
reason: "could not stat binary"
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
const classify = _classifyPath(resolved, cwd);
|
|
1162
|
+
return { resolvedPath: resolved, ...classify };
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/policy/pipe-chain.ts
|
|
1166
|
+
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1167
|
+
"cat",
|
|
1168
|
+
"head",
|
|
1169
|
+
"tail",
|
|
1170
|
+
"grep",
|
|
1171
|
+
"awk",
|
|
1172
|
+
"sed",
|
|
1173
|
+
"cut",
|
|
1174
|
+
"sort",
|
|
1175
|
+
"tee",
|
|
1176
|
+
"less",
|
|
1177
|
+
"more",
|
|
1178
|
+
"strings",
|
|
1179
|
+
"xxd"
|
|
1180
|
+
]);
|
|
1181
|
+
var SINK_COMMANDS = /* @__PURE__ */ new Set([
|
|
1182
|
+
"curl",
|
|
1183
|
+
"wget",
|
|
1184
|
+
"nc",
|
|
1185
|
+
"ncat",
|
|
1186
|
+
"netcat",
|
|
1187
|
+
"ssh",
|
|
1188
|
+
"scp",
|
|
1189
|
+
"rsync",
|
|
1190
|
+
"socat",
|
|
1191
|
+
"ftp",
|
|
1192
|
+
"sftp",
|
|
1193
|
+
"telnet"
|
|
1194
|
+
]);
|
|
1195
|
+
var OBFUSCATORS = /* @__PURE__ */ new Set([
|
|
1196
|
+
"base64",
|
|
1197
|
+
"gzip",
|
|
1198
|
+
"gunzip",
|
|
1199
|
+
"bzip2",
|
|
1200
|
+
"xz",
|
|
1201
|
+
"zstd",
|
|
1202
|
+
"openssl",
|
|
1203
|
+
"gpg",
|
|
1204
|
+
"python",
|
|
1205
|
+
"python3",
|
|
1206
|
+
"perl",
|
|
1207
|
+
"ruby",
|
|
1208
|
+
"node"
|
|
1209
|
+
]);
|
|
1210
|
+
var SENSITIVE_PATTERNS = [
|
|
1211
|
+
/(?:^|\/)\.env(?:\.|$)/i,
|
|
1212
|
+
// .env, .env.local, .env.production
|
|
1213
|
+
/id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
|
|
1214
|
+
// SSH private keys
|
|
1215
|
+
/\.pem$|\.key$|\.p12$|\.pfx$/i,
|
|
1216
|
+
// certificate files
|
|
1217
|
+
/(?:^|\/)\.ssh\//i,
|
|
1218
|
+
// ~/.ssh/ directory
|
|
1219
|
+
/(?:^|\/)\.aws\/credentials/i,
|
|
1220
|
+
// AWS credentials
|
|
1221
|
+
/(?:^|\/)\.netrc$/i,
|
|
1222
|
+
// netrc (stores HTTP credentials)
|
|
1223
|
+
/(?:^|\/)(passwd|shadow|sudoers)$/i,
|
|
1224
|
+
// /etc/passwd, /etc/shadow
|
|
1225
|
+
/(?:^|\/)credentials(?:\.json)?$/i
|
|
1226
|
+
// generic credentials files
|
|
1227
|
+
];
|
|
1228
|
+
function isSensitivePath(p) {
|
|
1229
|
+
return SENSITIVE_PATTERNS.some((re) => re.test(p));
|
|
1230
|
+
}
|
|
1231
|
+
function splitOnPipe(cmd) {
|
|
1232
|
+
const segments = [];
|
|
1233
|
+
let current = "";
|
|
1234
|
+
let inSingle = false;
|
|
1235
|
+
let inDouble = false;
|
|
1236
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
1237
|
+
const ch = cmd[i];
|
|
1238
|
+
if (ch === "'" && !inDouble) {
|
|
1239
|
+
inSingle = !inSingle;
|
|
1240
|
+
current += ch;
|
|
1241
|
+
} else if (ch === '"' && !inSingle) {
|
|
1242
|
+
inDouble = !inDouble;
|
|
1243
|
+
current += ch;
|
|
1244
|
+
} else if (ch === "|" && !inSingle && !inDouble && cmd[i + 1] !== "|" && (i === 0 || cmd[i - 1] !== "|")) {
|
|
1245
|
+
segments.push(current.trim());
|
|
1246
|
+
current = "";
|
|
1247
|
+
} else {
|
|
1248
|
+
current += ch;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (current.trim()) segments.push(current.trim());
|
|
1252
|
+
return segments.filter(Boolean);
|
|
1253
|
+
}
|
|
1254
|
+
function positionalTokens(segment) {
|
|
1255
|
+
return segment.split(/\s+/).slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
|
|
1256
|
+
}
|
|
1257
|
+
function analyzePipeChain(command) {
|
|
1258
|
+
const segments = splitOnPipe(command);
|
|
1259
|
+
if (segments.length < 2) {
|
|
1260
|
+
return {
|
|
1261
|
+
isPipeline: false,
|
|
1262
|
+
hasSensitiveSource: false,
|
|
1263
|
+
hasExternalSink: false,
|
|
1264
|
+
hasObfuscation: false,
|
|
1265
|
+
sourceFiles: [],
|
|
1266
|
+
sinkTargets: [],
|
|
1267
|
+
risk: "none"
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
const sourceFiles = [];
|
|
1271
|
+
const sinkTargets = [];
|
|
1272
|
+
let hasSensitiveSource = false;
|
|
1273
|
+
let hasExternalSink = false;
|
|
1274
|
+
let hasObfuscation = false;
|
|
1275
|
+
for (const segment of segments) {
|
|
1276
|
+
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
1277
|
+
if (tokens.length === 0) continue;
|
|
1278
|
+
const binary = tokens[0].toLowerCase();
|
|
1279
|
+
const args = positionalTokens(segment);
|
|
1280
|
+
if (SOURCE_COMMANDS.has(binary)) {
|
|
1281
|
+
sourceFiles.push(...args);
|
|
1282
|
+
if (args.some(isSensitivePath)) hasSensitiveSource = true;
|
|
1283
|
+
}
|
|
1284
|
+
if (OBFUSCATORS.has(binary)) hasObfuscation = true;
|
|
1285
|
+
if (SINK_COMMANDS.has(binary)) {
|
|
1286
|
+
const targets = args.filter(
|
|
1287
|
+
(a) => a.includes(".") || a.includes("://") || /^\d+\.\d+/.test(a)
|
|
1288
|
+
);
|
|
1289
|
+
sinkTargets.push(...targets);
|
|
1290
|
+
if (targets.length > 0) hasExternalSink = true;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const fullCmd = command.toLowerCase();
|
|
1294
|
+
if (!hasSensitiveSource) {
|
|
1295
|
+
const redirMatch = fullCmd.match(/<\s*(\S+)/);
|
|
1296
|
+
if (redirMatch && isSensitivePath(redirMatch[1])) {
|
|
1297
|
+
hasSensitiveSource = true;
|
|
1298
|
+
sourceFiles.push(redirMatch[1]);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
const risk = hasSensitiveSource && hasExternalSink && hasObfuscation ? "critical" : hasSensitiveSource && hasExternalSink ? "high" : hasExternalSink ? "medium" : "none";
|
|
1302
|
+
return {
|
|
1303
|
+
isPipeline: true,
|
|
1304
|
+
hasSensitiveSource,
|
|
1305
|
+
hasExternalSink,
|
|
1306
|
+
hasObfuscation,
|
|
1307
|
+
sourceFiles,
|
|
1308
|
+
sinkTargets,
|
|
1309
|
+
risk
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/policy/flag-tables.ts
|
|
1314
|
+
var import_path6 = __toESM(require("path"));
|
|
1315
|
+
var FLAGS_WITH_VALUES = {
|
|
1316
|
+
curl: /* @__PURE__ */ new Set([
|
|
1317
|
+
"-H",
|
|
1318
|
+
"--header",
|
|
1319
|
+
"-A",
|
|
1320
|
+
"--user-agent",
|
|
1321
|
+
"-e",
|
|
1322
|
+
"--referer",
|
|
1323
|
+
"-x",
|
|
1324
|
+
"--proxy",
|
|
1325
|
+
"-u",
|
|
1326
|
+
"--user",
|
|
1327
|
+
"-d",
|
|
1328
|
+
"--data",
|
|
1329
|
+
"--data-raw",
|
|
1330
|
+
"--data-binary",
|
|
1331
|
+
"-o",
|
|
1332
|
+
"--output",
|
|
1333
|
+
"-F",
|
|
1334
|
+
"--form",
|
|
1335
|
+
"--connect-to",
|
|
1336
|
+
"--resolve",
|
|
1337
|
+
"--cacert",
|
|
1338
|
+
"--cert",
|
|
1339
|
+
"--key",
|
|
1340
|
+
"-m",
|
|
1341
|
+
"--max-time"
|
|
1342
|
+
]),
|
|
1343
|
+
wget: /* @__PURE__ */ new Set([
|
|
1344
|
+
"-O",
|
|
1345
|
+
"--output-document",
|
|
1346
|
+
"-P",
|
|
1347
|
+
"--directory-prefix",
|
|
1348
|
+
"-U",
|
|
1349
|
+
"--user-agent",
|
|
1350
|
+
"-e",
|
|
1351
|
+
"--execute",
|
|
1352
|
+
"--proxy",
|
|
1353
|
+
"--ca-certificate"
|
|
1354
|
+
]),
|
|
1355
|
+
nc: /* @__PURE__ */ new Set(["-x", "-p", "-s", "-w", "-W", "-I", "-O"]),
|
|
1356
|
+
ncat: /* @__PURE__ */ new Set(["-x", "-p", "-s", "--proxy", "--proxy-auth", "-w", "--wait"]),
|
|
1357
|
+
netcat: /* @__PURE__ */ new Set(["-x", "-p", "-s", "-w"]),
|
|
1358
|
+
ssh: /* @__PURE__ */ new Set([
|
|
1359
|
+
"-i",
|
|
1360
|
+
"-l",
|
|
1361
|
+
"-p",
|
|
1362
|
+
"-o",
|
|
1363
|
+
"-E",
|
|
1364
|
+
"-F",
|
|
1365
|
+
"-J",
|
|
1366
|
+
"-L",
|
|
1367
|
+
"-R",
|
|
1368
|
+
"-W",
|
|
1369
|
+
"-b",
|
|
1370
|
+
"-c",
|
|
1371
|
+
"-D",
|
|
1372
|
+
"-e",
|
|
1373
|
+
"-I",
|
|
1374
|
+
"-S"
|
|
1375
|
+
]),
|
|
1376
|
+
scp: /* @__PURE__ */ new Set(["-i", "-o", "-P", "-S"]),
|
|
1377
|
+
rsync: /* @__PURE__ */ new Set(["-e", "--rsh", "--rsync-path", "--password-file", "--log-file"]),
|
|
1378
|
+
socat: /* @__PURE__ */ new Set([])
|
|
1379
|
+
// socat uses address syntax, not flags — no value-flags
|
|
1380
|
+
};
|
|
1381
|
+
function extractPositionalArgs(tokens, binary) {
|
|
1382
|
+
const binaryName = import_path6.default.basename(binary).replace(/\.exe$/i, "");
|
|
1383
|
+
const flagsWithValues = FLAGS_WITH_VALUES[binaryName] ?? /* @__PURE__ */ new Set();
|
|
1384
|
+
const positional = [];
|
|
1385
|
+
let skipNext = false;
|
|
1386
|
+
for (const token of tokens) {
|
|
1387
|
+
if (skipNext) {
|
|
1388
|
+
skipNext = false;
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
if (token.startsWith("--") && token.includes("=")) continue;
|
|
1392
|
+
if (token.startsWith("-") && token.length === 2 && flagsWithValues.has(token)) {
|
|
1393
|
+
skipNext = true;
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
if (token.startsWith("--") && flagsWithValues.has(token)) {
|
|
1397
|
+
skipNext = true;
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
const shortFlag = token.slice(0, 2);
|
|
1401
|
+
if (token.startsWith("-") && token.length > 2 && flagsWithValues.has(shortFlag)) continue;
|
|
1402
|
+
if (token.startsWith("-")) continue;
|
|
1403
|
+
if (token.startsWith("@")) continue;
|
|
1404
|
+
positional.push(token);
|
|
1405
|
+
}
|
|
1406
|
+
return positional;
|
|
1407
|
+
}
|
|
1408
|
+
function extractNetworkTargets(tokens, binary) {
|
|
1409
|
+
return extractPositionalArgs(tokens, binary).map((t) => t.includes("@") ? t.split("@")[1] : t).map((t) => {
|
|
1410
|
+
const colonIdx = t.indexOf(":");
|
|
1411
|
+
if (colonIdx === -1) return t;
|
|
1412
|
+
const afterColon = t.slice(colonIdx + 1);
|
|
1413
|
+
if (/^\d+$/.test(afterColon)) return t.slice(0, colonIdx);
|
|
1414
|
+
return t;
|
|
1415
|
+
}).filter(Boolean);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/policy/ssh-parser.ts
|
|
1419
|
+
function tokenize(cmd) {
|
|
1420
|
+
const tokens = [];
|
|
1421
|
+
let current = "";
|
|
1422
|
+
let inSingle = false;
|
|
1423
|
+
let inDouble = false;
|
|
1424
|
+
for (const ch of cmd) {
|
|
1425
|
+
if (ch === "'" && !inDouble) {
|
|
1426
|
+
inSingle = !inSingle;
|
|
1427
|
+
} else if (ch === '"' && !inSingle) {
|
|
1428
|
+
inDouble = !inDouble;
|
|
1429
|
+
} else if ((ch === " " || ch === " ") && !inSingle && !inDouble) {
|
|
1430
|
+
if (current) {
|
|
1431
|
+
tokens.push(current);
|
|
1432
|
+
current = "";
|
|
1433
|
+
}
|
|
1434
|
+
} else {
|
|
1435
|
+
current += ch;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (current) tokens.push(current);
|
|
1439
|
+
return tokens;
|
|
1440
|
+
}
|
|
1441
|
+
function parseHost(raw) {
|
|
1442
|
+
return raw.split("@").pop().split(":")[0];
|
|
1443
|
+
}
|
|
1444
|
+
function extractAllSshHosts(tokens) {
|
|
1445
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
1446
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1447
|
+
const t = tokens[i];
|
|
1448
|
+
if (t === "-J" && tokens[i + 1]) {
|
|
1449
|
+
for (const hop of tokens[++i].split(",")) {
|
|
1450
|
+
const h = parseHost(hop);
|
|
1451
|
+
if (h) hosts.add(h);
|
|
1452
|
+
}
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
if (t === "-o" && tokens[i + 1]?.toLowerCase().startsWith("proxyjump=")) {
|
|
1456
|
+
const val = tokens[++i].split("=").slice(1).join("=");
|
|
1457
|
+
for (const hop of val.split(",")) {
|
|
1458
|
+
const h = parseHost(hop);
|
|
1459
|
+
if (h) hosts.add(h);
|
|
1460
|
+
}
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (t === "-o" && tokens[i + 1]?.toLowerCase().startsWith("proxycommand=")) {
|
|
1464
|
+
const raw = tokens[++i].split("=").slice(1).join("=").replace(/^['"]|['"]$/g, "");
|
|
1465
|
+
const subTokens = tokenize(raw);
|
|
1466
|
+
const binary = subTokens[0] ?? "";
|
|
1467
|
+
extractNetworkTargets(subTokens.slice(1), binary).forEach((h) => hosts.add(h));
|
|
1468
|
+
extractAllSshHosts(subTokens.slice(1)).forEach((h) => hosts.add(h));
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
if (!t.startsWith("-")) {
|
|
1472
|
+
const h = parseHost(t);
|
|
1473
|
+
if (h) hosts.add(h);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return [...hosts].filter(Boolean);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// src/auth/trusted-hosts.ts
|
|
1480
|
+
var import_fs6 = __toESM(require("fs"));
|
|
1481
|
+
var import_path7 = __toESM(require("path"));
|
|
1482
|
+
var import_os5 = __toESM(require("os"));
|
|
1483
|
+
function getTrustedHostsPath() {
|
|
1484
|
+
return import_path7.default.join(import_os5.default.homedir(), ".node9", "trusted-hosts.json");
|
|
1485
|
+
}
|
|
1486
|
+
function readTrustedHosts() {
|
|
1487
|
+
try {
|
|
1488
|
+
const raw = import_fs6.default.readFileSync(getTrustedHostsPath(), "utf8");
|
|
1489
|
+
const parsed = JSON.parse(raw);
|
|
1490
|
+
return Array.isArray(parsed.hosts) ? parsed.hosts : [];
|
|
1491
|
+
} catch {
|
|
1492
|
+
return [];
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
function normalizeHost(raw) {
|
|
1496
|
+
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
1497
|
+
}
|
|
1498
|
+
function isTrustedHost(host) {
|
|
1499
|
+
const normalized = normalizeHost(host);
|
|
1500
|
+
return readTrustedHosts().some((entry) => {
|
|
1501
|
+
const entryHost = entry.host.toLowerCase();
|
|
1502
|
+
if (entryHost.startsWith("*.")) {
|
|
1503
|
+
const domain = entryHost.slice(2);
|
|
1504
|
+
return normalized === domain || normalized.endsWith("." + domain);
|
|
1505
|
+
}
|
|
1506
|
+
return normalized === entryHost;
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1070
1510
|
// src/policy/index.ts
|
|
1071
|
-
function
|
|
1511
|
+
function tokenize2(toolName) {
|
|
1072
1512
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
1073
1513
|
}
|
|
1074
1514
|
function matchesPattern(text, patterns) {
|
|
@@ -1081,9 +1521,9 @@ function matchesPattern(text, patterns) {
|
|
|
1081
1521
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1082
1522
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1083
1523
|
}
|
|
1084
|
-
function getNestedValue(obj,
|
|
1524
|
+
function getNestedValue(obj, path14) {
|
|
1085
1525
|
if (!obj || typeof obj !== "object") return null;
|
|
1086
|
-
return
|
|
1526
|
+
return path14.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1087
1527
|
}
|
|
1088
1528
|
function evaluateSmartConditions(args, rule) {
|
|
1089
1529
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -1207,7 +1647,7 @@ async function analyzeShellCommand(command) {
|
|
|
1207
1647
|
}
|
|
1208
1648
|
return { actions, paths, allTokens };
|
|
1209
1649
|
}
|
|
1210
|
-
async function evaluatePolicy(toolName, args, agent) {
|
|
1650
|
+
async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
1211
1651
|
const config = getConfig();
|
|
1212
1652
|
if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
|
|
1213
1653
|
if (config.policy.smartRules.length > 0) {
|
|
@@ -1237,11 +1677,66 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
1237
1677
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
1238
1678
|
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
1239
1679
|
}
|
|
1680
|
+
const pipeAnalysis = analyzePipeChain(shellCommand);
|
|
1681
|
+
if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
|
|
1682
|
+
const sinks = pipeAnalysis.sinkTargets;
|
|
1683
|
+
const allTrusted = sinks.length > 0 && sinks.every(isTrustedHost);
|
|
1684
|
+
if (pipeAnalysis.risk === "critical") {
|
|
1685
|
+
if (allTrusted) {
|
|
1686
|
+
return {
|
|
1687
|
+
decision: "review",
|
|
1688
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
1689
|
+
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
1690
|
+
tier: 3
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
return {
|
|
1694
|
+
decision: "block",
|
|
1695
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1696
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1697
|
+
tier: 3
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
if (allTrusted) {
|
|
1701
|
+
return { decision: "allow" };
|
|
1702
|
+
}
|
|
1703
|
+
return {
|
|
1704
|
+
decision: "review",
|
|
1705
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1706
|
+
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1707
|
+
tier: 3
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
const firstToken = analyzed.actions[0] ?? "";
|
|
1711
|
+
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
1712
|
+
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
1713
|
+
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
1714
|
+
allTokens.push(...sshHosts);
|
|
1715
|
+
}
|
|
1716
|
+
if (firstToken && import_path8.default.posix.isAbsolute(firstToken)) {
|
|
1717
|
+
const prov = checkProvenance(firstToken, cwd);
|
|
1718
|
+
if (prov.trustLevel === "suspect") {
|
|
1719
|
+
return {
|
|
1720
|
+
decision: config.settings.mode === "strict" ? "block" : "review",
|
|
1721
|
+
blockedByLabel: "Node9: Suspect Binary",
|
|
1722
|
+
reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
|
|
1723
|
+
tier: 3
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
if (prov.trustLevel === "unknown" && config.settings.mode === "strict") {
|
|
1727
|
+
return {
|
|
1728
|
+
decision: "review",
|
|
1729
|
+
blockedByLabel: "Node9: Unknown Binary (strict mode)",
|
|
1730
|
+
reason: `Binary "${firstToken}" \u2014 ${prov.reason}`,
|
|
1731
|
+
tier: 3
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1240
1735
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
1241
1736
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1242
1737
|
}
|
|
1243
1738
|
} else {
|
|
1244
|
-
allTokens =
|
|
1739
|
+
allTokens = tokenize2(toolName);
|
|
1245
1740
|
if (args && typeof args === "object") {
|
|
1246
1741
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
1247
1742
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
@@ -1319,18 +1814,18 @@ function isIgnoredTool(toolName) {
|
|
|
1319
1814
|
}
|
|
1320
1815
|
|
|
1321
1816
|
// src/auth/state.ts
|
|
1322
|
-
var
|
|
1323
|
-
var
|
|
1324
|
-
var
|
|
1325
|
-
var PAUSED_FILE =
|
|
1326
|
-
var TRUST_FILE =
|
|
1817
|
+
var import_fs7 = __toESM(require("fs"));
|
|
1818
|
+
var import_path9 = __toESM(require("path"));
|
|
1819
|
+
var import_os6 = __toESM(require("os"));
|
|
1820
|
+
var PAUSED_FILE = import_path9.default.join(import_os6.default.homedir(), ".node9", "PAUSED");
|
|
1821
|
+
var TRUST_FILE = import_path9.default.join(import_os6.default.homedir(), ".node9", "trust.json");
|
|
1327
1822
|
function checkPause() {
|
|
1328
1823
|
try {
|
|
1329
|
-
if (!
|
|
1330
|
-
const state = JSON.parse(
|
|
1824
|
+
if (!import_fs7.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
1825
|
+
const state = JSON.parse(import_fs7.default.readFileSync(PAUSED_FILE, "utf-8"));
|
|
1331
1826
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
1332
1827
|
try {
|
|
1333
|
-
|
|
1828
|
+
import_fs7.default.unlinkSync(PAUSED_FILE);
|
|
1334
1829
|
} catch {
|
|
1335
1830
|
}
|
|
1336
1831
|
return { paused: false };
|
|
@@ -1341,20 +1836,20 @@ function checkPause() {
|
|
|
1341
1836
|
}
|
|
1342
1837
|
}
|
|
1343
1838
|
function atomicWriteSync(filePath, data, options) {
|
|
1344
|
-
const dir =
|
|
1345
|
-
if (!
|
|
1346
|
-
const tmpPath = `${filePath}.${
|
|
1347
|
-
|
|
1348
|
-
|
|
1839
|
+
const dir = import_path9.default.dirname(filePath);
|
|
1840
|
+
if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
|
|
1841
|
+
const tmpPath = `${filePath}.${import_os6.default.hostname()}.${process.pid}.tmp`;
|
|
1842
|
+
import_fs7.default.writeFileSync(tmpPath, data, options);
|
|
1843
|
+
import_fs7.default.renameSync(tmpPath, filePath);
|
|
1349
1844
|
}
|
|
1350
1845
|
function getActiveTrustSession(toolName) {
|
|
1351
1846
|
try {
|
|
1352
|
-
if (!
|
|
1353
|
-
const trust = JSON.parse(
|
|
1847
|
+
if (!import_fs7.default.existsSync(TRUST_FILE)) return false;
|
|
1848
|
+
const trust = JSON.parse(import_fs7.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
1354
1849
|
const now = Date.now();
|
|
1355
1850
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
1356
1851
|
if (active.length !== trust.entries.length) {
|
|
1357
|
-
|
|
1852
|
+
import_fs7.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
1358
1853
|
}
|
|
1359
1854
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
1360
1855
|
} catch {
|
|
@@ -1365,8 +1860,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
1365
1860
|
try {
|
|
1366
1861
|
let trust = { entries: [] };
|
|
1367
1862
|
try {
|
|
1368
|
-
if (
|
|
1369
|
-
trust = JSON.parse(
|
|
1863
|
+
if (import_fs7.default.existsSync(TRUST_FILE)) {
|
|
1864
|
+
trust = JSON.parse(import_fs7.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
1370
1865
|
}
|
|
1371
1866
|
} catch {
|
|
1372
1867
|
}
|
|
@@ -1382,9 +1877,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
1382
1877
|
}
|
|
1383
1878
|
function getPersistentDecision(toolName) {
|
|
1384
1879
|
try {
|
|
1385
|
-
const file =
|
|
1386
|
-
if (!
|
|
1387
|
-
const decisions = JSON.parse(
|
|
1880
|
+
const file = import_path9.default.join(import_os6.default.homedir(), ".node9", "decisions.json");
|
|
1881
|
+
if (!import_fs7.default.existsSync(file)) return null;
|
|
1882
|
+
const decisions = JSON.parse(import_fs7.default.readFileSync(file, "utf-8"));
|
|
1388
1883
|
const d = decisions[toolName];
|
|
1389
1884
|
if (d === "allow" || d === "deny") return d;
|
|
1390
1885
|
} catch {
|
|
@@ -1393,17 +1888,17 @@ function getPersistentDecision(toolName) {
|
|
|
1393
1888
|
}
|
|
1394
1889
|
|
|
1395
1890
|
// src/auth/daemon.ts
|
|
1396
|
-
var
|
|
1397
|
-
var
|
|
1398
|
-
var
|
|
1891
|
+
var import_fs8 = __toESM(require("fs"));
|
|
1892
|
+
var import_path10 = __toESM(require("path"));
|
|
1893
|
+
var import_os7 = __toESM(require("os"));
|
|
1399
1894
|
var import_child_process = require("child_process");
|
|
1400
1895
|
var DAEMON_PORT = 7391;
|
|
1401
1896
|
var DAEMON_HOST = "127.0.0.1";
|
|
1402
1897
|
function getInternalToken() {
|
|
1403
1898
|
try {
|
|
1404
|
-
const pidFile =
|
|
1405
|
-
if (!
|
|
1406
|
-
const data = JSON.parse(
|
|
1899
|
+
const pidFile = import_path10.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
|
|
1900
|
+
if (!import_fs8.default.existsSync(pidFile)) return null;
|
|
1901
|
+
const data = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
|
|
1407
1902
|
process.kill(data.pid, 0);
|
|
1408
1903
|
return data.internalToken ?? null;
|
|
1409
1904
|
} catch {
|
|
@@ -1411,10 +1906,10 @@ function getInternalToken() {
|
|
|
1411
1906
|
}
|
|
1412
1907
|
}
|
|
1413
1908
|
function isDaemonRunning() {
|
|
1414
|
-
const pidFile =
|
|
1415
|
-
if (
|
|
1909
|
+
const pidFile = import_path10.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
|
|
1910
|
+
if (import_fs8.default.existsSync(pidFile)) {
|
|
1416
1911
|
try {
|
|
1417
|
-
const { pid, port } = JSON.parse(
|
|
1912
|
+
const { pid, port } = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
|
|
1418
1913
|
if (port !== DAEMON_PORT) return false;
|
|
1419
1914
|
process.kill(pid, 0);
|
|
1420
1915
|
return true;
|
|
@@ -1510,16 +2005,16 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1510
2005
|
|
|
1511
2006
|
// src/auth/orchestrator.ts
|
|
1512
2007
|
var import_net = __toESM(require("net"));
|
|
1513
|
-
var
|
|
1514
|
-
var
|
|
2008
|
+
var import_path13 = __toESM(require("path"));
|
|
2009
|
+
var import_os9 = __toESM(require("os"));
|
|
1515
2010
|
var import_crypto = require("crypto");
|
|
1516
2011
|
|
|
1517
2012
|
// src/ui/native.ts
|
|
1518
2013
|
var import_child_process2 = require("child_process");
|
|
1519
|
-
var
|
|
2014
|
+
var import_path12 = __toESM(require("path"));
|
|
1520
2015
|
|
|
1521
2016
|
// src/context-sniper.ts
|
|
1522
|
-
var
|
|
2017
|
+
var import_path11 = __toESM(require("path"));
|
|
1523
2018
|
function smartTruncate(str, maxLen = 500) {
|
|
1524
2019
|
if (str.length <= maxLen) return str;
|
|
1525
2020
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -1587,7 +2082,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
1587
2082
|
intent = "EDIT";
|
|
1588
2083
|
if (obj.file_path) {
|
|
1589
2084
|
editFilePath = String(obj.file_path);
|
|
1590
|
-
editFileName =
|
|
2085
|
+
editFileName = import_path11.default.basename(editFilePath);
|
|
1591
2086
|
}
|
|
1592
2087
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
1593
2088
|
contextSnippet = result.snippet;
|
|
@@ -1642,7 +2137,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
1642
2137
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1643
2138
|
const obj = parsed;
|
|
1644
2139
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
1645
|
-
const file = obj.file_path ?
|
|
2140
|
+
const file = obj.file_path ? import_path12.default.basename(String(obj.file_path)) : "file";
|
|
1646
2141
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
1647
2142
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
1648
2143
|
return {
|
|
@@ -1817,8 +2312,8 @@ end run`;
|
|
|
1817
2312
|
}
|
|
1818
2313
|
|
|
1819
2314
|
// src/auth/cloud.ts
|
|
1820
|
-
var
|
|
1821
|
-
var
|
|
2315
|
+
var import_fs9 = __toESM(require("fs"));
|
|
2316
|
+
var import_os8 = __toESM(require("os"));
|
|
1822
2317
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1823
2318
|
return fetch(`${creds.apiUrl}/audit`, {
|
|
1824
2319
|
method: "POST",
|
|
@@ -1830,9 +2325,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1830
2325
|
context: {
|
|
1831
2326
|
agent: meta?.agent,
|
|
1832
2327
|
mcpServer: meta?.mcpServer,
|
|
1833
|
-
hostname:
|
|
2328
|
+
hostname: import_os8.default.hostname(),
|
|
1834
2329
|
cwd: process.cwd(),
|
|
1835
|
-
platform:
|
|
2330
|
+
platform: import_os8.default.platform()
|
|
1836
2331
|
}
|
|
1837
2332
|
}),
|
|
1838
2333
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -1853,9 +2348,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
1853
2348
|
context: {
|
|
1854
2349
|
agent: meta?.agent,
|
|
1855
2350
|
mcpServer: meta?.mcpServer,
|
|
1856
|
-
hostname:
|
|
2351
|
+
hostname: import_os8.default.hostname(),
|
|
1857
2352
|
cwd: process.cwd(),
|
|
1858
|
-
platform:
|
|
2353
|
+
platform: import_os8.default.platform()
|
|
1859
2354
|
},
|
|
1860
2355
|
...riskMetadata && { riskMetadata }
|
|
1861
2356
|
}),
|
|
@@ -1911,14 +2406,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
1911
2406
|
});
|
|
1912
2407
|
clearTimeout(timer);
|
|
1913
2408
|
if (!res.ok) {
|
|
1914
|
-
|
|
2409
|
+
import_fs9.default.appendFileSync(
|
|
1915
2410
|
HOOK_DEBUG_LOG,
|
|
1916
2411
|
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
1917
2412
|
`
|
|
1918
2413
|
);
|
|
1919
2414
|
}
|
|
1920
2415
|
} catch (err) {
|
|
1921
|
-
|
|
2416
|
+
import_fs9.default.appendFileSync(
|
|
1922
2417
|
HOOK_DEBUG_LOG,
|
|
1923
2418
|
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
1924
2419
|
`
|
|
@@ -1927,7 +2422,7 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
1927
2422
|
}
|
|
1928
2423
|
|
|
1929
2424
|
// src/auth/orchestrator.ts
|
|
1930
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
2425
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os9.default.tmpdir(), "node9-activity.sock");
|
|
1931
2426
|
function notifyActivity(data) {
|
|
1932
2427
|
return new Promise((resolve) => {
|
|
1933
2428
|
try {
|
|
@@ -2009,7 +2504,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2009
2504
|
}
|
|
2010
2505
|
if (config.settings.mode === "audit") {
|
|
2011
2506
|
if (!isIgnoredTool(toolName)) {
|
|
2012
|
-
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
2507
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
2013
2508
|
if (policyResult.decision === "review") {
|
|
2014
2509
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
2015
2510
|
if (approvers.cloud && creds?.apiKey) {
|