@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.mjs
CHANGED
|
@@ -1,9 +1,61 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/audit/hasher.ts
|
|
12
|
+
import { createHash } from "crypto";
|
|
13
|
+
function canonicalise(value) {
|
|
14
|
+
return _canonicalise(value, /* @__PURE__ */ new WeakSet());
|
|
15
|
+
}
|
|
16
|
+
function _canonicalise(value, seen) {
|
|
17
|
+
if (value === null || typeof value !== "object") return value;
|
|
18
|
+
if (value instanceof Date) return value.toISOString();
|
|
19
|
+
if (value instanceof RegExp) return value.toString();
|
|
20
|
+
if (Buffer.isBuffer(value)) return value.toString("base64");
|
|
21
|
+
if (seen.has(value)) return "[Circular]";
|
|
22
|
+
seen.add(value);
|
|
23
|
+
let result;
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
result = value.map((v) => _canonicalise(v, seen));
|
|
26
|
+
} else {
|
|
27
|
+
const obj = value;
|
|
28
|
+
result = Object.fromEntries(
|
|
29
|
+
Object.keys(obj).sort().map((k) => [k, _canonicalise(obj[k], seen)])
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
seen.delete(value);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function hashArgs(args) {
|
|
36
|
+
const canonical = JSON.stringify(canonicalise(args) ?? null);
|
|
37
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
38
|
+
}
|
|
39
|
+
var init_hasher = __esm({
|
|
40
|
+
"src/audit/hasher.ts"() {
|
|
41
|
+
"use strict";
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
1
45
|
// src/audit/index.ts
|
|
46
|
+
var audit_exports = {};
|
|
47
|
+
__export(audit_exports, {
|
|
48
|
+
HOOK_DEBUG_LOG: () => HOOK_DEBUG_LOG,
|
|
49
|
+
LOCAL_AUDIT_LOG: () => LOCAL_AUDIT_LOG,
|
|
50
|
+
appendConfigAudit: () => appendConfigAudit,
|
|
51
|
+
appendHookDebug: () => appendHookDebug,
|
|
52
|
+
appendLocalAudit: () => appendLocalAudit,
|
|
53
|
+
appendToLog: () => appendToLog,
|
|
54
|
+
redactSecrets: () => redactSecrets
|
|
55
|
+
});
|
|
2
56
|
import fs from "fs";
|
|
3
57
|
import path from "path";
|
|
4
58
|
import os from "os";
|
|
5
|
-
var LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
|
|
6
|
-
var HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
|
|
7
59
|
function redactSecrets(text) {
|
|
8
60
|
if (!text) return text;
|
|
9
61
|
let redacted = text;
|
|
@@ -25,24 +77,24 @@ function appendToLog(logPath, entry) {
|
|
|
25
77
|
} catch {
|
|
26
78
|
}
|
|
27
79
|
}
|
|
28
|
-
function appendHookDebug(toolName, args, meta) {
|
|
29
|
-
const
|
|
80
|
+
function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
81
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
30
82
|
appendToLog(HOOK_DEBUG_LOG, {
|
|
31
83
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32
84
|
tool: toolName,
|
|
33
|
-
|
|
85
|
+
...argsField,
|
|
34
86
|
agent: meta?.agent,
|
|
35
87
|
mcpServer: meta?.mcpServer,
|
|
36
88
|
hostname: os.hostname(),
|
|
37
89
|
cwd: process.cwd()
|
|
38
90
|
});
|
|
39
91
|
}
|
|
40
|
-
function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
41
|
-
const
|
|
92
|
+
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
93
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
42
94
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
43
95
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
44
96
|
tool: toolName,
|
|
45
|
-
|
|
97
|
+
...argsField,
|
|
46
98
|
decision,
|
|
47
99
|
checkedBy,
|
|
48
100
|
agent: meta?.agent,
|
|
@@ -50,6 +102,25 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
|
50
102
|
hostname: os.hostname()
|
|
51
103
|
});
|
|
52
104
|
}
|
|
105
|
+
function appendConfigAudit(entry) {
|
|
106
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
107
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
108
|
+
...entry,
|
|
109
|
+
hostname: os.hostname()
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
|
|
113
|
+
var init_audit = __esm({
|
|
114
|
+
"src/audit/index.ts"() {
|
|
115
|
+
"use strict";
|
|
116
|
+
init_hasher();
|
|
117
|
+
LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
|
|
118
|
+
HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// src/core.ts
|
|
123
|
+
init_audit();
|
|
53
124
|
|
|
54
125
|
// src/config/index.ts
|
|
55
126
|
import fs3 from "fs";
|
|
@@ -118,7 +189,8 @@ var ConfigFileSchema = z.object({
|
|
|
118
189
|
environment: z.string().optional(),
|
|
119
190
|
slackEnabled: z.boolean().optional(),
|
|
120
191
|
enableTrustSessions: z.boolean().optional(),
|
|
121
|
-
allowGlobalPause: z.boolean().optional()
|
|
192
|
+
allowGlobalPause: z.boolean().optional(),
|
|
193
|
+
auditHashArgs: z.boolean().optional()
|
|
122
194
|
}).optional(),
|
|
123
195
|
policy: z.object({
|
|
124
196
|
sandboxPaths: z.array(z.string()).optional(),
|
|
@@ -414,6 +486,7 @@ var DEFAULT_CONFIG = {
|
|
|
414
486
|
approvalTimeoutMs: 12e4,
|
|
415
487
|
// 120-second auto-deny timeout
|
|
416
488
|
flightRecorder: true,
|
|
489
|
+
auditHashArgs: true,
|
|
417
490
|
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
418
491
|
},
|
|
419
492
|
policy: {
|
|
@@ -1456,16 +1529,35 @@ function readTrustedHosts() {
|
|
|
1456
1529
|
return [];
|
|
1457
1530
|
}
|
|
1458
1531
|
}
|
|
1532
|
+
var _cache = null;
|
|
1533
|
+
var CACHE_TTL_MS = 5e3;
|
|
1534
|
+
function getFileMtime() {
|
|
1535
|
+
try {
|
|
1536
|
+
return fs6.statSync(getTrustedHostsPath()).mtimeMs;
|
|
1537
|
+
} catch {
|
|
1538
|
+
return 0;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function getCachedHosts() {
|
|
1542
|
+
const now = Date.now();
|
|
1543
|
+
if (_cache && now < _cache.expiry) {
|
|
1544
|
+
const mtime = getFileMtime();
|
|
1545
|
+
if (mtime === _cache.mtime) return _cache.hosts;
|
|
1546
|
+
}
|
|
1547
|
+
const hosts = readTrustedHosts();
|
|
1548
|
+
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1549
|
+
return hosts;
|
|
1550
|
+
}
|
|
1459
1551
|
function normalizeHost(raw) {
|
|
1460
1552
|
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
1461
1553
|
}
|
|
1462
1554
|
function isTrustedHost(host) {
|
|
1463
1555
|
const normalized = normalizeHost(host);
|
|
1464
|
-
return
|
|
1556
|
+
return getCachedHosts().some((entry) => {
|
|
1465
1557
|
const entryHost = entry.host.toLowerCase();
|
|
1466
1558
|
if (entryHost.startsWith("*.")) {
|
|
1467
1559
|
const domain = entryHost.slice(2);
|
|
1468
|
-
return normalized
|
|
1560
|
+
return normalized.endsWith("." + domain);
|
|
1469
1561
|
}
|
|
1470
1562
|
return normalized === entryHost;
|
|
1471
1563
|
});
|
|
@@ -1662,7 +1754,12 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1662
1754
|
};
|
|
1663
1755
|
}
|
|
1664
1756
|
if (allTrusted) {
|
|
1665
|
-
return {
|
|
1757
|
+
return {
|
|
1758
|
+
decision: "allow",
|
|
1759
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1760
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1761
|
+
tier: 3
|
|
1762
|
+
};
|
|
1666
1763
|
}
|
|
1667
1764
|
return {
|
|
1668
1765
|
decision: "review",
|
|
@@ -1914,8 +2011,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
1914
2011
|
signal: ctrl.signal
|
|
1915
2012
|
});
|
|
1916
2013
|
if (!res.ok) throw new Error("Daemon fail");
|
|
1917
|
-
const { id } = await res.json();
|
|
1918
|
-
return id;
|
|
2014
|
+
const { id, allowCount } = await res.json();
|
|
2015
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
1919
2016
|
} finally {
|
|
1920
2017
|
clearTimeout(timer);
|
|
1921
2018
|
}
|
|
@@ -1954,15 +2051,54 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
|
1954
2051
|
signal: AbortSignal.timeout(3e3)
|
|
1955
2052
|
});
|
|
1956
2053
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
1957
|
-
const { id } = await res.json();
|
|
1958
|
-
return id;
|
|
2054
|
+
const { id, allowCount } = await res.json();
|
|
2055
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2056
|
+
}
|
|
2057
|
+
async function notifyTaint(filePath, source) {
|
|
2058
|
+
if (!isDaemonRunning()) return;
|
|
2059
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2060
|
+
try {
|
|
2061
|
+
await fetch(`${base}/taint`, {
|
|
2062
|
+
method: "POST",
|
|
2063
|
+
headers: { "Content-Type": "application/json" },
|
|
2064
|
+
body: JSON.stringify({ path: filePath, source }),
|
|
2065
|
+
signal: AbortSignal.timeout(1e3)
|
|
2066
|
+
});
|
|
2067
|
+
} catch {
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
async function checkTaint(paths) {
|
|
2071
|
+
if (paths.length === 0) return { tainted: false };
|
|
2072
|
+
if (!isDaemonRunning()) return { tainted: false, daemonUnavailable: true };
|
|
2073
|
+
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2074
|
+
try {
|
|
2075
|
+
const res = await fetch(`${base}/taint/check`, {
|
|
2076
|
+
method: "POST",
|
|
2077
|
+
headers: { "Content-Type": "application/json" },
|
|
2078
|
+
body: JSON.stringify({ paths }),
|
|
2079
|
+
signal: AbortSignal.timeout(2e3)
|
|
2080
|
+
});
|
|
2081
|
+
return await res.json();
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
try {
|
|
2084
|
+
const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
|
|
2085
|
+
appendToLog2(HOOK_DEBUG_LOG2, {
|
|
2086
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2087
|
+
event: "checkTaint-error",
|
|
2088
|
+
error: String(err),
|
|
2089
|
+
paths
|
|
2090
|
+
});
|
|
2091
|
+
} catch {
|
|
2092
|
+
}
|
|
2093
|
+
return { tainted: false, daemonUnavailable: true };
|
|
2094
|
+
}
|
|
1959
2095
|
}
|
|
1960
|
-
async function resolveViaDaemon(id, decision, internalToken) {
|
|
2096
|
+
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
1961
2097
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1962
2098
|
await fetch(`${base}/resolve/${id}`, {
|
|
1963
2099
|
method: "POST",
|
|
1964
2100
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
1965
|
-
body: JSON.stringify({ decision }),
|
|
2101
|
+
body: JSON.stringify({ decision, ...source && { source } }),
|
|
1966
2102
|
signal: AbortSignal.timeout(3e3)
|
|
1967
2103
|
});
|
|
1968
2104
|
}
|
|
@@ -2164,20 +2300,24 @@ ${smartTruncate(str, 500)}`
|
|
|
2164
2300
|
function escapePango(text) {
|
|
2165
2301
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2166
2302
|
}
|
|
2167
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2303
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2168
2304
|
const lines = [];
|
|
2169
2305
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
2170
2306
|
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
2171
2307
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
2172
2308
|
lines.push("");
|
|
2173
2309
|
lines.push(formattedArgs);
|
|
2310
|
+
if (allowCount >= 3) {
|
|
2311
|
+
lines.push("");
|
|
2312
|
+
lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
|
|
2313
|
+
}
|
|
2174
2314
|
if (!locked) {
|
|
2175
2315
|
lines.push("");
|
|
2176
2316
|
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
2177
2317
|
}
|
|
2178
2318
|
return lines.join("\n");
|
|
2179
2319
|
}
|
|
2180
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2320
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2181
2321
|
const lines = [];
|
|
2182
2322
|
if (locked) {
|
|
2183
2323
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -2189,6 +2329,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2189
2329
|
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
2190
2330
|
lines.push("");
|
|
2191
2331
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
2332
|
+
if (allowCount >= 3) {
|
|
2333
|
+
lines.push("");
|
|
2334
|
+
lines.push(
|
|
2335
|
+
`<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2192
2338
|
if (!locked) {
|
|
2193
2339
|
lines.push("");
|
|
2194
2340
|
lines.push(
|
|
@@ -2197,12 +2343,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2197
2343
|
}
|
|
2198
2344
|
return lines.join("\n");
|
|
2199
2345
|
}
|
|
2200
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
2346
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
|
|
2201
2347
|
if (isTestEnv()) return "deny";
|
|
2202
2348
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
2203
2349
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
2204
2350
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
2205
|
-
const message = buildPlainMessage(
|
|
2351
|
+
const message = buildPlainMessage(
|
|
2352
|
+
toolName,
|
|
2353
|
+
formattedArgs,
|
|
2354
|
+
agent,
|
|
2355
|
+
explainableLabel,
|
|
2356
|
+
locked,
|
|
2357
|
+
allowCount
|
|
2358
|
+
);
|
|
2206
2359
|
return new Promise((resolve) => {
|
|
2207
2360
|
let childProcess = null;
|
|
2208
2361
|
const onAbort = () => {
|
|
@@ -2234,7 +2387,8 @@ end run`;
|
|
|
2234
2387
|
formattedArgs,
|
|
2235
2388
|
agent,
|
|
2236
2389
|
explainableLabel,
|
|
2237
|
-
locked
|
|
2390
|
+
locked,
|
|
2391
|
+
allowCount
|
|
2238
2392
|
);
|
|
2239
2393
|
const argsList = [
|
|
2240
2394
|
locked ? "--info" : "--question",
|
|
@@ -2275,7 +2429,11 @@ end run`;
|
|
|
2275
2429
|
});
|
|
2276
2430
|
}
|
|
2277
2431
|
|
|
2432
|
+
// src/auth/orchestrator.ts
|
|
2433
|
+
init_audit();
|
|
2434
|
+
|
|
2278
2435
|
// src/auth/cloud.ts
|
|
2436
|
+
init_audit();
|
|
2279
2437
|
import fs9 from "fs";
|
|
2280
2438
|
import os8 from "os";
|
|
2281
2439
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
@@ -2386,6 +2544,51 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
2386
2544
|
}
|
|
2387
2545
|
|
|
2388
2546
|
// src/auth/orchestrator.ts
|
|
2547
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
2548
|
+
"write",
|
|
2549
|
+
"write_file",
|
|
2550
|
+
"create_file",
|
|
2551
|
+
"edit",
|
|
2552
|
+
"multiedit",
|
|
2553
|
+
"str_replace_based_edit_tool",
|
|
2554
|
+
"replace",
|
|
2555
|
+
"notebook_edit",
|
|
2556
|
+
"notebookedit"
|
|
2557
|
+
]);
|
|
2558
|
+
function isWriteTool(toolName) {
|
|
2559
|
+
const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
2560
|
+
return WRITE_TOOLS.has(t);
|
|
2561
|
+
}
|
|
2562
|
+
function extractFilePaths(toolName, args) {
|
|
2563
|
+
const paths = [];
|
|
2564
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
|
|
2565
|
+
const a = args;
|
|
2566
|
+
for (const key of ["file_path", "path", "filename", "source", "src", "input"]) {
|
|
2567
|
+
if (typeof a[key] === "string" && a[key]) paths.push(a[key]);
|
|
2568
|
+
}
|
|
2569
|
+
const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
|
|
2570
|
+
if (cmd) {
|
|
2571
|
+
for (const m of cmd.matchAll(/(?:-T|--upload-file|--data(?:-binary)?)\s+@?(\S+)/g)) {
|
|
2572
|
+
paths.push(m[1]);
|
|
2573
|
+
}
|
|
2574
|
+
for (const m of cmd.matchAll(/\b(?:scp|rsync)\s+(?:-\S+\s+)*(\S+)\s+\S+@/g)) {
|
|
2575
|
+
paths.push(m[1]);
|
|
2576
|
+
}
|
|
2577
|
+
for (const m of cmd.matchAll(/<\s*(\S+)/g)) {
|
|
2578
|
+
paths.push(m[1]);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
return paths.filter(Boolean);
|
|
2582
|
+
}
|
|
2583
|
+
function isNetworkTool(toolName, args) {
|
|
2584
|
+
const t = toolName.toLowerCase();
|
|
2585
|
+
if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
|
|
2586
|
+
const a = args;
|
|
2587
|
+
const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
|
|
2588
|
+
return /\b(curl|wget|scp|rsync|nc|ncat|netcat|ssh)\b/.test(cmd);
|
|
2589
|
+
}
|
|
2590
|
+
return false;
|
|
2591
|
+
}
|
|
2389
2592
|
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os9.tmpdir(), "node9-activity.sock");
|
|
2390
2593
|
function notifyActivity(data) {
|
|
2391
2594
|
return new Promise((resolve) => {
|
|
@@ -2416,7 +2619,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
2416
2619
|
id: actId,
|
|
2417
2620
|
tool: toolName,
|
|
2418
2621
|
ts: actTs,
|
|
2419
|
-
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
2622
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
2420
2623
|
label: result.blockedByLabel
|
|
2421
2624
|
});
|
|
2422
2625
|
}
|
|
@@ -2430,6 +2633,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2430
2633
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
2431
2634
|
const creds = getCredentials();
|
|
2432
2635
|
const config = getConfig(options?.cwd);
|
|
2636
|
+
const hashAuditArgs = config.settings.auditHashArgs === true;
|
|
2433
2637
|
const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
|
|
2434
2638
|
const approvers = {
|
|
2435
2639
|
...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
|
|
@@ -2440,13 +2644,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2440
2644
|
approvers.terminal = false;
|
|
2441
2645
|
}
|
|
2442
2646
|
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
2443
|
-
appendHookDebug(toolName, args, meta);
|
|
2647
|
+
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
2444
2648
|
}
|
|
2445
2649
|
const isManual = meta?.agent === "Terminal";
|
|
2446
2650
|
let explainableLabel = "Local Config";
|
|
2447
2651
|
let policyMatchedField;
|
|
2448
2652
|
let policyMatchedWord;
|
|
2449
2653
|
let riskMetadata;
|
|
2654
|
+
let taintWarning = null;
|
|
2655
|
+
if (isNetworkTool(toolName, args)) {
|
|
2656
|
+
const filePaths = extractFilePaths(toolName, args);
|
|
2657
|
+
if (filePaths.length > 0) {
|
|
2658
|
+
const taintResult = await checkTaint(filePaths);
|
|
2659
|
+
if (taintResult.tainted && taintResult.record) {
|
|
2660
|
+
const { path: taintedPath, source: taintSource } = taintResult.record;
|
|
2661
|
+
taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
|
|
2662
|
+
} else if (taintResult.daemonUnavailable) {
|
|
2663
|
+
taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2450
2667
|
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
2451
2668
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
2452
2669
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
@@ -2454,7 +2671,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2454
2671
|
if (dlpMatch) {
|
|
2455
2672
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
2456
2673
|
if (dlpMatch.severity === "block") {
|
|
2457
|
-
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
|
|
2674
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
|
|
2675
|
+
if (isWriteTool(toolName) && filePath) {
|
|
2676
|
+
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
|
|
2677
|
+
}
|
|
2458
2678
|
return {
|
|
2459
2679
|
approved: false,
|
|
2460
2680
|
reason: dlpReason,
|
|
@@ -2462,7 +2682,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2462
2682
|
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
2463
2683
|
};
|
|
2464
2684
|
}
|
|
2465
|
-
if (!isManual)
|
|
2685
|
+
if (!isManual)
|
|
2686
|
+
appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta, hashAuditArgs);
|
|
2466
2687
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
2467
2688
|
}
|
|
2468
2689
|
}
|
|
@@ -2470,7 +2691,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2470
2691
|
if (!isIgnoredTool(toolName)) {
|
|
2471
2692
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
2472
2693
|
if (policyResult.decision === "review") {
|
|
2473
|
-
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
2694
|
+
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
2474
2695
|
if (approvers.cloud && creds?.apiKey) {
|
|
2475
2696
|
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
2476
2697
|
}
|
|
@@ -2478,22 +2699,23 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2478
2699
|
}
|
|
2479
2700
|
return { approved: true, checkedBy: "audit" };
|
|
2480
2701
|
}
|
|
2481
|
-
if (!isIgnoredTool(toolName)) {
|
|
2702
|
+
if (!taintWarning && !isIgnoredTool(toolName)) {
|
|
2482
2703
|
if (getActiveTrustSession(toolName)) {
|
|
2483
2704
|
if (approvers.cloud && creds?.apiKey)
|
|
2484
2705
|
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
2485
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
|
|
2706
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
2486
2707
|
return { approved: true, checkedBy: "trust" };
|
|
2487
2708
|
}
|
|
2488
2709
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
2489
2710
|
if (policyResult.decision === "allow") {
|
|
2490
2711
|
if (approvers.cloud && creds?.apiKey)
|
|
2491
2712
|
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
2492
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
|
|
2713
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
|
|
2493
2714
|
return { approved: true, checkedBy: "local-policy" };
|
|
2494
2715
|
}
|
|
2495
2716
|
if (policyResult.decision === "block") {
|
|
2496
|
-
if (!isManual)
|
|
2717
|
+
if (!isManual)
|
|
2718
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
2497
2719
|
return {
|
|
2498
2720
|
approved: false,
|
|
2499
2721
|
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
@@ -2512,15 +2734,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2512
2734
|
policyMatchedWord,
|
|
2513
2735
|
policyResult.ruleName
|
|
2514
2736
|
);
|
|
2515
|
-
const persistent = getPersistentDecision(toolName);
|
|
2737
|
+
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
2516
2738
|
if (persistent === "allow") {
|
|
2517
2739
|
if (approvers.cloud && creds?.apiKey)
|
|
2518
2740
|
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
2519
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
|
|
2741
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
2520
2742
|
return { approved: true, checkedBy: "persistent" };
|
|
2521
2743
|
}
|
|
2522
2744
|
if (persistent === "deny") {
|
|
2523
|
-
if (!isManual)
|
|
2745
|
+
if (!isManual)
|
|
2746
|
+
appendLocalAudit(toolName, args, "deny", "persistent-deny", meta, hashAuditArgs);
|
|
2524
2747
|
return {
|
|
2525
2748
|
approved: false,
|
|
2526
2749
|
reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
|
|
@@ -2528,10 +2751,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2528
2751
|
blockedByLabel: "Persistent User Rule"
|
|
2529
2752
|
};
|
|
2530
2753
|
}
|
|
2531
|
-
} else {
|
|
2532
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
|
|
2754
|
+
} else if (!taintWarning) {
|
|
2755
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
|
|
2533
2756
|
return { approved: true };
|
|
2534
2757
|
}
|
|
2758
|
+
if (taintWarning) {
|
|
2759
|
+
explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
|
|
2760
|
+
riskMetadata = computeRiskMetadata(
|
|
2761
|
+
args,
|
|
2762
|
+
7,
|
|
2763
|
+
explainableLabel,
|
|
2764
|
+
void 0,
|
|
2765
|
+
void 0,
|
|
2766
|
+
taintWarning
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2535
2769
|
let cloudRequestId = null;
|
|
2536
2770
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
2537
2771
|
if (cloudEnforced) {
|
|
@@ -2550,7 +2784,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2550
2784
|
};
|
|
2551
2785
|
}
|
|
2552
2786
|
cloudRequestId = initResult.requestId || null;
|
|
2553
|
-
explainableLabel = "Organization Policy (SaaS)";
|
|
2787
|
+
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
2554
2788
|
} catch {
|
|
2555
2789
|
}
|
|
2556
2790
|
}
|
|
@@ -2579,13 +2813,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2579
2813
|
let viewerId = null;
|
|
2580
2814
|
const internalToken = getInternalToken();
|
|
2581
2815
|
let daemonEntryId = null;
|
|
2816
|
+
let daemonAllowCount = 1;
|
|
2582
2817
|
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
2583
2818
|
if (cloudEnforced && cloudRequestId) {
|
|
2584
|
-
|
|
2819
|
+
const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
2820
|
+
viewerId = viewer?.id ?? null;
|
|
2585
2821
|
daemonEntryId = viewerId;
|
|
2822
|
+
if (viewer) daemonAllowCount = viewer.allowCount;
|
|
2586
2823
|
} else {
|
|
2587
2824
|
try {
|
|
2588
|
-
|
|
2825
|
+
const entry = await registerDaemonEntry(
|
|
2589
2826
|
toolName,
|
|
2590
2827
|
args,
|
|
2591
2828
|
meta,
|
|
@@ -2593,6 +2830,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2593
2830
|
options?.activityId,
|
|
2594
2831
|
options?.cwd
|
|
2595
2832
|
);
|
|
2833
|
+
daemonEntryId = entry.id;
|
|
2834
|
+
daemonAllowCount = entry.allowCount;
|
|
2596
2835
|
} catch {
|
|
2597
2836
|
}
|
|
2598
2837
|
}
|
|
@@ -2628,7 +2867,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2628
2867
|
false,
|
|
2629
2868
|
signal,
|
|
2630
2869
|
policyMatchedField,
|
|
2631
|
-
policyMatchedWord
|
|
2870
|
+
policyMatchedWord,
|
|
2871
|
+
daemonAllowCount
|
|
2632
2872
|
);
|
|
2633
2873
|
if (decision === "always_allow") {
|
|
2634
2874
|
writeTrustSession(toolName, 36e5);
|
|
@@ -2686,10 +2926,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
2686
2926
|
if (!resolved) {
|
|
2687
2927
|
resolved = true;
|
|
2688
2928
|
abortController.abort();
|
|
2689
|
-
if (
|
|
2690
|
-
resolveViaDaemon(
|
|
2691
|
-
|
|
2692
|
-
|
|
2929
|
+
if (daemonEntryId && internalToken) {
|
|
2930
|
+
resolveViaDaemon(
|
|
2931
|
+
daemonEntryId,
|
|
2932
|
+
res.approved ? "allow" : "deny",
|
|
2933
|
+
internalToken,
|
|
2934
|
+
res.decisionSource
|
|
2935
|
+
).catch(() => null);
|
|
2693
2936
|
}
|
|
2694
2937
|
resolve(res);
|
|
2695
2938
|
}
|
|
@@ -2728,7 +2971,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
2728
2971
|
args,
|
|
2729
2972
|
finalResult.approved ? "allow" : "deny",
|
|
2730
2973
|
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
2731
|
-
meta
|
|
2974
|
+
meta,
|
|
2975
|
+
hashAuditArgs
|
|
2732
2976
|
);
|
|
2733
2977
|
}
|
|
2734
2978
|
return finalResult;
|