@node9/proxy 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -7
- package/dist/cli.js +1544 -438
- package/dist/cli.mjs +1540 -434
- package/dist/index.js +294 -56
- package/dist/index.mjs +290 -46
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,19 +30,52 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
30
|
-
// src/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// src/audit/hasher.ts
|
|
34
|
+
function canonicalise(value) {
|
|
35
|
+
return _canonicalise(value, /* @__PURE__ */ new WeakSet());
|
|
36
|
+
}
|
|
37
|
+
function _canonicalise(value, seen) {
|
|
38
|
+
if (value === null || typeof value !== "object") return value;
|
|
39
|
+
if (value instanceof Date) return value.toISOString();
|
|
40
|
+
if (value instanceof RegExp) return value.toString();
|
|
41
|
+
if (Buffer.isBuffer(value)) return value.toString("base64");
|
|
42
|
+
if (seen.has(value)) return "[Circular]";
|
|
43
|
+
seen.add(value);
|
|
44
|
+
let result;
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
result = value.map((v) => _canonicalise(v, seen));
|
|
47
|
+
} else {
|
|
48
|
+
const obj = value;
|
|
49
|
+
result = Object.fromEntries(
|
|
50
|
+
Object.keys(obj).sort().map((k) => [k, _canonicalise(obj[k], seen)])
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
seen.delete(value);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
function hashArgs(args) {
|
|
57
|
+
const canonical = JSON.stringify(canonicalise(args) ?? null);
|
|
58
|
+
return (0, import_crypto.createHash)("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
59
|
+
}
|
|
60
|
+
var import_crypto;
|
|
61
|
+
var init_hasher = __esm({
|
|
62
|
+
"src/audit/hasher.ts"() {
|
|
63
|
+
"use strict";
|
|
64
|
+
import_crypto = require("crypto");
|
|
65
|
+
}
|
|
34
66
|
});
|
|
35
|
-
module.exports = __toCommonJS(src_exports);
|
|
36
67
|
|
|
37
68
|
// src/audit/index.ts
|
|
38
|
-
var
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
69
|
+
var audit_exports = {};
|
|
70
|
+
__export(audit_exports, {
|
|
71
|
+
HOOK_DEBUG_LOG: () => HOOK_DEBUG_LOG,
|
|
72
|
+
LOCAL_AUDIT_LOG: () => LOCAL_AUDIT_LOG,
|
|
73
|
+
appendConfigAudit: () => appendConfigAudit,
|
|
74
|
+
appendHookDebug: () => appendHookDebug,
|
|
75
|
+
appendLocalAudit: () => appendLocalAudit,
|
|
76
|
+
appendToLog: () => appendToLog,
|
|
77
|
+
redactSecrets: () => redactSecrets
|
|
78
|
+
});
|
|
43
79
|
function redactSecrets(text) {
|
|
44
80
|
if (!text) return text;
|
|
45
81
|
let redacted = text;
|
|
@@ -61,24 +97,24 @@ function appendToLog(logPath, entry) {
|
|
|
61
97
|
} catch {
|
|
62
98
|
}
|
|
63
99
|
}
|
|
64
|
-
function appendHookDebug(toolName, args, meta) {
|
|
65
|
-
const
|
|
100
|
+
function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
101
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
66
102
|
appendToLog(HOOK_DEBUG_LOG, {
|
|
67
103
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
68
104
|
tool: toolName,
|
|
69
|
-
|
|
105
|
+
...argsField,
|
|
70
106
|
agent: meta?.agent,
|
|
71
107
|
mcpServer: meta?.mcpServer,
|
|
72
108
|
hostname: import_os.default.hostname(),
|
|
73
109
|
cwd: process.cwd()
|
|
74
110
|
});
|
|
75
111
|
}
|
|
76
|
-
function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
77
|
-
const
|
|
112
|
+
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
113
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
78
114
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
79
115
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
80
116
|
tool: toolName,
|
|
81
|
-
|
|
117
|
+
...argsField,
|
|
82
118
|
decision,
|
|
83
119
|
checkedBy,
|
|
84
120
|
agent: meta?.agent,
|
|
@@ -86,6 +122,35 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
|
86
122
|
hostname: import_os.default.hostname()
|
|
87
123
|
});
|
|
88
124
|
}
|
|
125
|
+
function appendConfigAudit(entry) {
|
|
126
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
127
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
128
|
+
...entry,
|
|
129
|
+
hostname: import_os.default.hostname()
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
var import_fs, import_path, import_os, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
|
|
133
|
+
var init_audit = __esm({
|
|
134
|
+
"src/audit/index.ts"() {
|
|
135
|
+
"use strict";
|
|
136
|
+
import_fs = __toESM(require("fs"));
|
|
137
|
+
import_path = __toESM(require("path"));
|
|
138
|
+
import_os = __toESM(require("os"));
|
|
139
|
+
init_hasher();
|
|
140
|
+
LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
141
|
+
HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// src/index.ts
|
|
146
|
+
var src_exports = {};
|
|
147
|
+
__export(src_exports, {
|
|
148
|
+
protect: () => protect
|
|
149
|
+
});
|
|
150
|
+
module.exports = __toCommonJS(src_exports);
|
|
151
|
+
|
|
152
|
+
// src/core.ts
|
|
153
|
+
init_audit();
|
|
89
154
|
|
|
90
155
|
// src/config/index.ts
|
|
91
156
|
var import_fs3 = __toESM(require("fs"));
|
|
@@ -154,7 +219,8 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
154
219
|
environment: import_zod.z.string().optional(),
|
|
155
220
|
slackEnabled: import_zod.z.boolean().optional(),
|
|
156
221
|
enableTrustSessions: import_zod.z.boolean().optional(),
|
|
157
|
-
allowGlobalPause: import_zod.z.boolean().optional()
|
|
222
|
+
allowGlobalPause: import_zod.z.boolean().optional(),
|
|
223
|
+
auditHashArgs: import_zod.z.boolean().optional()
|
|
158
224
|
}).optional(),
|
|
159
225
|
policy: import_zod.z.object({
|
|
160
226
|
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
@@ -450,6 +516,7 @@ var DEFAULT_CONFIG = {
|
|
|
450
516
|
approvalTimeoutMs: 12e4,
|
|
451
517
|
// 120-second auto-deny timeout
|
|
452
518
|
flightRecorder: true,
|
|
519
|
+
auditHashArgs: true,
|
|
453
520
|
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
454
521
|
},
|
|
455
522
|
policy: {
|
|
@@ -1492,16 +1559,35 @@ function readTrustedHosts() {
|
|
|
1492
1559
|
return [];
|
|
1493
1560
|
}
|
|
1494
1561
|
}
|
|
1562
|
+
var _cache = null;
|
|
1563
|
+
var CACHE_TTL_MS = 5e3;
|
|
1564
|
+
function getFileMtime() {
|
|
1565
|
+
try {
|
|
1566
|
+
return import_fs6.default.statSync(getTrustedHostsPath()).mtimeMs;
|
|
1567
|
+
} catch {
|
|
1568
|
+
return 0;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function getCachedHosts() {
|
|
1572
|
+
const now = Date.now();
|
|
1573
|
+
if (_cache && now < _cache.expiry) {
|
|
1574
|
+
const mtime = getFileMtime();
|
|
1575
|
+
if (mtime === _cache.mtime) return _cache.hosts;
|
|
1576
|
+
}
|
|
1577
|
+
const hosts = readTrustedHosts();
|
|
1578
|
+
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1579
|
+
return hosts;
|
|
1580
|
+
}
|
|
1495
1581
|
function normalizeHost(raw) {
|
|
1496
1582
|
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
1497
1583
|
}
|
|
1498
1584
|
function isTrustedHost(host) {
|
|
1499
1585
|
const normalized = normalizeHost(host);
|
|
1500
|
-
return
|
|
1586
|
+
return getCachedHosts().some((entry) => {
|
|
1501
1587
|
const entryHost = entry.host.toLowerCase();
|
|
1502
1588
|
if (entryHost.startsWith("*.")) {
|
|
1503
1589
|
const domain = entryHost.slice(2);
|
|
1504
|
-
return normalized
|
|
1590
|
+
return normalized.endsWith("." + domain);
|
|
1505
1591
|
}
|
|
1506
1592
|
return normalized === entryHost;
|
|
1507
1593
|
});
|
|
@@ -1698,7 +1784,12 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1698
1784
|
};
|
|
1699
1785
|
}
|
|
1700
1786
|
if (allTrusted) {
|
|
1701
|
-
return {
|
|
1787
|
+
return {
|
|
1788
|
+
decision: "allow",
|
|
1789
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1790
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1791
|
+
tier: 3
|
|
1792
|
+
};
|
|
1702
1793
|
}
|
|
1703
1794
|
return {
|
|
1704
1795
|
decision: "review",
|
|
@@ -1950,8 +2041,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
1950
2041
|
signal: ctrl.signal
|
|
1951
2042
|
});
|
|
1952
2043
|
if (!res.ok) throw new Error("Daemon fail");
|
|
1953
|
-
const { id } = await res.json();
|
|
1954
|
-
return id;
|
|
2044
|
+
const { id, allowCount } = await res.json();
|
|
2045
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
1955
2046
|
} finally {
|
|
1956
2047
|
clearTimeout(timer);
|
|
1957
2048
|
}
|
|
@@ -1990,15 +2081,54 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
|
1990
2081
|
signal: AbortSignal.timeout(3e3)
|
|
1991
2082
|
});
|
|
1992
2083
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
1993
|
-
const { id } = await res.json();
|
|
1994
|
-
return id;
|
|
2084
|
+
const { id, allowCount } = await res.json();
|
|
2085
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2086
|
+
}
|
|
2087
|
+
async function notifyTaint(filePath, source) {
|
|
2088
|
+
if (!isDaemonRunning()) return;
|
|
2089
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2090
|
+
try {
|
|
2091
|
+
await fetch(`${base}/taint`, {
|
|
2092
|
+
method: "POST",
|
|
2093
|
+
headers: { "Content-Type": "application/json" },
|
|
2094
|
+
body: JSON.stringify({ path: filePath, source }),
|
|
2095
|
+
signal: AbortSignal.timeout(1e3)
|
|
2096
|
+
});
|
|
2097
|
+
} catch {
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
async function checkTaint(paths) {
|
|
2101
|
+
if (paths.length === 0) return { tainted: false };
|
|
2102
|
+
if (!isDaemonRunning()) return { tainted: false, daemonUnavailable: true };
|
|
2103
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2104
|
+
try {
|
|
2105
|
+
const res = await fetch(`${base}/taint/check`, {
|
|
2106
|
+
method: "POST",
|
|
2107
|
+
headers: { "Content-Type": "application/json" },
|
|
2108
|
+
body: JSON.stringify({ paths }),
|
|
2109
|
+
signal: AbortSignal.timeout(2e3)
|
|
2110
|
+
});
|
|
2111
|
+
return await res.json();
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
try {
|
|
2114
|
+
const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
|
|
2115
|
+
appendToLog2(HOOK_DEBUG_LOG2, {
|
|
2116
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2117
|
+
event: "checkTaint-error",
|
|
2118
|
+
error: String(err),
|
|
2119
|
+
paths
|
|
2120
|
+
});
|
|
2121
|
+
} catch {
|
|
2122
|
+
}
|
|
2123
|
+
return { tainted: false, daemonUnavailable: true };
|
|
2124
|
+
}
|
|
1995
2125
|
}
|
|
1996
|
-
async function resolveViaDaemon(id, decision, internalToken) {
|
|
2126
|
+
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
1997
2127
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1998
2128
|
await fetch(`${base}/resolve/${id}`, {
|
|
1999
2129
|
method: "POST",
|
|
2000
2130
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
2001
|
-
body: JSON.stringify({ decision }),
|
|
2131
|
+
body: JSON.stringify({ decision, ...source && { source } }),
|
|
2002
2132
|
signal: AbortSignal.timeout(3e3)
|
|
2003
2133
|
});
|
|
2004
2134
|
}
|
|
@@ -2007,7 +2137,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
2007
2137
|
var import_net = __toESM(require("net"));
|
|
2008
2138
|
var import_path13 = __toESM(require("path"));
|
|
2009
2139
|
var import_os9 = __toESM(require("os"));
|
|
2010
|
-
var
|
|
2140
|
+
var import_crypto2 = require("crypto");
|
|
2011
2141
|
|
|
2012
2142
|
// src/ui/native.ts
|
|
2013
2143
|
var import_child_process2 = require("child_process");
|
|
@@ -2200,20 +2330,24 @@ ${smartTruncate(str, 500)}`
|
|
|
2200
2330
|
function escapePango(text) {
|
|
2201
2331
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2202
2332
|
}
|
|
2203
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2333
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2204
2334
|
const lines = [];
|
|
2205
2335
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
2206
2336
|
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
2207
2337
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
2208
2338
|
lines.push("");
|
|
2209
2339
|
lines.push(formattedArgs);
|
|
2340
|
+
if (allowCount >= 3) {
|
|
2341
|
+
lines.push("");
|
|
2342
|
+
lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
|
|
2343
|
+
}
|
|
2210
2344
|
if (!locked) {
|
|
2211
2345
|
lines.push("");
|
|
2212
2346
|
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
2213
2347
|
}
|
|
2214
2348
|
return lines.join("\n");
|
|
2215
2349
|
}
|
|
2216
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2350
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2217
2351
|
const lines = [];
|
|
2218
2352
|
if (locked) {
|
|
2219
2353
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -2225,6 +2359,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2225
2359
|
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
2226
2360
|
lines.push("");
|
|
2227
2361
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
2362
|
+
if (allowCount >= 3) {
|
|
2363
|
+
lines.push("");
|
|
2364
|
+
lines.push(
|
|
2365
|
+
`<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2228
2368
|
if (!locked) {
|
|
2229
2369
|
lines.push("");
|
|
2230
2370
|
lines.push(
|
|
@@ -2233,12 +2373,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2233
2373
|
}
|
|
2234
2374
|
return lines.join("\n");
|
|
2235
2375
|
}
|
|
2236
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
2376
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
|
|
2237
2377
|
if (isTestEnv()) return "deny";
|
|
2238
2378
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
2239
2379
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
2240
2380
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
2241
|
-
const message = buildPlainMessage(
|
|
2381
|
+
const message = buildPlainMessage(
|
|
2382
|
+
toolName,
|
|
2383
|
+
formattedArgs,
|
|
2384
|
+
agent,
|
|
2385
|
+
explainableLabel,
|
|
2386
|
+
locked,
|
|
2387
|
+
allowCount
|
|
2388
|
+
);
|
|
2242
2389
|
return new Promise((resolve) => {
|
|
2243
2390
|
let childProcess = null;
|
|
2244
2391
|
const onAbort = () => {
|
|
@@ -2270,7 +2417,8 @@ end run`;
|
|
|
2270
2417
|
formattedArgs,
|
|
2271
2418
|
agent,
|
|
2272
2419
|
explainableLabel,
|
|
2273
|
-
locked
|
|
2420
|
+
locked,
|
|
2421
|
+
allowCount
|
|
2274
2422
|
);
|
|
2275
2423
|
const argsList = [
|
|
2276
2424
|
locked ? "--info" : "--question",
|
|
@@ -2311,9 +2459,13 @@ end run`;
|
|
|
2311
2459
|
});
|
|
2312
2460
|
}
|
|
2313
2461
|
|
|
2462
|
+
// src/auth/orchestrator.ts
|
|
2463
|
+
init_audit();
|
|
2464
|
+
|
|
2314
2465
|
// src/auth/cloud.ts
|
|
2315
2466
|
var import_fs9 = __toESM(require("fs"));
|
|
2316
2467
|
var import_os8 = __toESM(require("os"));
|
|
2468
|
+
init_audit();
|
|
2317
2469
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
2318
2470
|
return fetch(`${creds.apiUrl}/audit`, {
|
|
2319
2471
|
method: "POST",
|
|
@@ -2422,6 +2574,51 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
2422
2574
|
}
|
|
2423
2575
|
|
|
2424
2576
|
// src/auth/orchestrator.ts
|
|
2577
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
2578
|
+
"write",
|
|
2579
|
+
"write_file",
|
|
2580
|
+
"create_file",
|
|
2581
|
+
"edit",
|
|
2582
|
+
"multiedit",
|
|
2583
|
+
"str_replace_based_edit_tool",
|
|
2584
|
+
"replace",
|
|
2585
|
+
"notebook_edit",
|
|
2586
|
+
"notebookedit"
|
|
2587
|
+
]);
|
|
2588
|
+
function isWriteTool(toolName) {
|
|
2589
|
+
const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
2590
|
+
return WRITE_TOOLS.has(t);
|
|
2591
|
+
}
|
|
2592
|
+
function extractFilePaths(toolName, args) {
|
|
2593
|
+
const paths = [];
|
|
2594
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
|
|
2595
|
+
const a = args;
|
|
2596
|
+
for (const key of ["file_path", "path", "filename", "source", "src", "input"]) {
|
|
2597
|
+
if (typeof a[key] === "string" && a[key]) paths.push(a[key]);
|
|
2598
|
+
}
|
|
2599
|
+
const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
|
|
2600
|
+
if (cmd) {
|
|
2601
|
+
for (const m of cmd.matchAll(/(?:-T|--upload-file|--data(?:-binary)?)\s+@?(\S+)/g)) {
|
|
2602
|
+
paths.push(m[1]);
|
|
2603
|
+
}
|
|
2604
|
+
for (const m of cmd.matchAll(/\b(?:scp|rsync)\s+(?:-\S+\s+)*(\S+)\s+\S+@/g)) {
|
|
2605
|
+
paths.push(m[1]);
|
|
2606
|
+
}
|
|
2607
|
+
for (const m of cmd.matchAll(/<\s*(\S+)/g)) {
|
|
2608
|
+
paths.push(m[1]);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
return paths.filter(Boolean);
|
|
2612
|
+
}
|
|
2613
|
+
function isNetworkTool(toolName, args) {
|
|
2614
|
+
const t = toolName.toLowerCase();
|
|
2615
|
+
if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
|
|
2616
|
+
const a = args;
|
|
2617
|
+
const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
|
|
2618
|
+
return /\b(curl|wget|scp|rsync|nc|ncat|netcat|ssh)\b/.test(cmd);
|
|
2619
|
+
}
|
|
2620
|
+
return false;
|
|
2621
|
+
}
|
|
2425
2622
|
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os9.default.tmpdir(), "node9-activity.sock");
|
|
2426
2623
|
function notifyActivity(data) {
|
|
2427
2624
|
return new Promise((resolve) => {
|
|
@@ -2440,7 +2637,7 @@ function notifyActivity(data) {
|
|
|
2440
2637
|
}
|
|
2441
2638
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
2442
2639
|
if (!options?.calledFromDaemon) {
|
|
2443
|
-
const actId = (0,
|
|
2640
|
+
const actId = (0, import_crypto2.randomUUID)();
|
|
2444
2641
|
const actTs = Date.now();
|
|
2445
2642
|
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
2446
2643
|
const result = await _authorizeHeadlessCore(toolName, args, meta, {
|
|
@@ -2452,7 +2649,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
2452
2649
|
id: actId,
|
|
2453
2650
|
tool: toolName,
|
|
2454
2651
|
ts: actTs,
|
|
2455
|
-
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
2652
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
2456
2653
|
label: result.blockedByLabel
|
|
2457
2654
|
});
|
|
2458
2655
|
}
|
|
@@ -2466,6 +2663,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2466
2663
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
2467
2664
|
const creds = getCredentials();
|
|
2468
2665
|
const config = getConfig(options?.cwd);
|
|
2666
|
+
const hashAuditArgs = config.settings.auditHashArgs === true;
|
|
2469
2667
|
const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
|
|
2470
2668
|
const approvers = {
|
|
2471
2669
|
...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
|
|
@@ -2476,13 +2674,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2476
2674
|
approvers.terminal = false;
|
|
2477
2675
|
}
|
|
2478
2676
|
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
2479
|
-
appendHookDebug(toolName, args, meta);
|
|
2677
|
+
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
2480
2678
|
}
|
|
2481
2679
|
const isManual = meta?.agent === "Terminal";
|
|
2482
2680
|
let explainableLabel = "Local Config";
|
|
2483
2681
|
let policyMatchedField;
|
|
2484
2682
|
let policyMatchedWord;
|
|
2485
2683
|
let riskMetadata;
|
|
2684
|
+
let taintWarning = null;
|
|
2685
|
+
if (isNetworkTool(toolName, args)) {
|
|
2686
|
+
const filePaths = extractFilePaths(toolName, args);
|
|
2687
|
+
if (filePaths.length > 0) {
|
|
2688
|
+
const taintResult = await checkTaint(filePaths);
|
|
2689
|
+
if (taintResult.tainted && taintResult.record) {
|
|
2690
|
+
const { path: taintedPath, source: taintSource } = taintResult.record;
|
|
2691
|
+
taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
|
|
2692
|
+
} else if (taintResult.daemonUnavailable) {
|
|
2693
|
+
taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2486
2697
|
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
2487
2698
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
2488
2699
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
@@ -2490,7 +2701,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2490
2701
|
if (dlpMatch) {
|
|
2491
2702
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
2492
2703
|
if (dlpMatch.severity === "block") {
|
|
2493
|
-
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
|
|
2704
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
|
|
2705
|
+
if (isWriteTool(toolName) && filePath) {
|
|
2706
|
+
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
|
|
2707
|
+
}
|
|
2494
2708
|
return {
|
|
2495
2709
|
approved: false,
|
|
2496
2710
|
reason: dlpReason,
|
|
@@ -2498,7 +2712,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2498
2712
|
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
2499
2713
|
};
|
|
2500
2714
|
}
|
|
2501
|
-
if (!isManual)
|
|
2715
|
+
if (!isManual)
|
|
2716
|
+
appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta, hashAuditArgs);
|
|
2502
2717
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
2503
2718
|
}
|
|
2504
2719
|
}
|
|
@@ -2506,7 +2721,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2506
2721
|
if (!isIgnoredTool(toolName)) {
|
|
2507
2722
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
2508
2723
|
if (policyResult.decision === "review") {
|
|
2509
|
-
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
2724
|
+
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
2510
2725
|
if (approvers.cloud && creds?.apiKey) {
|
|
2511
2726
|
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
2512
2727
|
}
|
|
@@ -2514,22 +2729,23 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2514
2729
|
}
|
|
2515
2730
|
return { approved: true, checkedBy: "audit" };
|
|
2516
2731
|
}
|
|
2517
|
-
if (!isIgnoredTool(toolName)) {
|
|
2732
|
+
if (!taintWarning && !isIgnoredTool(toolName)) {
|
|
2518
2733
|
if (getActiveTrustSession(toolName)) {
|
|
2519
2734
|
if (approvers.cloud && creds?.apiKey)
|
|
2520
2735
|
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
2521
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
2736
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
2522
2737
|
return { approved: true, checkedBy: "trust" };
|
|
2523
2738
|
}
|
|
2524
2739
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
2525
2740
|
if (policyResult.decision === "allow") {
|
|
2526
2741
|
if (approvers.cloud && creds?.apiKey)
|
|
2527
2742
|
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
2528
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
2743
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
|
|
2529
2744
|
return { approved: true, checkedBy: "local-policy" };
|
|
2530
2745
|
}
|
|
2531
2746
|
if (policyResult.decision === "block") {
|
|
2532
|
-
if (!isManual)
|
|
2747
|
+
if (!isManual)
|
|
2748
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
2533
2749
|
return {
|
|
2534
2750
|
approved: false,
|
|
2535
2751
|
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
@@ -2548,15 +2764,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2548
2764
|
policyMatchedWord,
|
|
2549
2765
|
policyResult.ruleName
|
|
2550
2766
|
);
|
|
2551
|
-
const persistent = getPersistentDecision(toolName);
|
|
2767
|
+
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
2552
2768
|
if (persistent === "allow") {
|
|
2553
2769
|
if (approvers.cloud && creds?.apiKey)
|
|
2554
2770
|
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
2555
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
2771
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
2556
2772
|
return { approved: true, checkedBy: "persistent" };
|
|
2557
2773
|
}
|
|
2558
2774
|
if (persistent === "deny") {
|
|
2559
|
-
if (!isManual)
|
|
2775
|
+
if (!isManual)
|
|
2776
|
+
appendLocalAudit(toolName, args, "deny", "persistent-deny", meta, hashAuditArgs);
|
|
2560
2777
|
return {
|
|
2561
2778
|
approved: false,
|
|
2562
2779
|
reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
|
|
@@ -2564,10 +2781,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2564
2781
|
blockedByLabel: "Persistent User Rule"
|
|
2565
2782
|
};
|
|
2566
2783
|
}
|
|
2567
|
-
} else {
|
|
2568
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
2784
|
+
} else if (!taintWarning) {
|
|
2785
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
|
|
2569
2786
|
return { approved: true };
|
|
2570
2787
|
}
|
|
2788
|
+
if (taintWarning) {
|
|
2789
|
+
explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
|
|
2790
|
+
riskMetadata = computeRiskMetadata(
|
|
2791
|
+
args,
|
|
2792
|
+
7,
|
|
2793
|
+
explainableLabel,
|
|
2794
|
+
void 0,
|
|
2795
|
+
void 0,
|
|
2796
|
+
taintWarning
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2571
2799
|
let cloudRequestId = null;
|
|
2572
2800
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
2573
2801
|
if (cloudEnforced) {
|
|
@@ -2586,7 +2814,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2586
2814
|
};
|
|
2587
2815
|
}
|
|
2588
2816
|
cloudRequestId = initResult.requestId || null;
|
|
2589
|
-
explainableLabel = "Organization Policy (SaaS)";
|
|
2817
|
+
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
2590
2818
|
} catch {
|
|
2591
2819
|
}
|
|
2592
2820
|
}
|
|
@@ -2615,13 +2843,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2615
2843
|
let viewerId = null;
|
|
2616
2844
|
const internalToken = getInternalToken();
|
|
2617
2845
|
let daemonEntryId = null;
|
|
2846
|
+
let daemonAllowCount = 1;
|
|
2618
2847
|
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
2619
2848
|
if (cloudEnforced && cloudRequestId) {
|
|
2620
|
-
|
|
2849
|
+
const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
2850
|
+
viewerId = viewer?.id ?? null;
|
|
2621
2851
|
daemonEntryId = viewerId;
|
|
2852
|
+
if (viewer) daemonAllowCount = viewer.allowCount;
|
|
2622
2853
|
} else {
|
|
2623
2854
|
try {
|
|
2624
|
-
|
|
2855
|
+
const entry = await registerDaemonEntry(
|
|
2625
2856
|
toolName,
|
|
2626
2857
|
args,
|
|
2627
2858
|
meta,
|
|
@@ -2629,6 +2860,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2629
2860
|
options?.activityId,
|
|
2630
2861
|
options?.cwd
|
|
2631
2862
|
);
|
|
2863
|
+
daemonEntryId = entry.id;
|
|
2864
|
+
daemonAllowCount = entry.allowCount;
|
|
2632
2865
|
} catch {
|
|
2633
2866
|
}
|
|
2634
2867
|
}
|
|
@@ -2664,7 +2897,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2664
2897
|
false,
|
|
2665
2898
|
signal,
|
|
2666
2899
|
policyMatchedField,
|
|
2667
|
-
policyMatchedWord
|
|
2900
|
+
policyMatchedWord,
|
|
2901
|
+
daemonAllowCount
|
|
2668
2902
|
);
|
|
2669
2903
|
if (decision === "always_allow") {
|
|
2670
2904
|
writeTrustSession(toolName, 36e5);
|
|
@@ -2722,10 +2956,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
2722
2956
|
if (!resolved) {
|
|
2723
2957
|
resolved = true;
|
|
2724
2958
|
abortController.abort();
|
|
2725
|
-
if (
|
|
2726
|
-
resolveViaDaemon(
|
|
2727
|
-
|
|
2728
|
-
|
|
2959
|
+
if (daemonEntryId && internalToken) {
|
|
2960
|
+
resolveViaDaemon(
|
|
2961
|
+
daemonEntryId,
|
|
2962
|
+
res.approved ? "allow" : "deny",
|
|
2963
|
+
internalToken,
|
|
2964
|
+
res.decisionSource
|
|
2965
|
+
).catch(() => null);
|
|
2729
2966
|
}
|
|
2730
2967
|
resolve(res);
|
|
2731
2968
|
}
|
|
@@ -2764,7 +3001,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
2764
3001
|
args,
|
|
2765
3002
|
finalResult.approved ? "allow" : "deny",
|
|
2766
3003
|
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
2767
|
-
meta
|
|
3004
|
+
meta,
|
|
3005
|
+
hashAuditArgs
|
|
2768
3006
|
);
|
|
2769
3007
|
}
|
|
2770
3008
|
return finalResult;
|