@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.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 path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
159
|
+
return ` \u2022 ${path14}: ${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 path8 from "path";
|
|
889
890
|
import pm from "picomatch";
|
|
890
891
|
import { parse } from "sh-syntax";
|
|
891
892
|
|
|
@@ -1031,8 +1032,447 @@ 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
|
+
|
|
1443
|
+
// src/auth/trusted-hosts.ts
|
|
1444
|
+
import fs6 from "fs";
|
|
1445
|
+
import path7 from "path";
|
|
1446
|
+
import os5 from "os";
|
|
1447
|
+
function getTrustedHostsPath() {
|
|
1448
|
+
return path7.join(os5.homedir(), ".node9", "trusted-hosts.json");
|
|
1449
|
+
}
|
|
1450
|
+
function readTrustedHosts() {
|
|
1451
|
+
try {
|
|
1452
|
+
const raw = fs6.readFileSync(getTrustedHostsPath(), "utf8");
|
|
1453
|
+
const parsed = JSON.parse(raw);
|
|
1454
|
+
return Array.isArray(parsed.hosts) ? parsed.hosts : [];
|
|
1455
|
+
} catch {
|
|
1456
|
+
return [];
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
function normalizeHost(raw) {
|
|
1460
|
+
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
1461
|
+
}
|
|
1462
|
+
function isTrustedHost(host) {
|
|
1463
|
+
const normalized = normalizeHost(host);
|
|
1464
|
+
return readTrustedHosts().some((entry) => {
|
|
1465
|
+
const entryHost = entry.host.toLowerCase();
|
|
1466
|
+
if (entryHost.startsWith("*.")) {
|
|
1467
|
+
const domain = entryHost.slice(2);
|
|
1468
|
+
return normalized === domain || normalized.endsWith("." + domain);
|
|
1469
|
+
}
|
|
1470
|
+
return normalized === entryHost;
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1034
1474
|
// src/policy/index.ts
|
|
1035
|
-
function
|
|
1475
|
+
function tokenize2(toolName) {
|
|
1036
1476
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
1037
1477
|
}
|
|
1038
1478
|
function matchesPattern(text, patterns) {
|
|
@@ -1045,9 +1485,9 @@ function matchesPattern(text, patterns) {
|
|
|
1045
1485
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1046
1486
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1047
1487
|
}
|
|
1048
|
-
function getNestedValue(obj,
|
|
1488
|
+
function getNestedValue(obj, path14) {
|
|
1049
1489
|
if (!obj || typeof obj !== "object") return null;
|
|
1050
|
-
return
|
|
1490
|
+
return path14.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1051
1491
|
}
|
|
1052
1492
|
function evaluateSmartConditions(args, rule) {
|
|
1053
1493
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -1171,7 +1611,7 @@ async function analyzeShellCommand(command) {
|
|
|
1171
1611
|
}
|
|
1172
1612
|
return { actions, paths, allTokens };
|
|
1173
1613
|
}
|
|
1174
|
-
async function evaluatePolicy(toolName, args, agent) {
|
|
1614
|
+
async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
1175
1615
|
const config = getConfig();
|
|
1176
1616
|
if (matchesPattern(toolName, config.policy.ignoredTools)) return { decision: "allow" };
|
|
1177
1617
|
if (config.policy.smartRules.length > 0) {
|
|
@@ -1201,11 +1641,66 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
1201
1641
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
1202
1642
|
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
1203
1643
|
}
|
|
1644
|
+
const pipeAnalysis = analyzePipeChain(shellCommand);
|
|
1645
|
+
if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
|
|
1646
|
+
const sinks = pipeAnalysis.sinkTargets;
|
|
1647
|
+
const allTrusted = sinks.length > 0 && sinks.every(isTrustedHost);
|
|
1648
|
+
if (pipeAnalysis.risk === "critical") {
|
|
1649
|
+
if (allTrusted) {
|
|
1650
|
+
return {
|
|
1651
|
+
decision: "review",
|
|
1652
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
1653
|
+
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
1654
|
+
tier: 3
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
return {
|
|
1658
|
+
decision: "block",
|
|
1659
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1660
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1661
|
+
tier: 3
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
if (allTrusted) {
|
|
1665
|
+
return { decision: "allow" };
|
|
1666
|
+
}
|
|
1667
|
+
return {
|
|
1668
|
+
decision: "review",
|
|
1669
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1670
|
+
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1671
|
+
tier: 3
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
const firstToken = analyzed.actions[0] ?? "";
|
|
1675
|
+
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
1676
|
+
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
1677
|
+
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
1678
|
+
allTokens.push(...sshHosts);
|
|
1679
|
+
}
|
|
1680
|
+
if (firstToken && path8.posix.isAbsolute(firstToken)) {
|
|
1681
|
+
const prov = checkProvenance(firstToken, cwd);
|
|
1682
|
+
if (prov.trustLevel === "suspect") {
|
|
1683
|
+
return {
|
|
1684
|
+
decision: config.settings.mode === "strict" ? "block" : "review",
|
|
1685
|
+
blockedByLabel: "Node9: Suspect Binary",
|
|
1686
|
+
reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
|
|
1687
|
+
tier: 3
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
if (prov.trustLevel === "unknown" && config.settings.mode === "strict") {
|
|
1691
|
+
return {
|
|
1692
|
+
decision: "review",
|
|
1693
|
+
blockedByLabel: "Node9: Unknown Binary (strict mode)",
|
|
1694
|
+
reason: `Binary "${firstToken}" \u2014 ${prov.reason}`,
|
|
1695
|
+
tier: 3
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1204
1699
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
1205
1700
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1206
1701
|
}
|
|
1207
1702
|
} else {
|
|
1208
|
-
allTokens =
|
|
1703
|
+
allTokens = tokenize2(toolName);
|
|
1209
1704
|
if (args && typeof args === "object") {
|
|
1210
1705
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
1211
1706
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
@@ -1283,18 +1778,18 @@ function isIgnoredTool(toolName) {
|
|
|
1283
1778
|
}
|
|
1284
1779
|
|
|
1285
1780
|
// src/auth/state.ts
|
|
1286
|
-
import
|
|
1287
|
-
import
|
|
1288
|
-
import
|
|
1289
|
-
var PAUSED_FILE =
|
|
1290
|
-
var TRUST_FILE =
|
|
1781
|
+
import fs7 from "fs";
|
|
1782
|
+
import path9 from "path";
|
|
1783
|
+
import os6 from "os";
|
|
1784
|
+
var PAUSED_FILE = path9.join(os6.homedir(), ".node9", "PAUSED");
|
|
1785
|
+
var TRUST_FILE = path9.join(os6.homedir(), ".node9", "trust.json");
|
|
1291
1786
|
function checkPause() {
|
|
1292
1787
|
try {
|
|
1293
|
-
if (!
|
|
1294
|
-
const state = JSON.parse(
|
|
1788
|
+
if (!fs7.existsSync(PAUSED_FILE)) return { paused: false };
|
|
1789
|
+
const state = JSON.parse(fs7.readFileSync(PAUSED_FILE, "utf-8"));
|
|
1295
1790
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
1296
1791
|
try {
|
|
1297
|
-
|
|
1792
|
+
fs7.unlinkSync(PAUSED_FILE);
|
|
1298
1793
|
} catch {
|
|
1299
1794
|
}
|
|
1300
1795
|
return { paused: false };
|
|
@@ -1305,20 +1800,20 @@ function checkPause() {
|
|
|
1305
1800
|
}
|
|
1306
1801
|
}
|
|
1307
1802
|
function atomicWriteSync(filePath, data, options) {
|
|
1308
|
-
const dir =
|
|
1309
|
-
if (!
|
|
1310
|
-
const tmpPath = `${filePath}.${
|
|
1311
|
-
|
|
1312
|
-
|
|
1803
|
+
const dir = path9.dirname(filePath);
|
|
1804
|
+
if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
|
|
1805
|
+
const tmpPath = `${filePath}.${os6.hostname()}.${process.pid}.tmp`;
|
|
1806
|
+
fs7.writeFileSync(tmpPath, data, options);
|
|
1807
|
+
fs7.renameSync(tmpPath, filePath);
|
|
1313
1808
|
}
|
|
1314
1809
|
function getActiveTrustSession(toolName) {
|
|
1315
1810
|
try {
|
|
1316
|
-
if (!
|
|
1317
|
-
const trust = JSON.parse(
|
|
1811
|
+
if (!fs7.existsSync(TRUST_FILE)) return false;
|
|
1812
|
+
const trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
|
|
1318
1813
|
const now = Date.now();
|
|
1319
1814
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
1320
1815
|
if (active.length !== trust.entries.length) {
|
|
1321
|
-
|
|
1816
|
+
fs7.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
1322
1817
|
}
|
|
1323
1818
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
1324
1819
|
} catch {
|
|
@@ -1329,8 +1824,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
1329
1824
|
try {
|
|
1330
1825
|
let trust = { entries: [] };
|
|
1331
1826
|
try {
|
|
1332
|
-
if (
|
|
1333
|
-
trust = JSON.parse(
|
|
1827
|
+
if (fs7.existsSync(TRUST_FILE)) {
|
|
1828
|
+
trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
|
|
1334
1829
|
}
|
|
1335
1830
|
} catch {
|
|
1336
1831
|
}
|
|
@@ -1346,9 +1841,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
1346
1841
|
}
|
|
1347
1842
|
function getPersistentDecision(toolName) {
|
|
1348
1843
|
try {
|
|
1349
|
-
const file =
|
|
1350
|
-
if (!
|
|
1351
|
-
const decisions = JSON.parse(
|
|
1844
|
+
const file = path9.join(os6.homedir(), ".node9", "decisions.json");
|
|
1845
|
+
if (!fs7.existsSync(file)) return null;
|
|
1846
|
+
const decisions = JSON.parse(fs7.readFileSync(file, "utf-8"));
|
|
1352
1847
|
const d = decisions[toolName];
|
|
1353
1848
|
if (d === "allow" || d === "deny") return d;
|
|
1354
1849
|
} catch {
|
|
@@ -1357,17 +1852,17 @@ function getPersistentDecision(toolName) {
|
|
|
1357
1852
|
}
|
|
1358
1853
|
|
|
1359
1854
|
// src/auth/daemon.ts
|
|
1360
|
-
import
|
|
1361
|
-
import
|
|
1362
|
-
import
|
|
1855
|
+
import fs8 from "fs";
|
|
1856
|
+
import path10 from "path";
|
|
1857
|
+
import os7 from "os";
|
|
1363
1858
|
import { spawnSync } from "child_process";
|
|
1364
1859
|
var DAEMON_PORT = 7391;
|
|
1365
1860
|
var DAEMON_HOST = "127.0.0.1";
|
|
1366
1861
|
function getInternalToken() {
|
|
1367
1862
|
try {
|
|
1368
|
-
const pidFile =
|
|
1369
|
-
if (!
|
|
1370
|
-
const data = JSON.parse(
|
|
1863
|
+
const pidFile = path10.join(os7.homedir(), ".node9", "daemon.pid");
|
|
1864
|
+
if (!fs8.existsSync(pidFile)) return null;
|
|
1865
|
+
const data = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
|
|
1371
1866
|
process.kill(data.pid, 0);
|
|
1372
1867
|
return data.internalToken ?? null;
|
|
1373
1868
|
} catch {
|
|
@@ -1375,10 +1870,10 @@ function getInternalToken() {
|
|
|
1375
1870
|
}
|
|
1376
1871
|
}
|
|
1377
1872
|
function isDaemonRunning() {
|
|
1378
|
-
const pidFile =
|
|
1379
|
-
if (
|
|
1873
|
+
const pidFile = path10.join(os7.homedir(), ".node9", "daemon.pid");
|
|
1874
|
+
if (fs8.existsSync(pidFile)) {
|
|
1380
1875
|
try {
|
|
1381
|
-
const { pid, port } = JSON.parse(
|
|
1876
|
+
const { pid, port } = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
|
|
1382
1877
|
if (port !== DAEMON_PORT) return false;
|
|
1383
1878
|
process.kill(pid, 0);
|
|
1384
1879
|
return true;
|
|
@@ -1474,16 +1969,16 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1474
1969
|
|
|
1475
1970
|
// src/auth/orchestrator.ts
|
|
1476
1971
|
import net from "net";
|
|
1477
|
-
import
|
|
1478
|
-
import
|
|
1972
|
+
import path13 from "path";
|
|
1973
|
+
import os9 from "os";
|
|
1479
1974
|
import { randomUUID } from "crypto";
|
|
1480
1975
|
|
|
1481
1976
|
// src/ui/native.ts
|
|
1482
1977
|
import { spawn } from "child_process";
|
|
1483
|
-
import
|
|
1978
|
+
import path12 from "path";
|
|
1484
1979
|
|
|
1485
1980
|
// src/context-sniper.ts
|
|
1486
|
-
import
|
|
1981
|
+
import path11 from "path";
|
|
1487
1982
|
function smartTruncate(str, maxLen = 500) {
|
|
1488
1983
|
if (str.length <= maxLen) return str;
|
|
1489
1984
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -1551,7 +2046,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
1551
2046
|
intent = "EDIT";
|
|
1552
2047
|
if (obj.file_path) {
|
|
1553
2048
|
editFilePath = String(obj.file_path);
|
|
1554
|
-
editFileName =
|
|
2049
|
+
editFileName = path11.basename(editFilePath);
|
|
1555
2050
|
}
|
|
1556
2051
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
1557
2052
|
contextSnippet = result.snippet;
|
|
@@ -1606,7 +2101,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
1606
2101
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1607
2102
|
const obj = parsed;
|
|
1608
2103
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
1609
|
-
const file = obj.file_path ?
|
|
2104
|
+
const file = obj.file_path ? path12.basename(String(obj.file_path)) : "file";
|
|
1610
2105
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
1611
2106
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
1612
2107
|
return {
|
|
@@ -1781,8 +2276,8 @@ end run`;
|
|
|
1781
2276
|
}
|
|
1782
2277
|
|
|
1783
2278
|
// src/auth/cloud.ts
|
|
1784
|
-
import
|
|
1785
|
-
import
|
|
2279
|
+
import fs9 from "fs";
|
|
2280
|
+
import os8 from "os";
|
|
1786
2281
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
1787
2282
|
return fetch(`${creds.apiUrl}/audit`, {
|
|
1788
2283
|
method: "POST",
|
|
@@ -1794,9 +2289,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1794
2289
|
context: {
|
|
1795
2290
|
agent: meta?.agent,
|
|
1796
2291
|
mcpServer: meta?.mcpServer,
|
|
1797
|
-
hostname:
|
|
2292
|
+
hostname: os8.hostname(),
|
|
1798
2293
|
cwd: process.cwd(),
|
|
1799
|
-
platform:
|
|
2294
|
+
platform: os8.platform()
|
|
1800
2295
|
}
|
|
1801
2296
|
}),
|
|
1802
2297
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -1817,9 +2312,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
1817
2312
|
context: {
|
|
1818
2313
|
agent: meta?.agent,
|
|
1819
2314
|
mcpServer: meta?.mcpServer,
|
|
1820
|
-
hostname:
|
|
2315
|
+
hostname: os8.hostname(),
|
|
1821
2316
|
cwd: process.cwd(),
|
|
1822
|
-
platform:
|
|
2317
|
+
platform: os8.platform()
|
|
1823
2318
|
},
|
|
1824
2319
|
...riskMetadata && { riskMetadata }
|
|
1825
2320
|
}),
|
|
@@ -1875,14 +2370,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
1875
2370
|
});
|
|
1876
2371
|
clearTimeout(timer);
|
|
1877
2372
|
if (!res.ok) {
|
|
1878
|
-
|
|
2373
|
+
fs9.appendFileSync(
|
|
1879
2374
|
HOOK_DEBUG_LOG,
|
|
1880
2375
|
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
1881
2376
|
`
|
|
1882
2377
|
);
|
|
1883
2378
|
}
|
|
1884
2379
|
} catch (err) {
|
|
1885
|
-
|
|
2380
|
+
fs9.appendFileSync(
|
|
1886
2381
|
HOOK_DEBUG_LOG,
|
|
1887
2382
|
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
1888
2383
|
`
|
|
@@ -1891,7 +2386,7 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
1891
2386
|
}
|
|
1892
2387
|
|
|
1893
2388
|
// src/auth/orchestrator.ts
|
|
1894
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
2389
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os9.tmpdir(), "node9-activity.sock");
|
|
1895
2390
|
function notifyActivity(data) {
|
|
1896
2391
|
return new Promise((resolve) => {
|
|
1897
2392
|
try {
|
|
@@ -1973,7 +2468,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
1973
2468
|
}
|
|
1974
2469
|
if (config.settings.mode === "audit") {
|
|
1975
2470
|
if (!isIgnoredTool(toolName)) {
|
|
1976
|
-
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
2471
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
1977
2472
|
if (policyResult.decision === "review") {
|
|
1978
2473
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
1979
2474
|
if (approvers.cloud && creds?.apiKey) {
|