@node9/proxy 1.5.2 → 1.5.3
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 +60 -7
- package/dist/cli.js +945 -190
- package/dist/cli.mjs +942 -187
- package/dist/index.js +156 -47
- package/dist/index.mjs +156 -47
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -198,12 +198,21 @@ var SmartRuleSchema = import_zod.z.object({
|
|
|
198
198
|
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
199
199
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
200
200
|
}),
|
|
201
|
-
reason: import_zod.z.string().optional()
|
|
201
|
+
reason: import_zod.z.string().optional(),
|
|
202
|
+
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
203
|
+
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
204
|
+
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
205
|
+
dependsOnState: import_zod.z.array(import_zod.z.string()).transform(
|
|
206
|
+
(arr) => arr.filter(
|
|
207
|
+
(p) => p === "no_test_passed_since_last_edit"
|
|
208
|
+
)
|
|
209
|
+
).optional(),
|
|
210
|
+
recoveryCommand: import_zod.z.string().optional()
|
|
202
211
|
});
|
|
203
212
|
var ConfigFileSchema = import_zod.z.object({
|
|
204
213
|
version: import_zod.z.string().optional(),
|
|
205
214
|
settings: import_zod.z.object({
|
|
206
|
-
mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
|
|
215
|
+
mode: import_zod.z.enum(["standard", "strict", "audit", "observe"]).optional(),
|
|
207
216
|
autoStartDaemon: import_zod.z.boolean().optional(),
|
|
208
217
|
enableUndo: import_zod.z.boolean().optional(),
|
|
209
218
|
enableHookLogDebug: import_zod.z.boolean().optional(),
|
|
@@ -257,8 +266,8 @@ function sanitizeConfig(raw) {
|
|
|
257
266
|
}
|
|
258
267
|
}
|
|
259
268
|
const lines = result.error.issues.map((issue) => {
|
|
260
|
-
const
|
|
261
|
-
return ` \u2022 ${
|
|
269
|
+
const path13 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
270
|
+
return ` \u2022 ${path13}: ${issue.message}`;
|
|
262
271
|
});
|
|
263
272
|
return {
|
|
264
273
|
sanitized,
|
|
@@ -816,12 +825,17 @@ function getConfig(cwd) {
|
|
|
816
825
|
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
817
826
|
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
818
827
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
828
|
+
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
819
829
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
820
830
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
821
831
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
822
832
|
if (p.toolInspection)
|
|
823
833
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
824
|
-
if (p.smartRules)
|
|
834
|
+
if (p.smartRules) {
|
|
835
|
+
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
836
|
+
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
837
|
+
mergedPolicy.smartRules = [...defaultBlocks, ...p.smartRules, ...defaultNonBlocks];
|
|
838
|
+
}
|
|
825
839
|
if (p.snapshot) {
|
|
826
840
|
const s2 = p.snapshot;
|
|
827
841
|
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
@@ -1607,9 +1621,9 @@ function matchesPattern(text, patterns) {
|
|
|
1607
1621
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1608
1622
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1609
1623
|
}
|
|
1610
|
-
function getNestedValue(obj,
|
|
1624
|
+
function getNestedValue(obj, path13) {
|
|
1611
1625
|
if (!obj || typeof obj !== "object") return null;
|
|
1612
|
-
return
|
|
1626
|
+
return path13.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1613
1627
|
}
|
|
1614
1628
|
function evaluateSmartConditions(args, rule) {
|
|
1615
1629
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -1748,7 +1762,13 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1748
1762
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
1749
1763
|
reason: matchedRule.reason,
|
|
1750
1764
|
tier: 2,
|
|
1751
|
-
ruleName: matchedRule.name ?? matchedRule.tool
|
|
1765
|
+
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1766
|
+
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1767
|
+
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1768
|
+
},
|
|
1769
|
+
...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
|
|
1770
|
+
recoveryCommand: matchedRule.recoveryCommand
|
|
1771
|
+
}
|
|
1752
1772
|
};
|
|
1753
1773
|
}
|
|
1754
1774
|
}
|
|
@@ -1980,9 +2000,39 @@ function getPersistentDecision(toolName) {
|
|
|
1980
2000
|
|
|
1981
2001
|
// src/auth/daemon.ts
|
|
1982
2002
|
var import_fs8 = __toESM(require("fs"));
|
|
2003
|
+
var import_net = __toESM(require("net"));
|
|
1983
2004
|
var import_path10 = __toESM(require("path"));
|
|
1984
2005
|
var import_os7 = __toESM(require("os"));
|
|
1985
2006
|
var import_child_process = require("child_process");
|
|
2007
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path10.default.join(import_os7.default.tmpdir(), "node9-activity.sock");
|
|
2008
|
+
function notifyActivitySocket(data) {
|
|
2009
|
+
return new Promise((resolve) => {
|
|
2010
|
+
try {
|
|
2011
|
+
const payload = JSON.stringify(data);
|
|
2012
|
+
const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
|
|
2013
|
+
sock.on("connect", () => {
|
|
2014
|
+
sock.on("close", resolve);
|
|
2015
|
+
sock.end(payload);
|
|
2016
|
+
});
|
|
2017
|
+
sock.on("error", resolve);
|
|
2018
|
+
} catch {
|
|
2019
|
+
resolve();
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
async function checkStatePredicates(predicates) {
|
|
2024
|
+
if (predicates.length === 0) return {};
|
|
2025
|
+
try {
|
|
2026
|
+
const qs = predicates.map(encodeURIComponent).join(",");
|
|
2027
|
+
const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/state/check?predicates=${qs}`, {
|
|
2028
|
+
signal: AbortSignal.timeout(100)
|
|
2029
|
+
});
|
|
2030
|
+
if (!res.ok) return null;
|
|
2031
|
+
return await res.json();
|
|
2032
|
+
} catch {
|
|
2033
|
+
return null;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1986
2036
|
var DAEMON_PORT = 7391;
|
|
1987
2037
|
var DAEMON_HOST = "127.0.0.1";
|
|
1988
2038
|
function getInternalToken() {
|
|
@@ -2018,7 +2068,7 @@ function isDaemonRunning() {
|
|
|
2018
2068
|
return false;
|
|
2019
2069
|
}
|
|
2020
2070
|
}
|
|
2021
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
|
|
2071
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
2022
2072
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2023
2073
|
const ctrl = new AbortController();
|
|
2024
2074
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2036,7 +2086,10 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2036
2086
|
// activity-result as the CLI used for the pending activity event.
|
|
2037
2087
|
activityId,
|
|
2038
2088
|
...riskMetadata && { riskMetadata },
|
|
2039
|
-
...cwd && { cwd }
|
|
2089
|
+
...cwd && { cwd },
|
|
2090
|
+
...recoveryCommand && { recoveryCommand },
|
|
2091
|
+
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2092
|
+
...viewOnly && { viewOnly: true }
|
|
2040
2093
|
}),
|
|
2041
2094
|
signal: ctrl.signal
|
|
2042
2095
|
});
|
|
@@ -2056,10 +2109,10 @@ async function waitForDaemonDecision(id, signal) {
|
|
|
2056
2109
|
try {
|
|
2057
2110
|
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
2058
2111
|
if (!waitRes.ok) return { decision: "deny" };
|
|
2059
|
-
const { decision, source } = await waitRes.json();
|
|
2112
|
+
const { decision, source, reason } = await waitRes.json();
|
|
2060
2113
|
if (decision === "allow") return { decision: "allow", source };
|
|
2061
2114
|
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
2062
|
-
return { decision: "deny", source };
|
|
2115
|
+
return { decision: "deny", source, reason };
|
|
2063
2116
|
} finally {
|
|
2064
2117
|
clearTimeout(waitTimer);
|
|
2065
2118
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
@@ -2134,9 +2187,6 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
2134
2187
|
}
|
|
2135
2188
|
|
|
2136
2189
|
// src/auth/orchestrator.ts
|
|
2137
|
-
var import_net = __toESM(require("net"));
|
|
2138
|
-
var import_path13 = __toESM(require("path"));
|
|
2139
|
-
var import_os9 = __toESM(require("os"));
|
|
2140
2190
|
var import_crypto2 = require("crypto");
|
|
2141
2191
|
|
|
2142
2192
|
// src/ui/native.ts
|
|
@@ -2619,21 +2669,8 @@ function isNetworkTool(toolName, args) {
|
|
|
2619
2669
|
}
|
|
2620
2670
|
return false;
|
|
2621
2671
|
}
|
|
2622
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os9.default.tmpdir(), "node9-activity.sock");
|
|
2623
2672
|
function notifyActivity(data) {
|
|
2624
|
-
return
|
|
2625
|
-
try {
|
|
2626
|
-
const payload = JSON.stringify(data);
|
|
2627
|
-
const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
|
|
2628
|
-
sock.on("connect", () => {
|
|
2629
|
-
sock.on("close", resolve);
|
|
2630
|
-
sock.end(payload);
|
|
2631
|
-
});
|
|
2632
|
-
sock.on("error", resolve);
|
|
2633
|
-
} catch {
|
|
2634
|
-
resolve();
|
|
2635
|
-
}
|
|
2636
|
-
});
|
|
2673
|
+
return notifyActivitySocket(data);
|
|
2637
2674
|
}
|
|
2638
2675
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
2639
2676
|
if (!options?.calledFromDaemon) {
|
|
@@ -2650,7 +2687,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
2650
2687
|
tool: toolName,
|
|
2651
2688
|
ts: actTs,
|
|
2652
2689
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
2653
|
-
label: result.blockedByLabel
|
|
2690
|
+
label: result.blockedByLabel,
|
|
2691
|
+
ruleHit: result.ruleHit,
|
|
2692
|
+
observeWouldBlock: result.observeWouldBlock
|
|
2654
2693
|
});
|
|
2655
2694
|
}
|
|
2656
2695
|
return result;
|
|
@@ -2677,10 +2716,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2677
2716
|
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
2678
2717
|
}
|
|
2679
2718
|
const isManual = meta?.agent === "Terminal";
|
|
2719
|
+
const isObserveMode = config.settings.mode === "observe";
|
|
2680
2720
|
let explainableLabel = "Local Config";
|
|
2681
2721
|
let policyMatchedField;
|
|
2682
2722
|
let policyMatchedWord;
|
|
2683
2723
|
let riskMetadata;
|
|
2724
|
+
let statefulRecoveryCommand;
|
|
2684
2725
|
let taintWarning = null;
|
|
2685
2726
|
if (isNetworkTool(toolName, args)) {
|
|
2686
2727
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -2701,10 +2742,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2701
2742
|
if (dlpMatch) {
|
|
2702
2743
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
2703
2744
|
if (dlpMatch.severity === "block") {
|
|
2704
|
-
if (!isManual)
|
|
2745
|
+
if (!isManual)
|
|
2746
|
+
appendLocalAudit(
|
|
2747
|
+
toolName,
|
|
2748
|
+
args,
|
|
2749
|
+
"deny",
|
|
2750
|
+
isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
|
|
2751
|
+
meta,
|
|
2752
|
+
true
|
|
2753
|
+
);
|
|
2705
2754
|
if (isWriteTool(toolName) && filePath) {
|
|
2706
2755
|
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
|
|
2707
2756
|
}
|
|
2757
|
+
if (isObserveMode) {
|
|
2758
|
+
return {
|
|
2759
|
+
approved: true,
|
|
2760
|
+
checkedBy: "audit",
|
|
2761
|
+
observeWouldBlock: true,
|
|
2762
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2708
2765
|
return {
|
|
2709
2766
|
approved: false,
|
|
2710
2767
|
reason: dlpReason,
|
|
@@ -2717,6 +2774,31 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2717
2774
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
2718
2775
|
}
|
|
2719
2776
|
}
|
|
2777
|
+
if (isObserveMode) {
|
|
2778
|
+
if (!isIgnoredTool(toolName)) {
|
|
2779
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
2780
|
+
const wouldBlock = policyResult.decision === "block";
|
|
2781
|
+
if (!isManual)
|
|
2782
|
+
appendLocalAudit(
|
|
2783
|
+
toolName,
|
|
2784
|
+
args,
|
|
2785
|
+
"allow",
|
|
2786
|
+
wouldBlock ? "observe-mode-would-block" : "observe-mode",
|
|
2787
|
+
meta,
|
|
2788
|
+
hashAuditArgs
|
|
2789
|
+
);
|
|
2790
|
+
return {
|
|
2791
|
+
approved: true,
|
|
2792
|
+
checkedBy: "audit",
|
|
2793
|
+
...wouldBlock && {
|
|
2794
|
+
observeWouldBlock: true,
|
|
2795
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
2796
|
+
ruleHit: policyResult.ruleName
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
return { approved: true, checkedBy: "audit" };
|
|
2801
|
+
}
|
|
2720
2802
|
if (config.settings.mode === "audit") {
|
|
2721
2803
|
if (!isIgnoredTool(toolName)) {
|
|
2722
2804
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
@@ -2744,14 +2826,34 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2744
2826
|
return { approved: true, checkedBy: "local-policy" };
|
|
2745
2827
|
}
|
|
2746
2828
|
if (policyResult.decision === "block") {
|
|
2747
|
-
if (
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2829
|
+
if (policyResult.dependsOnStatePredicates?.length) {
|
|
2830
|
+
const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
|
|
2831
|
+
const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
|
|
2832
|
+
if (stateResults === null && !isManual) {
|
|
2833
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
2834
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2835
|
+
event: "state-check-fail-open",
|
|
2836
|
+
tool: toolName,
|
|
2837
|
+
rule: policyResult.ruleName,
|
|
2838
|
+
predicates: policyResult.dependsOnStatePredicates,
|
|
2839
|
+
reason: "daemon unreachable or /state/check timed out \u2014 block rule downgraded to review"
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
if (predicatesMet && policyResult.recoveryCommand) {
|
|
2843
|
+
statefulRecoveryCommand = policyResult.recoveryCommand;
|
|
2844
|
+
}
|
|
2845
|
+
} else {
|
|
2846
|
+
if (!isManual)
|
|
2847
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
2848
|
+
return {
|
|
2849
|
+
approved: false,
|
|
2850
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
2851
|
+
blockedBy: "local-config",
|
|
2852
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
2853
|
+
ruleHit: policyResult.ruleName,
|
|
2854
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2755
2857
|
}
|
|
2756
2858
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
2757
2859
|
policyMatchedField = policyResult.matchedField;
|
|
@@ -2858,7 +2960,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2858
2960
|
meta,
|
|
2859
2961
|
riskMetadata,
|
|
2860
2962
|
options?.activityId,
|
|
2861
|
-
options?.cwd
|
|
2963
|
+
options?.cwd,
|
|
2964
|
+
statefulRecoveryCommand
|
|
2862
2965
|
);
|
|
2863
2966
|
daemonEntryId = entry.id;
|
|
2864
2967
|
daemonAllowCount = entry.allowCount;
|
|
@@ -2919,20 +3022,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2919
3022
|
if (daemonEntryId && (approvers.browser || approvers.terminal)) {
|
|
2920
3023
|
racePromises.push(
|
|
2921
3024
|
(async () => {
|
|
2922
|
-
const {
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3025
|
+
const {
|
|
3026
|
+
decision: daemonDecision,
|
|
3027
|
+
source: decisionSource,
|
|
3028
|
+
reason: daemonReason
|
|
3029
|
+
} = await waitForDaemonDecision(daemonEntryId, signal);
|
|
2926
3030
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
2927
3031
|
const isApproved = daemonDecision === "allow";
|
|
2928
|
-
const
|
|
3032
|
+
const isRedirect = decisionSource === "terminal-redirect";
|
|
3033
|
+
const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
|
|
2929
3034
|
const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
|
|
2930
3035
|
return {
|
|
2931
3036
|
approved: isApproved,
|
|
2932
|
-
reason: isApproved ? void 0 :
|
|
3037
|
+
reason: isApproved ? void 0 : (
|
|
3038
|
+
// Use the redirect reason from the tail when choice [2] was selected;
|
|
3039
|
+
// otherwise fall back to the generic rejection message.
|
|
3040
|
+
isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
|
|
3041
|
+
),
|
|
2933
3042
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
2934
3043
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
2935
|
-
blockedByLabel: `User Decision (${via})`,
|
|
3044
|
+
blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
|
|
2936
3045
|
decisionSource: src
|
|
2937
3046
|
};
|
|
2938
3047
|
})()
|