@node9/proxy 1.63.0 → 1.64.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 +908 -336
- package/dist/cli.mjs +906 -334
- package/dist/dashboard.mjs +82 -25
- package/dist/index.js +144 -39
- package/dist/index.mjs +144 -39
- package/dist/scan-ink.mjs +6 -5
- package/package.json +1 -1
package/dist/dashboard.mjs
CHANGED
|
@@ -1959,6 +1959,7 @@ var init_dist = __esm({
|
|
|
1959
1959
|
name: "redis",
|
|
1960
1960
|
description: "Protects Redis instances from destructive AI operations",
|
|
1961
1961
|
aliases: [],
|
|
1962
|
+
_scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
|
|
1962
1963
|
smartRules: [
|
|
1963
1964
|
{
|
|
1964
1965
|
name: "shield:redis:block-flushall",
|
|
@@ -1967,7 +1968,7 @@ var init_dist = __esm({
|
|
|
1967
1968
|
{
|
|
1968
1969
|
field: "command",
|
|
1969
1970
|
op: "matches",
|
|
1970
|
-
value: "\\bFLUSHALL\\b",
|
|
1971
|
+
value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
|
|
1971
1972
|
flags: "i"
|
|
1972
1973
|
}
|
|
1973
1974
|
],
|
|
@@ -1981,7 +1982,7 @@ var init_dist = __esm({
|
|
|
1981
1982
|
{
|
|
1982
1983
|
field: "command",
|
|
1983
1984
|
op: "matches",
|
|
1984
|
-
value: "\\bFLUSHDB\\b",
|
|
1985
|
+
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
1985
1986
|
flags: "i"
|
|
1986
1987
|
}
|
|
1987
1988
|
],
|
|
@@ -1995,7 +1996,7 @@ var init_dist = __esm({
|
|
|
1995
1996
|
{
|
|
1996
1997
|
field: "command",
|
|
1997
1998
|
op: "matches",
|
|
1998
|
-
value: "\\
|
|
1999
|
+
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
1999
2000
|
flags: "i"
|
|
2000
2001
|
}
|
|
2001
2002
|
],
|
|
@@ -2009,7 +2010,7 @@ var init_dist = __esm({
|
|
|
2009
2010
|
{
|
|
2010
2011
|
field: "command",
|
|
2011
2012
|
op: "matches",
|
|
2012
|
-
value: "\\bCONFIG\\s+SET\\b",
|
|
2013
|
+
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
2013
2014
|
flags: "i"
|
|
2014
2015
|
}
|
|
2015
2016
|
],
|
|
@@ -2023,7 +2024,7 @@ var init_dist = __esm({
|
|
|
2023
2024
|
{
|
|
2024
2025
|
field: "command",
|
|
2025
2026
|
op: "matches",
|
|
2026
|
-
value: "
|
|
2027
|
+
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
2027
2028
|
flags: "i"
|
|
2028
2029
|
}
|
|
2029
2030
|
],
|
|
@@ -2302,6 +2303,7 @@ var init_config_schema = __esm({
|
|
|
2302
2303
|
// nudge-only (default), and the scan cadence in minutes.
|
|
2303
2304
|
mcpAutoWrap: z.boolean().optional(),
|
|
2304
2305
|
mcpReconcileIntervalMinutes: z.number().positive().optional(),
|
|
2306
|
+
mcpStaleAfterDays: z.number().min(0).optional(),
|
|
2305
2307
|
cloudSyncIntervalHours: z.number().positive().optional(),
|
|
2306
2308
|
// Seconds-granular override for the cloud policy sync cadence. Wins over
|
|
2307
2309
|
// cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
|
|
@@ -2466,6 +2468,7 @@ var init_config = __esm({
|
|
|
2466
2468
|
init_managed();
|
|
2467
2469
|
init_build();
|
|
2468
2470
|
init_trusted_hosts();
|
|
2471
|
+
init_dist();
|
|
2469
2472
|
DANGEROUS_WORDS = [
|
|
2470
2473
|
"mkfs",
|
|
2471
2474
|
// formats/wipes a filesystem partition
|
|
@@ -2967,6 +2970,50 @@ var init_scan_watermark = __esm({
|
|
|
2967
2970
|
}
|
|
2968
2971
|
});
|
|
2969
2972
|
|
|
2973
|
+
// src/audit/decision.ts
|
|
2974
|
+
function classifyDecision(a, b) {
|
|
2975
|
+
const isRow = !!a && typeof a === "object";
|
|
2976
|
+
const decision = isRow ? a.decision : a;
|
|
2977
|
+
const attribution = isRow ? a.checkedBy ?? a.source : b;
|
|
2978
|
+
const raw = typeof decision === "string" ? decision : String(decision ?? "");
|
|
2979
|
+
const src = typeof attribution === "string" ? attribution.toLowerCase() : "";
|
|
2980
|
+
const d = raw.toLowerCase();
|
|
2981
|
+
if (src && has(src, "observe-mode")) {
|
|
2982
|
+
return { outcome: "observe", label: "Would block", raw };
|
|
2983
|
+
}
|
|
2984
|
+
if (d === "dlp") return { outcome: "info", label: "Finding", raw };
|
|
2985
|
+
if (d === "mcp-discovered") return { outcome: "info", label: "Info", raw };
|
|
2986
|
+
if (d === "allow" || d === "allowed") {
|
|
2987
|
+
if (src === "post-hook") return { outcome: "allow", label: "Ran", raw };
|
|
2988
|
+
if (HUMAN_SOURCES.has(src)) {
|
|
2989
|
+
return { outcome: "allow", label: "Approved", raw };
|
|
2990
|
+
}
|
|
2991
|
+
return { outcome: "allow", label: "Auto-allowed", raw };
|
|
2992
|
+
}
|
|
2993
|
+
if (d === "deny" || d === "auto-deny" || d === "block") {
|
|
2994
|
+
if (TIMEOUT_SOURCES.has(src)) {
|
|
2995
|
+
return { outcome: "deny", label: "Timed out", raw };
|
|
2996
|
+
}
|
|
2997
|
+
if (HUMAN_SOURCES.has(src)) {
|
|
2998
|
+
return { outcome: "deny", label: "Denied", raw };
|
|
2999
|
+
}
|
|
3000
|
+
return { outcome: "deny", label: "Blocked", raw };
|
|
3001
|
+
}
|
|
3002
|
+
if (d === "review" || d === "pending") {
|
|
3003
|
+
return { outcome: "info", label: "Pending", raw };
|
|
3004
|
+
}
|
|
3005
|
+
return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
|
|
3006
|
+
}
|
|
3007
|
+
var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
|
|
3008
|
+
var init_decision = __esm({
|
|
3009
|
+
"src/audit/decision.ts"() {
|
|
3010
|
+
"use strict";
|
|
3011
|
+
HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
|
|
3012
|
+
TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
|
|
3013
|
+
has = (s, needle) => s.includes(needle);
|
|
3014
|
+
}
|
|
3015
|
+
});
|
|
3016
|
+
|
|
2970
3017
|
// src/cli/aggregate/report-audit.ts
|
|
2971
3018
|
import fs5 from "fs";
|
|
2972
3019
|
import os6 from "os";
|
|
@@ -3062,8 +3109,15 @@ function parseAuditLog(logPath) {
|
|
|
3062
3109
|
}
|
|
3063
3110
|
});
|
|
3064
3111
|
}
|
|
3065
|
-
function
|
|
3066
|
-
|
|
3112
|
+
function viewOf(e) {
|
|
3113
|
+
const outcome = classifyDecision(e).outcome;
|
|
3114
|
+
const observed = outcome === "observe";
|
|
3115
|
+
return {
|
|
3116
|
+
observed,
|
|
3117
|
+
ran: outcome === "allow" || observed,
|
|
3118
|
+
blocked: outcome === "deny" || outcome === "unknown",
|
|
3119
|
+
info: outcome === "info"
|
|
3120
|
+
};
|
|
3067
3121
|
}
|
|
3068
3122
|
function isDlp(checkedBy) {
|
|
3069
3123
|
return !!checkedBy?.includes("dlp");
|
|
@@ -3520,12 +3574,12 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3520
3574
|
const priorStart = new Date(start.getTime() - periodMs);
|
|
3521
3575
|
const priorEntries = allEntries.filter((e) => {
|
|
3522
3576
|
if (e.source === "post-hook") return false;
|
|
3577
|
+
if (e.source === "response-dlp") return false;
|
|
3578
|
+
if (typeof e.decision !== "string") return false;
|
|
3523
3579
|
const ts = new Date(e.ts);
|
|
3524
3580
|
return ts >= priorStart && ts <= priorEnd;
|
|
3525
3581
|
});
|
|
3526
|
-
const priorBlocked = priorEntries.filter(
|
|
3527
|
-
(e) => typeof e.decision === "string" && !isAllow(e.decision)
|
|
3528
|
-
).length;
|
|
3582
|
+
const priorBlocked = priorEntries.filter((e) => viewOf(e).blocked).length;
|
|
3529
3583
|
const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
|
|
3530
3584
|
const excludeTests = opts.excludeTests === true;
|
|
3531
3585
|
const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
|
|
@@ -3568,15 +3622,17 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3568
3622
|
let dimInjectionBlocked = 0;
|
|
3569
3623
|
for (const e of entries) {
|
|
3570
3624
|
if (superseded.has(supersedeKey(e))) continue;
|
|
3571
|
-
const
|
|
3625
|
+
const view = viewOf(e);
|
|
3572
3626
|
const dateKey = e.ts.slice(0, 10);
|
|
3573
3627
|
const userInteracted = e.source === "daemon";
|
|
3574
|
-
if (
|
|
3575
|
-
if (
|
|
3628
|
+
if (view.observed) {
|
|
3629
|
+
if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
|
|
3630
|
+
} else if (view.info) {
|
|
3631
|
+
} else if (userInteracted) {
|
|
3632
|
+
if (view.ran) userApproved++;
|
|
3576
3633
|
else userDenied++;
|
|
3577
|
-
} else if (
|
|
3634
|
+
} else if (view.blocked) {
|
|
3578
3635
|
if (e.checkedBy === "timeout") timedOut++;
|
|
3579
|
-
else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
|
|
3580
3636
|
else if (isDlp(e.checkedBy)) dlpBlocked++;
|
|
3581
3637
|
else if (e.checkedBy === "local-decision") userDenied++;
|
|
3582
3638
|
else if (e.checkedBy !== "loop-detected") hardBlocked++;
|
|
@@ -3588,7 +3644,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3588
3644
|
if (cb.includes("would-block") && (cb.includes("pii") || cb.includes("dlp"))) {
|
|
3589
3645
|
dimDataObserved++;
|
|
3590
3646
|
}
|
|
3591
|
-
if (
|
|
3647
|
+
if (view.blocked && !userInteracted) {
|
|
3592
3648
|
switch (dimensionOfBlock(cb, e.ruleName ?? "")) {
|
|
3593
3649
|
case "network":
|
|
3594
3650
|
dimNetworkBlocked++;
|
|
@@ -3606,15 +3662,15 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3606
3662
|
}
|
|
3607
3663
|
const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
|
|
3608
3664
|
t.calls++;
|
|
3609
|
-
if (
|
|
3665
|
+
if (view.blocked) t.blocked++;
|
|
3610
3666
|
toolMap.set(e.tool, t);
|
|
3611
|
-
if (
|
|
3667
|
+
if (view.blocked) {
|
|
3612
3668
|
const key = e.checkedBy ?? (e.source === "daemon" ? "local-decision" : null);
|
|
3613
3669
|
if (key) {
|
|
3614
3670
|
blockMap.set(key, (blockMap.get(key) ?? 0) + 1);
|
|
3615
3671
|
}
|
|
3616
3672
|
}
|
|
3617
|
-
if (
|
|
3673
|
+
if ((view.blocked || view.observed) && e.ruleName) {
|
|
3618
3674
|
ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
|
|
3619
3675
|
}
|
|
3620
3676
|
if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
|
|
@@ -3623,7 +3679,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3623
3679
|
hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
|
|
3624
3680
|
const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
|
|
3625
3681
|
d.calls++;
|
|
3626
|
-
if (
|
|
3682
|
+
if (view.blocked) d.blocked++;
|
|
3627
3683
|
dailyMap.set(dateKey, d);
|
|
3628
3684
|
}
|
|
3629
3685
|
for (const e of allEntries) {
|
|
@@ -3699,6 +3755,7 @@ var init_report_audit = __esm({
|
|
|
3699
3755
|
init_costSync();
|
|
3700
3756
|
init_litellm();
|
|
3701
3757
|
init_cost_codex();
|
|
3758
|
+
init_decision();
|
|
3702
3759
|
TEST_COMMAND_RE = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
|
|
3703
3760
|
SUPERSEDE_WINDOW_MS = 6e4;
|
|
3704
3761
|
GEMINI_FALLBACK_MODELS2 = ["gemini-2.5-flash", "gemini-2.0-flash"];
|
|
@@ -4874,20 +4931,20 @@ function aggregateAudit(entries, startMs, endMs = Date.now()) {
|
|
|
4874
4931
|
for (const e of inWindow) {
|
|
4875
4932
|
if (e.sessionId) sessionSet.add(e.sessionId);
|
|
4876
4933
|
if (e.mcpServer) mcpSet.add(e.mcpServer);
|
|
4877
|
-
const
|
|
4934
|
+
const isAllow = e.decision === "allow" || e.decision === "observe-allow";
|
|
4878
4935
|
const isReview = e.decision === "review";
|
|
4879
4936
|
const isLoop = e.checkedBy === "loop-detected";
|
|
4880
4937
|
const isDlp2 = !!(e.checkedBy && e.checkedBy.toLowerCase().includes("dlp"));
|
|
4881
|
-
if (
|
|
4938
|
+
if (isAllow) allow++;
|
|
4882
4939
|
else if (isReview) review++;
|
|
4883
4940
|
else block++;
|
|
4884
4941
|
if (isLoop) loops++;
|
|
4885
4942
|
if (isDlp2) dlpHits++;
|
|
4886
4943
|
const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
|
|
4887
4944
|
t.calls++;
|
|
4888
|
-
if (!
|
|
4945
|
+
if (!isAllow && !isReview) t.blocked++;
|
|
4889
4946
|
toolMap.set(e.tool, t);
|
|
4890
|
-
if (!
|
|
4947
|
+
if (!isAllow && e.checkedBy) {
|
|
4891
4948
|
blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
|
|
4892
4949
|
}
|
|
4893
4950
|
if (TEST_TOOLS.has(e.tool) && typeof e.args?.command === "string") {
|
|
@@ -4895,7 +4952,7 @@ function aggregateAudit(entries, startMs, endMs = Date.now()) {
|
|
|
4895
4952
|
if (head && /^[a-zA-Z0-9._-]+$/.test(head)) {
|
|
4896
4953
|
const s = shellMap.get(head) ?? { count: 0, blocked: 0 };
|
|
4897
4954
|
s.count++;
|
|
4898
|
-
if (!
|
|
4955
|
+
if (!isAllow && !isReview) s.blocked++;
|
|
4899
4956
|
shellMap.set(head, s);
|
|
4900
4957
|
}
|
|
4901
4958
|
}
|
package/dist/index.js
CHANGED
|
@@ -325,6 +325,7 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
325
325
|
// nudge-only (default), and the scan cadence in minutes.
|
|
326
326
|
mcpAutoWrap: import_zod.z.boolean().optional(),
|
|
327
327
|
mcpReconcileIntervalMinutes: import_zod.z.number().positive().optional(),
|
|
328
|
+
mcpStaleAfterDays: import_zod.z.number().min(0).optional(),
|
|
328
329
|
cloudSyncIntervalHours: import_zod.z.number().positive().optional(),
|
|
329
330
|
// Seconds-granular override for the cloud policy sync cadence. Wins over
|
|
330
331
|
// cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
|
|
@@ -3335,6 +3336,7 @@ var redis_default = {
|
|
|
3335
3336
|
name: "redis",
|
|
3336
3337
|
description: "Protects Redis instances from destructive AI operations",
|
|
3337
3338
|
aliases: [],
|
|
3339
|
+
_scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
|
|
3338
3340
|
smartRules: [
|
|
3339
3341
|
{
|
|
3340
3342
|
name: "shield:redis:block-flushall",
|
|
@@ -3343,7 +3345,7 @@ var redis_default = {
|
|
|
3343
3345
|
{
|
|
3344
3346
|
field: "command",
|
|
3345
3347
|
op: "matches",
|
|
3346
|
-
value: "\\bFLUSHALL\\b",
|
|
3348
|
+
value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
|
|
3347
3349
|
flags: "i"
|
|
3348
3350
|
}
|
|
3349
3351
|
],
|
|
@@ -3357,7 +3359,7 @@ var redis_default = {
|
|
|
3357
3359
|
{
|
|
3358
3360
|
field: "command",
|
|
3359
3361
|
op: "matches",
|
|
3360
|
-
value: "\\bFLUSHDB\\b",
|
|
3362
|
+
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
3361
3363
|
flags: "i"
|
|
3362
3364
|
}
|
|
3363
3365
|
],
|
|
@@ -3371,7 +3373,7 @@ var redis_default = {
|
|
|
3371
3373
|
{
|
|
3372
3374
|
field: "command",
|
|
3373
3375
|
op: "matches",
|
|
3374
|
-
value: "\\
|
|
3376
|
+
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
3375
3377
|
flags: "i"
|
|
3376
3378
|
}
|
|
3377
3379
|
],
|
|
@@ -3385,7 +3387,7 @@ var redis_default = {
|
|
|
3385
3387
|
{
|
|
3386
3388
|
field: "command",
|
|
3387
3389
|
op: "matches",
|
|
3388
|
-
value: "\\bCONFIG\\s+SET\\b",
|
|
3390
|
+
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
3389
3391
|
flags: "i"
|
|
3390
3392
|
}
|
|
3391
3393
|
],
|
|
@@ -3399,7 +3401,7 @@ var redis_default = {
|
|
|
3399
3401
|
{
|
|
3400
3402
|
field: "command",
|
|
3401
3403
|
op: "matches",
|
|
3402
|
-
value: "
|
|
3404
|
+
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
3403
3405
|
flags: "i"
|
|
3404
3406
|
}
|
|
3405
3407
|
],
|
|
@@ -4068,6 +4070,49 @@ function getActiveEnvironment(config) {
|
|
|
4068
4070
|
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
4069
4071
|
return config.environments[env] ?? null;
|
|
4070
4072
|
}
|
|
4073
|
+
var cacheReadFailureLogged = false;
|
|
4074
|
+
function readRulesCacheResilient(cacheFile) {
|
|
4075
|
+
let existed = false;
|
|
4076
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
4077
|
+
let content;
|
|
4078
|
+
try {
|
|
4079
|
+
content = import_fs4.default.readFileSync(cacheFile, "utf-8");
|
|
4080
|
+
existed = true;
|
|
4081
|
+
} catch (err) {
|
|
4082
|
+
if (err.code === "ENOENT") return {};
|
|
4083
|
+
continue;
|
|
4084
|
+
}
|
|
4085
|
+
try {
|
|
4086
|
+
return JSON.parse(content);
|
|
4087
|
+
} catch {
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
if (existed) {
|
|
4091
|
+
const backup = import_path4.default.join(import_path4.default.dirname(cacheFile), "rules-cache.last-good.json");
|
|
4092
|
+
if (backup !== cacheFile) {
|
|
4093
|
+
try {
|
|
4094
|
+
const raw = JSON.parse(import_fs4.default.readFileSync(backup, "utf-8"));
|
|
4095
|
+
logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
|
|
4096
|
+
return raw;
|
|
4097
|
+
} catch {
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
logCacheReadIssue(cacheFile, "RULES_CACHE_UNREADABLE");
|
|
4101
|
+
}
|
|
4102
|
+
return {};
|
|
4103
|
+
}
|
|
4104
|
+
function logCacheReadIssue(cacheFile, kind) {
|
|
4105
|
+
if (cacheReadFailureLogged) return;
|
|
4106
|
+
cacheReadFailureLogged = true;
|
|
4107
|
+
try {
|
|
4108
|
+
import_fs4.default.appendFileSync(
|
|
4109
|
+
import_path4.default.join(import_os4.default.homedir(), ".node9", "hook-debug.log"),
|
|
4110
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
|
|
4111
|
+
`
|
|
4112
|
+
);
|
|
4113
|
+
} catch {
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4071
4116
|
function getConfig(cwd) {
|
|
4072
4117
|
if (!cwd && cachedConfig) return cachedConfig;
|
|
4073
4118
|
const globalPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "config.json");
|
|
@@ -4135,6 +4180,7 @@ function getConfig(cwd) {
|
|
|
4135
4180
|
if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
|
|
4136
4181
|
if (s.mcpReconcileIntervalMinutes !== void 0)
|
|
4137
4182
|
mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
|
|
4183
|
+
if (s.mcpStaleAfterDays !== void 0) mergedSettings.mcpStaleAfterDays = s.mcpStaleAfterDays;
|
|
4138
4184
|
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
4139
4185
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
4140
4186
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -4144,12 +4190,13 @@ function getConfig(cwd) {
|
|
|
4144
4190
|
if (p.smartRules) {
|
|
4145
4191
|
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
4146
4192
|
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
4147
|
-
const
|
|
4193
|
+
const localRules = p.smartRules.map(({ pinned: _pinned, ...r }) => r);
|
|
4194
|
+
const userRuleNames = new Set(localRules.filter((r) => r.name).map((r) => r.name));
|
|
4148
4195
|
const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
|
|
4149
4196
|
const filteredNonBlocks = defaultNonBlocks.filter(
|
|
4150
4197
|
(r) => !r.name || !userRuleNames.has(r.name)
|
|
4151
4198
|
);
|
|
4152
|
-
mergedPolicy.smartRules = [...filteredBlocks, ...
|
|
4199
|
+
mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
|
|
4153
4200
|
}
|
|
4154
4201
|
if (p.snapshot) {
|
|
4155
4202
|
const s2 = p.snapshot;
|
|
@@ -4214,10 +4261,12 @@ function getConfig(cwd) {
|
|
|
4214
4261
|
applyLayer(globalConfig);
|
|
4215
4262
|
applyLayer(projectConfig);
|
|
4216
4263
|
let cloudManagedShields = [];
|
|
4264
|
+
let modeCloudControlled = false;
|
|
4265
|
+
let modeCloudStaged = false;
|
|
4217
4266
|
{
|
|
4218
4267
|
const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
|
|
4219
4268
|
try {
|
|
4220
|
-
const raw =
|
|
4269
|
+
const raw = readRulesCacheResilient(cacheFile);
|
|
4221
4270
|
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
4222
4271
|
applyLayer({ policy: { smartRules: raw.rules } });
|
|
4223
4272
|
}
|
|
@@ -4234,6 +4283,9 @@ function getConfig(cwd) {
|
|
|
4234
4283
|
locked.includes("mode")
|
|
4235
4284
|
);
|
|
4236
4285
|
}
|
|
4286
|
+
if (typeof mc.mode === "string" || locked.includes("mode")) {
|
|
4287
|
+
modeCloudControlled = true;
|
|
4288
|
+
}
|
|
4237
4289
|
if (mc.egress && typeof mc.egress === "object") {
|
|
4238
4290
|
const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : void 0;
|
|
4239
4291
|
mergedPolicy.egress = applyManagedEgress(
|
|
@@ -4332,19 +4384,28 @@ function getConfig(cwd) {
|
|
|
4332
4384
|
}
|
|
4333
4385
|
if (raw.shadowMode === true) {
|
|
4334
4386
|
mergedSettings.mode = "observe";
|
|
4387
|
+
modeCloudStaged = true;
|
|
4335
4388
|
}
|
|
4336
4389
|
} catch {
|
|
4337
4390
|
}
|
|
4338
4391
|
}
|
|
4339
4392
|
const shieldOverrides = readShieldOverrides();
|
|
4340
4393
|
const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
|
|
4394
|
+
const cloudManagedSet = new Set(cloudManagedShields);
|
|
4341
4395
|
for (const shieldName of activeShieldNames) {
|
|
4342
|
-
const
|
|
4396
|
+
const isCloudMandated = cloudManagedSet.has(shieldName);
|
|
4397
|
+
const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
|
|
4343
4398
|
if (!shield) continue;
|
|
4344
4399
|
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4345
|
-
const ruleOverrides = shieldOverrides[shieldName] ?? {};
|
|
4400
|
+
const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
|
|
4346
4401
|
for (const rule of shield.smartRules) {
|
|
4347
|
-
|
|
4402
|
+
const collides = rule.name ? existingRuleNames.has(rule.name) : false;
|
|
4403
|
+
if (isCloudMandated) {
|
|
4404
|
+
if (collides) {
|
|
4405
|
+
mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
|
|
4406
|
+
}
|
|
4407
|
+
mergedPolicy.smartRules.push({ ...rule, pinned: true });
|
|
4408
|
+
} else if (!collides) {
|
|
4348
4409
|
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
4349
4410
|
mergedPolicy.smartRules.push(
|
|
4350
4411
|
overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
|
|
@@ -4360,7 +4421,27 @@ function getConfig(cwd) {
|
|
|
4360
4421
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4361
4422
|
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
4362
4423
|
}
|
|
4363
|
-
|
|
4424
|
+
const envMode = process.env.NODE9_MODE;
|
|
4425
|
+
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
4426
|
+
mergedSettings.mode = envMode;
|
|
4427
|
+
}
|
|
4428
|
+
if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4429
|
+
mergedSettings.mode = "standard";
|
|
4430
|
+
}
|
|
4431
|
+
const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4432
|
+
if (modeCloudControlled && mergedSettings.mode === "strict") {
|
|
4433
|
+
for (const name of Object.keys(mergedEnvironments)) {
|
|
4434
|
+
if (mergedEnvironments[name]?.requireApproval === false) {
|
|
4435
|
+
const cleaned = { ...mergedEnvironments[name] };
|
|
4436
|
+
delete cleaned.requireApproval;
|
|
4437
|
+
mergedEnvironments[name] = cleaned;
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
if (managedFloorActive) {
|
|
4442
|
+
mergedPolicy.ignoredTools = [...DEFAULT_CONFIG.policy.ignoredTools];
|
|
4443
|
+
mergedPolicy.sandboxPaths = [...DEFAULT_CONFIG.policy.sandboxPaths];
|
|
4444
|
+
}
|
|
4364
4445
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
4365
4446
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
4366
4447
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
@@ -4748,6 +4829,18 @@ function isDaemonRunning() {
|
|
|
4748
4829
|
}
|
|
4749
4830
|
return false;
|
|
4750
4831
|
}
|
|
4832
|
+
async function daemonHasInteractiveApprover(timeoutMs = 400) {
|
|
4833
|
+
try {
|
|
4834
|
+
const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/approver`, {
|
|
4835
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
4836
|
+
});
|
|
4837
|
+
if (!res.ok) return false;
|
|
4838
|
+
const body = await res.json();
|
|
4839
|
+
return body.interactive === true;
|
|
4840
|
+
} catch {
|
|
4841
|
+
return false;
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4751
4844
|
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
|
|
4752
4845
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
4753
4846
|
const ctrl = new AbortController();
|
|
@@ -5444,6 +5537,12 @@ function isNetworkTool(toolName, args) {
|
|
|
5444
5537
|
function notifyActivity(data) {
|
|
5445
5538
|
return notifyActivitySocket(data);
|
|
5446
5539
|
}
|
|
5540
|
+
async function hasReachableHumanApprover(opts) {
|
|
5541
|
+
const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
5542
|
+
const nativeReachable = !opts.calledFromDaemon && opts.approvers.native !== false && hasDisplay;
|
|
5543
|
+
if (nativeReachable) return true;
|
|
5544
|
+
return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
|
|
5545
|
+
}
|
|
5447
5546
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
5448
5547
|
if (!options?.calledFromDaemon) {
|
|
5449
5548
|
const actId = (0, import_crypto4.randomUUID)();
|
|
@@ -5758,6 +5857,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5758
5857
|
return { approved: true, checkedBy: "local-policy" };
|
|
5759
5858
|
}
|
|
5760
5859
|
if (policyResult.decision === "block") {
|
|
5860
|
+
const daemonUp = isDaemonRunning();
|
|
5861
|
+
let humanApproverReachable = false;
|
|
5862
|
+
if (!policyResult.dependsOnStatePredicates?.length && daemonUp && !isTestEnv2) {
|
|
5863
|
+
humanApproverReachable = await hasReachableHumanApprover({
|
|
5864
|
+
approvers,
|
|
5865
|
+
calledFromDaemon: options?.calledFromDaemon
|
|
5866
|
+
});
|
|
5867
|
+
}
|
|
5868
|
+
const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
|
|
5869
|
+
const hardBlock = () => {
|
|
5870
|
+
if (!isManual)
|
|
5871
|
+
appendLocalAudit(
|
|
5872
|
+
toolName,
|
|
5873
|
+
args,
|
|
5874
|
+
"deny",
|
|
5875
|
+
"smart-rule-block",
|
|
5876
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
5877
|
+
hashAuditArgs
|
|
5878
|
+
);
|
|
5879
|
+
return {
|
|
5880
|
+
approved: false,
|
|
5881
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
5882
|
+
blockedBy: "local-config",
|
|
5883
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
5884
|
+
ruleHit: policyResult.ruleName,
|
|
5885
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
5886
|
+
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
5887
|
+
};
|
|
5888
|
+
};
|
|
5761
5889
|
if (policyResult.dependsOnStatePredicates?.length) {
|
|
5762
5890
|
const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
|
|
5763
5891
|
const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
|
|
@@ -5774,7 +5902,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5774
5902
|
if (predicatesMet && policyResult.recoveryCommand) {
|
|
5775
5903
|
statefulRecoveryCommand = policyResult.recoveryCommand;
|
|
5776
5904
|
}
|
|
5777
|
-
} else if (
|
|
5905
|
+
} else if (mayDowngrade) {
|
|
5778
5906
|
if (!isManual)
|
|
5779
5907
|
appendLocalAudit(
|
|
5780
5908
|
toolName,
|
|
@@ -5796,36 +5924,13 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5796
5924
|
};
|
|
5797
5925
|
}
|
|
5798
5926
|
} else {
|
|
5799
|
-
|
|
5800
|
-
appendLocalAudit(
|
|
5801
|
-
toolName,
|
|
5802
|
-
args,
|
|
5803
|
-
"deny",
|
|
5804
|
-
"smart-rule-block",
|
|
5805
|
-
// Include policyResult.ruleName so the [2] Report SHIELDS
|
|
5806
|
-
// panel can attribute this block to its specific shield
|
|
5807
|
-
// (e.g. `shield:project-jail:block-read-ssh`) via the
|
|
5808
|
-
// rule→shield map. checkedBy stays as the generic
|
|
5809
|
-
// `smart-rule-block` for backward compat with existing
|
|
5810
|
-
// log readers.
|
|
5811
|
-
{ ...meta, ruleName: policyResult.ruleName },
|
|
5812
|
-
hashAuditArgs
|
|
5813
|
-
);
|
|
5814
|
-
return {
|
|
5815
|
-
approved: false,
|
|
5816
|
-
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
5817
|
-
blockedBy: "local-config",
|
|
5818
|
-
blockedByLabel: policyResult.blockedByLabel,
|
|
5819
|
-
ruleHit: policyResult.ruleName,
|
|
5820
|
-
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
5821
|
-
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
5822
|
-
};
|
|
5927
|
+
return hardBlock();
|
|
5823
5928
|
}
|
|
5824
5929
|
}
|
|
5825
5930
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
5826
5931
|
policyMatchedField = policyResult.matchedField;
|
|
5827
5932
|
policyMatchedWord = policyResult.matchedWord;
|
|
5828
|
-
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
5933
|
+
if (policyResult.ruleName || policyResult.tier === 7) localSmartRuleMatched = true;
|
|
5829
5934
|
if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
|
|
5830
5935
|
else if (policyResult.reason) policyRuleDescription = policyResult.reason;
|
|
5831
5936
|
riskMetadata = computeRiskMetadata(
|
|
@@ -5837,7 +5942,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5837
5942
|
policyResult.ruleName
|
|
5838
5943
|
);
|
|
5839
5944
|
if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
|
|
5840
|
-
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
5945
|
+
const persistent = policyResult.ruleName || policyResult.tier === 7 ? null : getPersistentDecision(toolName);
|
|
5841
5946
|
if (persistent === "allow" && !appPermReview) {
|
|
5842
5947
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
5843
5948
|
return { approved: true, checkedBy: "persistent" };
|