@node9/proxy 1.63.0 → 1.65.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 +1787 -943
- package/dist/cli.mjs +1770 -926
- package/dist/dashboard.mjs +84 -26
- package/dist/index.js +161 -41
- package/dist/index.mjs +161 -41
- 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)
|
|
@@ -2457,7 +2459,7 @@ var init_trusted_hosts = __esm({
|
|
|
2457
2459
|
});
|
|
2458
2460
|
|
|
2459
2461
|
// src/config/index.ts
|
|
2460
|
-
var DANGEROUS_WORDS, DEFAULT_CONFIG;
|
|
2462
|
+
var DANGEROUS_WORDS, DEFAULT_CONFIG, CACHE_LOG_REARM_MS;
|
|
2461
2463
|
var init_config = __esm({
|
|
2462
2464
|
"src/config/index.ts"() {
|
|
2463
2465
|
"use strict";
|
|
@@ -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
|
|
@@ -2681,6 +2684,7 @@ var init_config = __esm({
|
|
|
2681
2684
|
},
|
|
2682
2685
|
environments: {}
|
|
2683
2686
|
};
|
|
2687
|
+
CACHE_LOG_REARM_MS = 5 * 60 * 1e3;
|
|
2684
2688
|
}
|
|
2685
2689
|
});
|
|
2686
2690
|
|
|
@@ -2967,6 +2971,50 @@ var init_scan_watermark = __esm({
|
|
|
2967
2971
|
}
|
|
2968
2972
|
});
|
|
2969
2973
|
|
|
2974
|
+
// src/audit/decision.ts
|
|
2975
|
+
function classifyDecision(a, b) {
|
|
2976
|
+
const isRow = !!a && typeof a === "object";
|
|
2977
|
+
const decision = isRow ? a.decision : a;
|
|
2978
|
+
const attribution = isRow ? a.checkedBy ?? a.source : b;
|
|
2979
|
+
const raw = typeof decision === "string" ? decision : String(decision ?? "");
|
|
2980
|
+
const src = typeof attribution === "string" ? attribution.toLowerCase() : "";
|
|
2981
|
+
const d = raw.toLowerCase();
|
|
2982
|
+
if (src && has(src, "observe-mode")) {
|
|
2983
|
+
return { outcome: "observe", label: "Would block", raw };
|
|
2984
|
+
}
|
|
2985
|
+
if (d === "dlp") return { outcome: "info", label: "Finding", raw };
|
|
2986
|
+
if (d === "mcp-discovered") return { outcome: "info", label: "Info", raw };
|
|
2987
|
+
if (d === "allow" || d === "allowed") {
|
|
2988
|
+
if (src === "post-hook") return { outcome: "allow", label: "Ran", raw };
|
|
2989
|
+
if (HUMAN_SOURCES.has(src)) {
|
|
2990
|
+
return { outcome: "allow", label: "Approved", raw };
|
|
2991
|
+
}
|
|
2992
|
+
return { outcome: "allow", label: "Auto-allowed", raw };
|
|
2993
|
+
}
|
|
2994
|
+
if (d === "deny" || d === "auto-deny" || d === "block") {
|
|
2995
|
+
if (TIMEOUT_SOURCES.has(src)) {
|
|
2996
|
+
return { outcome: "deny", label: "Timed out", raw };
|
|
2997
|
+
}
|
|
2998
|
+
if (HUMAN_SOURCES.has(src)) {
|
|
2999
|
+
return { outcome: "deny", label: "Denied", raw };
|
|
3000
|
+
}
|
|
3001
|
+
return { outcome: "deny", label: "Blocked", raw };
|
|
3002
|
+
}
|
|
3003
|
+
if (d === "review" || d === "pending") {
|
|
3004
|
+
return { outcome: "info", label: "Pending", raw };
|
|
3005
|
+
}
|
|
3006
|
+
return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
|
|
3007
|
+
}
|
|
3008
|
+
var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
|
|
3009
|
+
var init_decision = __esm({
|
|
3010
|
+
"src/audit/decision.ts"() {
|
|
3011
|
+
"use strict";
|
|
3012
|
+
HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
|
|
3013
|
+
TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
|
|
3014
|
+
has = (s, needle) => s.includes(needle);
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
|
|
2970
3018
|
// src/cli/aggregate/report-audit.ts
|
|
2971
3019
|
import fs5 from "fs";
|
|
2972
3020
|
import os6 from "os";
|
|
@@ -3062,8 +3110,15 @@ function parseAuditLog(logPath) {
|
|
|
3062
3110
|
}
|
|
3063
3111
|
});
|
|
3064
3112
|
}
|
|
3065
|
-
function
|
|
3066
|
-
|
|
3113
|
+
function viewOf(e) {
|
|
3114
|
+
const outcome = classifyDecision(e).outcome;
|
|
3115
|
+
const observed = outcome === "observe";
|
|
3116
|
+
return {
|
|
3117
|
+
observed,
|
|
3118
|
+
ran: outcome === "allow" || observed,
|
|
3119
|
+
blocked: outcome === "deny" || outcome === "unknown",
|
|
3120
|
+
info: outcome === "info"
|
|
3121
|
+
};
|
|
3067
3122
|
}
|
|
3068
3123
|
function isDlp(checkedBy) {
|
|
3069
3124
|
return !!checkedBy?.includes("dlp");
|
|
@@ -3520,12 +3575,12 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3520
3575
|
const priorStart = new Date(start.getTime() - periodMs);
|
|
3521
3576
|
const priorEntries = allEntries.filter((e) => {
|
|
3522
3577
|
if (e.source === "post-hook") return false;
|
|
3578
|
+
if (e.source === "response-dlp") return false;
|
|
3579
|
+
if (typeof e.decision !== "string") return false;
|
|
3523
3580
|
const ts = new Date(e.ts);
|
|
3524
3581
|
return ts >= priorStart && ts <= priorEnd;
|
|
3525
3582
|
});
|
|
3526
|
-
const priorBlocked = priorEntries.filter(
|
|
3527
|
-
(e) => typeof e.decision === "string" && !isAllow(e.decision)
|
|
3528
|
-
).length;
|
|
3583
|
+
const priorBlocked = priorEntries.filter((e) => viewOf(e).blocked).length;
|
|
3529
3584
|
const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
|
|
3530
3585
|
const excludeTests = opts.excludeTests === true;
|
|
3531
3586
|
const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
|
|
@@ -3568,15 +3623,17 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3568
3623
|
let dimInjectionBlocked = 0;
|
|
3569
3624
|
for (const e of entries) {
|
|
3570
3625
|
if (superseded.has(supersedeKey(e))) continue;
|
|
3571
|
-
const
|
|
3626
|
+
const view = viewOf(e);
|
|
3572
3627
|
const dateKey = e.ts.slice(0, 10);
|
|
3573
3628
|
const userInteracted = e.source === "daemon";
|
|
3574
|
-
if (
|
|
3575
|
-
if (
|
|
3629
|
+
if (view.observed) {
|
|
3630
|
+
if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
|
|
3631
|
+
} else if (view.info) {
|
|
3632
|
+
} else if (userInteracted) {
|
|
3633
|
+
if (view.ran) userApproved++;
|
|
3576
3634
|
else userDenied++;
|
|
3577
|
-
} else if (
|
|
3635
|
+
} else if (view.blocked) {
|
|
3578
3636
|
if (e.checkedBy === "timeout") timedOut++;
|
|
3579
|
-
else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
|
|
3580
3637
|
else if (isDlp(e.checkedBy)) dlpBlocked++;
|
|
3581
3638
|
else if (e.checkedBy === "local-decision") userDenied++;
|
|
3582
3639
|
else if (e.checkedBy !== "loop-detected") hardBlocked++;
|
|
@@ -3588,7 +3645,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3588
3645
|
if (cb.includes("would-block") && (cb.includes("pii") || cb.includes("dlp"))) {
|
|
3589
3646
|
dimDataObserved++;
|
|
3590
3647
|
}
|
|
3591
|
-
if (
|
|
3648
|
+
if (view.blocked && !userInteracted) {
|
|
3592
3649
|
switch (dimensionOfBlock(cb, e.ruleName ?? "")) {
|
|
3593
3650
|
case "network":
|
|
3594
3651
|
dimNetworkBlocked++;
|
|
@@ -3606,15 +3663,15 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3606
3663
|
}
|
|
3607
3664
|
const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
|
|
3608
3665
|
t.calls++;
|
|
3609
|
-
if (
|
|
3666
|
+
if (view.blocked) t.blocked++;
|
|
3610
3667
|
toolMap.set(e.tool, t);
|
|
3611
|
-
if (
|
|
3668
|
+
if (view.blocked) {
|
|
3612
3669
|
const key = e.checkedBy ?? (e.source === "daemon" ? "local-decision" : null);
|
|
3613
3670
|
if (key) {
|
|
3614
3671
|
blockMap.set(key, (blockMap.get(key) ?? 0) + 1);
|
|
3615
3672
|
}
|
|
3616
3673
|
}
|
|
3617
|
-
if (
|
|
3674
|
+
if ((view.blocked || view.observed) && e.ruleName) {
|
|
3618
3675
|
ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
|
|
3619
3676
|
}
|
|
3620
3677
|
if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
|
|
@@ -3623,7 +3680,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3623
3680
|
hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
|
|
3624
3681
|
const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
|
|
3625
3682
|
d.calls++;
|
|
3626
|
-
if (
|
|
3683
|
+
if (view.blocked) d.blocked++;
|
|
3627
3684
|
dailyMap.set(dateKey, d);
|
|
3628
3685
|
}
|
|
3629
3686
|
for (const e of allEntries) {
|
|
@@ -3699,6 +3756,7 @@ var init_report_audit = __esm({
|
|
|
3699
3756
|
init_costSync();
|
|
3700
3757
|
init_litellm();
|
|
3701
3758
|
init_cost_codex();
|
|
3759
|
+
init_decision();
|
|
3702
3760
|
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
3761
|
SUPERSEDE_WINDOW_MS = 6e4;
|
|
3704
3762
|
GEMINI_FALLBACK_MODELS2 = ["gemini-2.5-flash", "gemini-2.0-flash"];
|
|
@@ -4874,20 +4932,20 @@ function aggregateAudit(entries, startMs, endMs = Date.now()) {
|
|
|
4874
4932
|
for (const e of inWindow) {
|
|
4875
4933
|
if (e.sessionId) sessionSet.add(e.sessionId);
|
|
4876
4934
|
if (e.mcpServer) mcpSet.add(e.mcpServer);
|
|
4877
|
-
const
|
|
4935
|
+
const isAllow = e.decision === "allow" || e.decision === "observe-allow";
|
|
4878
4936
|
const isReview = e.decision === "review";
|
|
4879
4937
|
const isLoop = e.checkedBy === "loop-detected";
|
|
4880
4938
|
const isDlp2 = !!(e.checkedBy && e.checkedBy.toLowerCase().includes("dlp"));
|
|
4881
|
-
if (
|
|
4939
|
+
if (isAllow) allow++;
|
|
4882
4940
|
else if (isReview) review++;
|
|
4883
4941
|
else block++;
|
|
4884
4942
|
if (isLoop) loops++;
|
|
4885
4943
|
if (isDlp2) dlpHits++;
|
|
4886
4944
|
const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
|
|
4887
4945
|
t.calls++;
|
|
4888
|
-
if (!
|
|
4946
|
+
if (!isAllow && !isReview) t.blocked++;
|
|
4889
4947
|
toolMap.set(e.tool, t);
|
|
4890
|
-
if (!
|
|
4948
|
+
if (!isAllow && e.checkedBy) {
|
|
4891
4949
|
blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
|
|
4892
4950
|
}
|
|
4893
4951
|
if (TEST_TOOLS.has(e.tool) && typeof e.args?.command === "string") {
|
|
@@ -4895,7 +4953,7 @@ function aggregateAudit(entries, startMs, endMs = Date.now()) {
|
|
|
4895
4953
|
if (head && /^[a-zA-Z0-9._-]+$/.test(head)) {
|
|
4896
4954
|
const s = shellMap.get(head) ?? { count: 0, blocked: 0 };
|
|
4897
4955
|
s.count++;
|
|
4898
|
-
if (!
|
|
4956
|
+
if (!isAllow && !isReview) s.blocked++;
|
|
4899
4957
|
shellMap.set(head, s);
|
|
4900
4958
|
}
|
|
4901
4959
|
}
|
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,61 @@ 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
|
+
function readRulesCacheResilient(cacheFile) {
|
|
4074
|
+
let existed = false;
|
|
4075
|
+
let sawReadError = 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
|
+
sawReadError = true;
|
|
4084
|
+
continue;
|
|
4085
|
+
}
|
|
4086
|
+
try {
|
|
4087
|
+
const parsed = JSON.parse(content);
|
|
4088
|
+
lastParsedRulesCache = parsed;
|
|
4089
|
+
return parsed;
|
|
4090
|
+
} catch {
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
if (existed || sawReadError) {
|
|
4094
|
+
const backup = import_path4.default.join(import_path4.default.dirname(cacheFile), "rules-cache.last-good.json");
|
|
4095
|
+
if (backup !== cacheFile) {
|
|
4096
|
+
try {
|
|
4097
|
+
const raw = JSON.parse(import_fs4.default.readFileSync(backup, "utf-8"));
|
|
4098
|
+
logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
|
|
4099
|
+
lastParsedRulesCache = raw;
|
|
4100
|
+
return raw;
|
|
4101
|
+
} catch {
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
if (lastParsedRulesCache) {
|
|
4105
|
+
logCacheReadIssue(cacheFile, "RULES_CACHE_USED_MEMORY");
|
|
4106
|
+
return lastParsedRulesCache;
|
|
4107
|
+
}
|
|
4108
|
+
logCacheReadIssue(cacheFile, "RULES_CACHE_UNREADABLE");
|
|
4109
|
+
}
|
|
4110
|
+
return {};
|
|
4111
|
+
}
|
|
4112
|
+
var lastParsedRulesCache = null;
|
|
4113
|
+
var CACHE_LOG_REARM_MS = 5 * 60 * 1e3;
|
|
4114
|
+
var cacheReadLastLoggedAt = 0;
|
|
4115
|
+
function logCacheReadIssue(cacheFile, kind) {
|
|
4116
|
+
const now = Date.now();
|
|
4117
|
+
if (now - cacheReadLastLoggedAt < CACHE_LOG_REARM_MS) return;
|
|
4118
|
+
cacheReadLastLoggedAt = now;
|
|
4119
|
+
try {
|
|
4120
|
+
import_fs4.default.appendFileSync(
|
|
4121
|
+
import_path4.default.join(import_os4.default.homedir(), ".node9", "hook-debug.log"),
|
|
4122
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
|
|
4123
|
+
`
|
|
4124
|
+
);
|
|
4125
|
+
} catch {
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4071
4128
|
function getConfig(cwd) {
|
|
4072
4129
|
if (!cwd && cachedConfig) return cachedConfig;
|
|
4073
4130
|
const globalPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "config.json");
|
|
@@ -4135,6 +4192,7 @@ function getConfig(cwd) {
|
|
|
4135
4192
|
if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
|
|
4136
4193
|
if (s.mcpReconcileIntervalMinutes !== void 0)
|
|
4137
4194
|
mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
|
|
4195
|
+
if (s.mcpStaleAfterDays !== void 0) mergedSettings.mcpStaleAfterDays = s.mcpStaleAfterDays;
|
|
4138
4196
|
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
4139
4197
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
4140
4198
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -4144,12 +4202,13 @@ function getConfig(cwd) {
|
|
|
4144
4202
|
if (p.smartRules) {
|
|
4145
4203
|
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
4146
4204
|
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
4147
|
-
const
|
|
4205
|
+
const localRules = p.smartRules.map(({ pinned: _pinned, ...r }) => r);
|
|
4206
|
+
const userRuleNames = new Set(localRules.filter((r) => r.name).map((r) => r.name));
|
|
4148
4207
|
const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
|
|
4149
4208
|
const filteredNonBlocks = defaultNonBlocks.filter(
|
|
4150
4209
|
(r) => !r.name || !userRuleNames.has(r.name)
|
|
4151
4210
|
);
|
|
4152
|
-
mergedPolicy.smartRules = [...filteredBlocks, ...
|
|
4211
|
+
mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
|
|
4153
4212
|
}
|
|
4154
4213
|
if (p.snapshot) {
|
|
4155
4214
|
const s2 = p.snapshot;
|
|
@@ -4214,10 +4273,12 @@ function getConfig(cwd) {
|
|
|
4214
4273
|
applyLayer(globalConfig);
|
|
4215
4274
|
applyLayer(projectConfig);
|
|
4216
4275
|
let cloudManagedShields = [];
|
|
4276
|
+
let modeCloudControlled = false;
|
|
4277
|
+
let modeCloudStaged = false;
|
|
4217
4278
|
{
|
|
4218
4279
|
const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
|
|
4219
4280
|
try {
|
|
4220
|
-
const raw =
|
|
4281
|
+
const raw = readRulesCacheResilient(cacheFile);
|
|
4221
4282
|
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
4222
4283
|
applyLayer({ policy: { smartRules: raw.rules } });
|
|
4223
4284
|
}
|
|
@@ -4234,6 +4295,9 @@ function getConfig(cwd) {
|
|
|
4234
4295
|
locked.includes("mode")
|
|
4235
4296
|
);
|
|
4236
4297
|
}
|
|
4298
|
+
if (typeof mc.mode === "string" || locked.includes("mode")) {
|
|
4299
|
+
modeCloudControlled = true;
|
|
4300
|
+
}
|
|
4237
4301
|
if (mc.egress && typeof mc.egress === "object") {
|
|
4238
4302
|
const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : void 0;
|
|
4239
4303
|
mergedPolicy.egress = applyManagedEgress(
|
|
@@ -4332,19 +4396,28 @@ function getConfig(cwd) {
|
|
|
4332
4396
|
}
|
|
4333
4397
|
if (raw.shadowMode === true) {
|
|
4334
4398
|
mergedSettings.mode = "observe";
|
|
4399
|
+
modeCloudStaged = true;
|
|
4335
4400
|
}
|
|
4336
4401
|
} catch {
|
|
4337
4402
|
}
|
|
4338
4403
|
}
|
|
4339
4404
|
const shieldOverrides = readShieldOverrides();
|
|
4340
4405
|
const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
|
|
4406
|
+
const cloudManagedSet = new Set(cloudManagedShields);
|
|
4341
4407
|
for (const shieldName of activeShieldNames) {
|
|
4342
|
-
const
|
|
4408
|
+
const isCloudMandated = cloudManagedSet.has(shieldName);
|
|
4409
|
+
const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
|
|
4343
4410
|
if (!shield) continue;
|
|
4344
4411
|
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4345
|
-
const ruleOverrides = shieldOverrides[shieldName] ?? {};
|
|
4412
|
+
const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
|
|
4346
4413
|
for (const rule of shield.smartRules) {
|
|
4347
|
-
|
|
4414
|
+
const collides = rule.name ? existingRuleNames.has(rule.name) : false;
|
|
4415
|
+
if (isCloudMandated) {
|
|
4416
|
+
if (collides) {
|
|
4417
|
+
mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
|
|
4418
|
+
}
|
|
4419
|
+
mergedPolicy.smartRules.push({ ...rule, pinned: true });
|
|
4420
|
+
} else if (!collides) {
|
|
4348
4421
|
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
4349
4422
|
mergedPolicy.smartRules.push(
|
|
4350
4423
|
overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
|
|
@@ -4360,7 +4433,27 @@ function getConfig(cwd) {
|
|
|
4360
4433
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4361
4434
|
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
4362
4435
|
}
|
|
4363
|
-
|
|
4436
|
+
const envMode = process.env.NODE9_MODE;
|
|
4437
|
+
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
4438
|
+
mergedSettings.mode = envMode;
|
|
4439
|
+
}
|
|
4440
|
+
if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4441
|
+
mergedSettings.mode = "standard";
|
|
4442
|
+
}
|
|
4443
|
+
const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4444
|
+
if (modeCloudControlled && mergedSettings.mode === "strict") {
|
|
4445
|
+
for (const name of Object.keys(mergedEnvironments)) {
|
|
4446
|
+
if (mergedEnvironments[name]?.requireApproval === false) {
|
|
4447
|
+
const cleaned = { ...mergedEnvironments[name] };
|
|
4448
|
+
delete cleaned.requireApproval;
|
|
4449
|
+
mergedEnvironments[name] = cleaned;
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
if (managedFloorActive) {
|
|
4454
|
+
mergedPolicy.ignoredTools = [...DEFAULT_CONFIG.policy.ignoredTools];
|
|
4455
|
+
mergedPolicy.sandboxPaths = [...DEFAULT_CONFIG.policy.sandboxPaths];
|
|
4456
|
+
}
|
|
4364
4457
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
4365
4458
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
4366
4459
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
@@ -4748,6 +4841,18 @@ function isDaemonRunning() {
|
|
|
4748
4841
|
}
|
|
4749
4842
|
return false;
|
|
4750
4843
|
}
|
|
4844
|
+
async function daemonHasInteractiveApprover(timeoutMs = 400) {
|
|
4845
|
+
try {
|
|
4846
|
+
const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/approver`, {
|
|
4847
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
4848
|
+
});
|
|
4849
|
+
if (!res.ok) return false;
|
|
4850
|
+
const body = await res.json();
|
|
4851
|
+
return body.interactive === true;
|
|
4852
|
+
} catch {
|
|
4853
|
+
return false;
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4751
4856
|
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
|
|
4752
4857
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
4753
4858
|
const ctrl = new AbortController();
|
|
@@ -5444,6 +5549,12 @@ function isNetworkTool(toolName, args) {
|
|
|
5444
5549
|
function notifyActivity(data) {
|
|
5445
5550
|
return notifyActivitySocket(data);
|
|
5446
5551
|
}
|
|
5552
|
+
async function hasReachableHumanApprover(opts) {
|
|
5553
|
+
const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
5554
|
+
const nativeReachable = !opts.calledFromDaemon && opts.approvers.native !== false && hasDisplay;
|
|
5555
|
+
if (nativeReachable) return true;
|
|
5556
|
+
return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
|
|
5557
|
+
}
|
|
5447
5558
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
5448
5559
|
if (!options?.calledFromDaemon) {
|
|
5449
5560
|
const actId = (0, import_crypto4.randomUUID)();
|
|
@@ -5517,6 +5628,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5517
5628
|
let riskMetadata;
|
|
5518
5629
|
let statefulRecoveryCommand;
|
|
5519
5630
|
let localSmartRuleMatched = false;
|
|
5631
|
+
let hardBlockDowngraded = false;
|
|
5520
5632
|
let taintWarning = null;
|
|
5521
5633
|
if (isNetworkTool(toolName, args)) {
|
|
5522
5634
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -5758,6 +5870,36 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5758
5870
|
return { approved: true, checkedBy: "local-policy" };
|
|
5759
5871
|
}
|
|
5760
5872
|
if (policyResult.decision === "block") {
|
|
5873
|
+
hardBlockDowngraded = true;
|
|
5874
|
+
const daemonUp = isDaemonRunning();
|
|
5875
|
+
let humanApproverReachable = false;
|
|
5876
|
+
if (!policyResult.dependsOnStatePredicates?.length && daemonUp && !isTestEnv2) {
|
|
5877
|
+
humanApproverReachable = await hasReachableHumanApprover({
|
|
5878
|
+
approvers,
|
|
5879
|
+
calledFromDaemon: options?.calledFromDaemon
|
|
5880
|
+
});
|
|
5881
|
+
}
|
|
5882
|
+
const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
|
|
5883
|
+
const hardBlock = () => {
|
|
5884
|
+
if (!isManual)
|
|
5885
|
+
appendLocalAudit(
|
|
5886
|
+
toolName,
|
|
5887
|
+
args,
|
|
5888
|
+
"deny",
|
|
5889
|
+
"smart-rule-block",
|
|
5890
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
5891
|
+
hashAuditArgs
|
|
5892
|
+
);
|
|
5893
|
+
return {
|
|
5894
|
+
approved: false,
|
|
5895
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
5896
|
+
blockedBy: "local-config",
|
|
5897
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
5898
|
+
ruleHit: policyResult.ruleName,
|
|
5899
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
5900
|
+
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
5901
|
+
};
|
|
5902
|
+
};
|
|
5761
5903
|
if (policyResult.dependsOnStatePredicates?.length) {
|
|
5762
5904
|
const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
|
|
5763
5905
|
const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
|
|
@@ -5774,7 +5916,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5774
5916
|
if (predicatesMet && policyResult.recoveryCommand) {
|
|
5775
5917
|
statefulRecoveryCommand = policyResult.recoveryCommand;
|
|
5776
5918
|
}
|
|
5777
|
-
} else if (
|
|
5919
|
+
} else if (mayDowngrade) {
|
|
5778
5920
|
if (!isManual)
|
|
5779
5921
|
appendLocalAudit(
|
|
5780
5922
|
toolName,
|
|
@@ -5796,36 +5938,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5796
5938
|
};
|
|
5797
5939
|
}
|
|
5798
5940
|
} 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
|
-
};
|
|
5941
|
+
return hardBlock();
|
|
5823
5942
|
}
|
|
5824
5943
|
}
|
|
5825
5944
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
5826
5945
|
policyMatchedField = policyResult.matchedField;
|
|
5827
5946
|
policyMatchedWord = policyResult.matchedWord;
|
|
5828
|
-
if (policyResult.ruleName
|
|
5947
|
+
if (policyResult.ruleName || policyResult.tier === 7 || hardBlockDowngraded)
|
|
5948
|
+
localSmartRuleMatched = true;
|
|
5829
5949
|
if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
|
|
5830
5950
|
else if (policyResult.reason) policyRuleDescription = policyResult.reason;
|
|
5831
5951
|
riskMetadata = computeRiskMetadata(
|
|
@@ -5837,7 +5957,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5837
5957
|
policyResult.ruleName
|
|
5838
5958
|
);
|
|
5839
5959
|
if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
|
|
5840
|
-
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
5960
|
+
const persistent = policyResult.ruleName || policyResult.tier === 7 || hardBlockDowngraded ? null : getPersistentDecision(toolName);
|
|
5841
5961
|
if (persistent === "allow" && !appPermReview) {
|
|
5842
5962
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
5843
5963
|
return { approved: true, checkedBy: "persistent" };
|
|
@@ -5870,7 +5990,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5870
5990
|
return { approved: true };
|
|
5871
5991
|
}
|
|
5872
5992
|
}
|
|
5873
|
-
if (!taintWarning && !appPermReview && getActiveTrustSession(toolName, args)) {
|
|
5993
|
+
if (!taintWarning && !appPermReview && !hardBlockDowngraded && getActiveTrustSession(toolName, args)) {
|
|
5874
5994
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
5875
5995
|
return { approved: true, checkedBy: "trust" };
|
|
5876
5996
|
}
|
|
@@ -5934,7 +6054,7 @@ ${appPermReview}`
|
|
|
5934
6054
|
forceReview
|
|
5935
6055
|
);
|
|
5936
6056
|
if (!initResult.pending) {
|
|
5937
|
-
if (initResult.shadowMode && !appPermReview) {
|
|
6057
|
+
if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
|
5938
6058
|
return { approved: true, checkedBy: "cloud" };
|
|
5939
6059
|
}
|
|
5940
6060
|
if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|