@node9/proxy 1.3.1 → 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/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.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 path13 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
195
|
+
return ` \u2022 ${path13}: ${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_path7 = __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,416 @@ 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
|
+
|
|
1070
1479
|
// src/policy/index.ts
|
|
1071
|
-
function
|
|
1480
|
+
function tokenize2(toolName) {
|
|
1072
1481
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
1073
1482
|
}
|
|
1074
1483
|
function matchesPattern(text, patterns) {
|
|
@@ -1081,9 +1490,9 @@ function matchesPattern(text, patterns) {
|
|
|
1081
1490
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1082
1491
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1083
1492
|
}
|
|
1084
|
-
function getNestedValue(obj,
|
|
1493
|
+
function getNestedValue(obj, path13) {
|
|
1085
1494
|
if (!obj || typeof obj !== "object") return null;
|
|
1086
|
-
return
|
|
1495
|
+
return path13.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1087
1496
|
}
|
|
1088
1497
|
function evaluateSmartConditions(args, rule) {
|
|
1089
1498
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -1207,7 +1616,7 @@ async function analyzeShellCommand(command) {
|
|
|
1207
1616
|
}
|
|
1208
1617
|
return { actions, paths, allTokens };
|
|
1209
1618
|
}
|
|
1210
|
-
async function evaluatePolicy(toolName, args, agent) {
|
|
1619
|
+
async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
1211
1620
|
const config = getConfig();
|
|
1212
1621
|
if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
|
|
1213
1622
|
if (config.policy.smartRules.length > 0) {
|
|
@@ -1237,11 +1646,55 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
1237
1646
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
1238
1647
|
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
1239
1648
|
}
|
|
1649
|
+
const pipeAnalysis = analyzePipeChain(shellCommand);
|
|
1650
|
+
if (pipeAnalysis.isPipeline) {
|
|
1651
|
+
if (pipeAnalysis.risk === "critical") {
|
|
1652
|
+
return {
|
|
1653
|
+
decision: "block",
|
|
1654
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1655
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
|
|
1656
|
+
tier: 3
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
if (pipeAnalysis.risk === "high") {
|
|
1660
|
+
return {
|
|
1661
|
+
decision: "review",
|
|
1662
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1663
|
+
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
|
|
1664
|
+
tier: 3
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
const firstToken = analyzed.actions[0] ?? "";
|
|
1669
|
+
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
1670
|
+
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
1671
|
+
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
1672
|
+
allTokens.push(...sshHosts);
|
|
1673
|
+
}
|
|
1674
|
+
if (firstToken && import_path7.default.posix.isAbsolute(firstToken)) {
|
|
1675
|
+
const prov = checkProvenance(firstToken, cwd);
|
|
1676
|
+
if (prov.trustLevel === "suspect") {
|
|
1677
|
+
return {
|
|
1678
|
+
decision: config.settings.mode === "strict" ? "block" : "review",
|
|
1679
|
+
blockedByLabel: "Node9: Suspect Binary",
|
|
1680
|
+
reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
|
|
1681
|
+
tier: 3
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
if (prov.trustLevel === "unknown" && config.settings.mode === "strict") {
|
|
1685
|
+
return {
|
|
1686
|
+
decision: "review",
|
|
1687
|
+
blockedByLabel: "Node9: Unknown Binary (strict mode)",
|
|
1688
|
+
reason: `Binary "${firstToken}" \u2014 ${prov.reason}`,
|
|
1689
|
+
tier: 3
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1240
1693
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
1241
1694
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1242
1695
|
}
|
|
1243
1696
|
} else {
|
|
1244
|
-
allTokens =
|
|
1697
|
+
allTokens = tokenize2(toolName);
|
|
1245
1698
|
if (args && typeof args === "object") {
|
|
1246
1699
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
1247
1700
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
@@ -1319,18 +1772,18 @@ function isIgnoredTool(toolName) {
|
|
|
1319
1772
|
}
|
|
1320
1773
|
|
|
1321
1774
|
// src/auth/state.ts
|
|
1322
|
-
var
|
|
1323
|
-
var
|
|
1324
|
-
var
|
|
1325
|
-
var PAUSED_FILE =
|
|
1326
|
-
var TRUST_FILE =
|
|
1775
|
+
var import_fs6 = __toESM(require("fs"));
|
|
1776
|
+
var import_path8 = __toESM(require("path"));
|
|
1777
|
+
var import_os5 = __toESM(require("os"));
|
|
1778
|
+
var PAUSED_FILE = import_path8.default.join(import_os5.default.homedir(), ".node9", "PAUSED");
|
|
1779
|
+
var TRUST_FILE = import_path8.default.join(import_os5.default.homedir(), ".node9", "trust.json");
|
|
1327
1780
|
function checkPause() {
|
|
1328
1781
|
try {
|
|
1329
|
-
if (!
|
|
1330
|
-
const state = JSON.parse(
|
|
1782
|
+
if (!import_fs6.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
1783
|
+
const state = JSON.parse(import_fs6.default.readFileSync(PAUSED_FILE, "utf-8"));
|
|
1331
1784
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
1332
1785
|
try {
|
|
1333
|
-
|
|
1786
|
+
import_fs6.default.unlinkSync(PAUSED_FILE);
|
|
1334
1787
|
} catch {
|
|
1335
1788
|
}
|
|
1336
1789
|
return { paused: false };
|
|
@@ -1341,20 +1794,20 @@ function checkPause() {
|
|
|
1341
1794
|
}
|
|
1342
1795
|
}
|
|
1343
1796
|
function atomicWriteSync(filePath, data, options) {
|
|
1344
|
-
const dir =
|
|
1345
|
-
if (!
|
|
1346
|
-
const tmpPath = `${filePath}.${
|
|
1347
|
-
|
|
1348
|
-
|
|
1797
|
+
const dir = import_path8.default.dirname(filePath);
|
|
1798
|
+
if (!import_fs6.default.existsSync(dir)) import_fs6.default.mkdirSync(dir, { recursive: true });
|
|
1799
|
+
const tmpPath = `${filePath}.${import_os5.default.hostname()}.${process.pid}.tmp`;
|
|
1800
|
+
import_fs6.default.writeFileSync(tmpPath, data, options);
|
|
1801
|
+
import_fs6.default.renameSync(tmpPath, filePath);
|
|
1349
1802
|
}
|
|
1350
1803
|
function getActiveTrustSession(toolName) {
|
|
1351
1804
|
try {
|
|
1352
|
-
if (!
|
|
1353
|
-
const trust = JSON.parse(
|
|
1805
|
+
if (!import_fs6.default.existsSync(TRUST_FILE)) return false;
|
|
1806
|
+
const trust = JSON.parse(import_fs6.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
1354
1807
|
const now = Date.now();
|
|
1355
1808
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
1356
1809
|
if (active.length !== trust.entries.length) {
|
|
1357
|
-
|
|
1810
|
+
import_fs6.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
1358
1811
|
}
|
|
1359
1812
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
1360
1813
|
} catch {
|
|
@@ -1365,8 +1818,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
1365
1818
|
try {
|
|
1366
1819
|
let trust = { entries: [] };
|
|
1367
1820
|
try {
|
|
1368
|
-
if (
|
|
1369
|
-
trust = JSON.parse(
|
|
1821
|
+
if (import_fs6.default.existsSync(TRUST_FILE)) {
|
|
1822
|
+
trust = JSON.parse(import_fs6.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
1370
1823
|
}
|
|
1371
1824
|
} catch {
|
|
1372
1825
|
}
|
|
@@ -1382,9 +1835,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
1382
1835
|
}
|
|
1383
1836
|
function getPersistentDecision(toolName) {
|
|
1384
1837
|
try {
|
|
1385
|
-
const file =
|
|
1386
|
-
if (!
|
|
1387
|
-
const decisions = JSON.parse(
|
|
1838
|
+
const file = import_path8.default.join(import_os5.default.homedir(), ".node9", "decisions.json");
|
|
1839
|
+
if (!import_fs6.default.existsSync(file)) return null;
|
|
1840
|
+
const decisions = JSON.parse(import_fs6.default.readFileSync(file, "utf-8"));
|
|
1388
1841
|
const d = decisions[toolName];
|
|
1389
1842
|
if (d === "allow" || d === "deny") return d;
|
|
1390
1843
|
} catch {
|
|
@@ -1393,17 +1846,17 @@ function getPersistentDecision(toolName) {
|
|
|
1393
1846
|
}
|
|
1394
1847
|
|
|
1395
1848
|
// src/auth/daemon.ts
|
|
1396
|
-
var
|
|
1397
|
-
var
|
|
1398
|
-
var
|
|
1849
|
+
var import_fs7 = __toESM(require("fs"));
|
|
1850
|
+
var import_path9 = __toESM(require("path"));
|
|
1851
|
+
var import_os6 = __toESM(require("os"));
|
|
1399
1852
|
var import_child_process = require("child_process");
|
|
1400
1853
|
var DAEMON_PORT = 7391;
|
|
1401
1854
|
var DAEMON_HOST = "127.0.0.1";
|
|
1402
1855
|
function getInternalToken() {
|
|
1403
1856
|
try {
|
|
1404
|
-
const pidFile =
|
|
1405
|
-
if (!
|
|
1406
|
-
const data = JSON.parse(
|
|
1857
|
+
const pidFile = import_path9.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
|
|
1858
|
+
if (!import_fs7.default.existsSync(pidFile)) return null;
|
|
1859
|
+
const data = JSON.parse(import_fs7.default.readFileSync(pidFile, "utf-8"));
|
|
1407
1860
|
process.kill(data.pid, 0);
|
|
1408
1861
|
return data.internalToken ?? null;
|
|
1409
1862
|
} catch {
|
|
@@ -1411,10 +1864,10 @@ function getInternalToken() {
|
|
|
1411
1864
|
}
|
|
1412
1865
|
}
|
|
1413
1866
|
function isDaemonRunning() {
|
|
1414
|
-
const pidFile =
|
|
1415
|
-
if (
|
|
1867
|
+
const pidFile = import_path9.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
|
|
1868
|
+
if (import_fs7.default.existsSync(pidFile)) {
|
|
1416
1869
|
try {
|
|
1417
|
-
const { pid, port } = JSON.parse(
|
|
1870
|
+
const { pid, port } = JSON.parse(import_fs7.default.readFileSync(pidFile, "utf-8"));
|
|
1418
1871
|
if (port !== DAEMON_PORT) return false;
|
|
1419
1872
|
process.kill(pid, 0);
|
|
1420
1873
|
return true;
|
|
@@ -1510,16 +1963,16 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1510
1963
|
|
|
1511
1964
|
// src/auth/orchestrator.ts
|
|
1512
1965
|
var import_net = __toESM(require("net"));
|
|
1513
|
-
var
|
|
1514
|
-
var
|
|
1966
|
+
var import_path12 = __toESM(require("path"));
|
|
1967
|
+
var import_os8 = __toESM(require("os"));
|
|
1515
1968
|
var import_crypto = require("crypto");
|
|
1516
1969
|
|
|
1517
1970
|
// src/ui/native.ts
|
|
1518
1971
|
var import_child_process2 = require("child_process");
|
|
1519
|
-
var
|
|
1972
|
+
var import_path11 = __toESM(require("path"));
|
|
1520
1973
|
|
|
1521
1974
|
// src/context-sniper.ts
|
|
1522
|
-
var
|
|
1975
|
+
var import_path10 = __toESM(require("path"));
|
|
1523
1976
|
function smartTruncate(str, maxLen = 500) {
|
|
1524
1977
|
if (str.length <= maxLen) return str;
|
|
1525
1978
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -1587,7 +2040,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
1587
2040
|
intent = "EDIT";
|
|
1588
2041
|
if (obj.file_path) {
|
|
1589
2042
|
editFilePath = String(obj.file_path);
|
|
1590
|
-
editFileName =
|
|
2043
|
+
editFileName = import_path10.default.basename(editFilePath);
|
|
1591
2044
|
}
|
|
1592
2045
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
1593
2046
|
contextSnippet = result.snippet;
|
|
@@ -1642,7 +2095,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
1642
2095
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1643
2096
|
const obj = parsed;
|
|
1644
2097
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
1645
|
-
const file = obj.file_path ?
|
|
2098
|
+
const file = obj.file_path ? import_path11.default.basename(String(obj.file_path)) : "file";
|
|
1646
2099
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
1647
2100
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
1648
2101
|
return {
|
|
@@ -1817,8 +2270,8 @@ end run`;
|
|
|
1817
2270
|
}
|
|
1818
2271
|
|
|
1819
2272
|
// src/auth/cloud.ts
|
|
1820
|
-
var
|
|
1821
|
-
var
|
|
2273
|
+
var import_fs8 = __toESM(require("fs"));
|
|
2274
|
+
var import_os7 = __toESM(require("os"));
|
|
1822
2275
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1823
2276
|
return fetch(`${creds.apiUrl}/audit`, {
|
|
1824
2277
|
method: "POST",
|
|
@@ -1830,9 +2283,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1830
2283
|
context: {
|
|
1831
2284
|
agent: meta?.agent,
|
|
1832
2285
|
mcpServer: meta?.mcpServer,
|
|
1833
|
-
hostname:
|
|
2286
|
+
hostname: import_os7.default.hostname(),
|
|
1834
2287
|
cwd: process.cwd(),
|
|
1835
|
-
platform:
|
|
2288
|
+
platform: import_os7.default.platform()
|
|
1836
2289
|
}
|
|
1837
2290
|
}),
|
|
1838
2291
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -1853,9 +2306,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
1853
2306
|
context: {
|
|
1854
2307
|
agent: meta?.agent,
|
|
1855
2308
|
mcpServer: meta?.mcpServer,
|
|
1856
|
-
hostname:
|
|
2309
|
+
hostname: import_os7.default.hostname(),
|
|
1857
2310
|
cwd: process.cwd(),
|
|
1858
|
-
platform:
|
|
2311
|
+
platform: import_os7.default.platform()
|
|
1859
2312
|
},
|
|
1860
2313
|
...riskMetadata && { riskMetadata }
|
|
1861
2314
|
}),
|
|
@@ -1911,14 +2364,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
1911
2364
|
});
|
|
1912
2365
|
clearTimeout(timer);
|
|
1913
2366
|
if (!res.ok) {
|
|
1914
|
-
|
|
2367
|
+
import_fs8.default.appendFileSync(
|
|
1915
2368
|
HOOK_DEBUG_LOG,
|
|
1916
2369
|
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
1917
2370
|
`
|
|
1918
2371
|
);
|
|
1919
2372
|
}
|
|
1920
2373
|
} catch (err) {
|
|
1921
|
-
|
|
2374
|
+
import_fs8.default.appendFileSync(
|
|
1922
2375
|
HOOK_DEBUG_LOG,
|
|
1923
2376
|
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
1924
2377
|
`
|
|
@@ -1927,7 +2380,7 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
1927
2380
|
}
|
|
1928
2381
|
|
|
1929
2382
|
// src/auth/orchestrator.ts
|
|
1930
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
2383
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path12.default.join(import_os8.default.tmpdir(), "node9-activity.sock");
|
|
1931
2384
|
function notifyActivity(data) {
|
|
1932
2385
|
return new Promise((resolve) => {
|
|
1933
2386
|
try {
|
|
@@ -2009,7 +2462,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2009
2462
|
}
|
|
2010
2463
|
if (config.settings.mode === "audit") {
|
|
2011
2464
|
if (!isIgnoredTool(toolName)) {
|
|
2012
|
-
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
2465
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
2013
2466
|
if (policyResult.decision === "review") {
|
|
2014
2467
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
2015
2468
|
if (approvers.cloud && creds?.apiKey) {
|