@node9/proxy 1.62.1 → 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 +926 -340
- package/dist/cli.mjs +924 -338
- 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/index.mjs
CHANGED
|
@@ -295,6 +295,7 @@ var ConfigFileSchema = z.object({
|
|
|
295
295
|
// nudge-only (default), and the scan cadence in minutes.
|
|
296
296
|
mcpAutoWrap: z.boolean().optional(),
|
|
297
297
|
mcpReconcileIntervalMinutes: z.number().positive().optional(),
|
|
298
|
+
mcpStaleAfterDays: z.number().min(0).optional(),
|
|
298
299
|
cloudSyncIntervalHours: z.number().positive().optional(),
|
|
299
300
|
// Seconds-granular override for the cloud policy sync cadence. Wins over
|
|
300
301
|
// cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
|
|
@@ -3305,6 +3306,7 @@ var redis_default = {
|
|
|
3305
3306
|
name: "redis",
|
|
3306
3307
|
description: "Protects Redis instances from destructive AI operations",
|
|
3307
3308
|
aliases: [],
|
|
3309
|
+
_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.",
|
|
3308
3310
|
smartRules: [
|
|
3309
3311
|
{
|
|
3310
3312
|
name: "shield:redis:block-flushall",
|
|
@@ -3313,7 +3315,7 @@ var redis_default = {
|
|
|
3313
3315
|
{
|
|
3314
3316
|
field: "command",
|
|
3315
3317
|
op: "matches",
|
|
3316
|
-
value: "\\bFLUSHALL\\b",
|
|
3318
|
+
value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
|
|
3317
3319
|
flags: "i"
|
|
3318
3320
|
}
|
|
3319
3321
|
],
|
|
@@ -3327,7 +3329,7 @@ var redis_default = {
|
|
|
3327
3329
|
{
|
|
3328
3330
|
field: "command",
|
|
3329
3331
|
op: "matches",
|
|
3330
|
-
value: "\\bFLUSHDB\\b",
|
|
3332
|
+
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
3331
3333
|
flags: "i"
|
|
3332
3334
|
}
|
|
3333
3335
|
],
|
|
@@ -3341,7 +3343,7 @@ var redis_default = {
|
|
|
3341
3343
|
{
|
|
3342
3344
|
field: "command",
|
|
3343
3345
|
op: "matches",
|
|
3344
|
-
value: "\\
|
|
3346
|
+
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
3345
3347
|
flags: "i"
|
|
3346
3348
|
}
|
|
3347
3349
|
],
|
|
@@ -3355,7 +3357,7 @@ var redis_default = {
|
|
|
3355
3357
|
{
|
|
3356
3358
|
field: "command",
|
|
3357
3359
|
op: "matches",
|
|
3358
|
-
value: "\\bCONFIG\\s+SET\\b",
|
|
3360
|
+
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
3359
3361
|
flags: "i"
|
|
3360
3362
|
}
|
|
3361
3363
|
],
|
|
@@ -3369,7 +3371,7 @@ var redis_default = {
|
|
|
3369
3371
|
{
|
|
3370
3372
|
field: "command",
|
|
3371
3373
|
op: "matches",
|
|
3372
|
-
value: "
|
|
3374
|
+
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
3373
3375
|
flags: "i"
|
|
3374
3376
|
}
|
|
3375
3377
|
],
|
|
@@ -4038,6 +4040,49 @@ function getActiveEnvironment(config) {
|
|
|
4038
4040
|
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
4039
4041
|
return config.environments[env] ?? null;
|
|
4040
4042
|
}
|
|
4043
|
+
var cacheReadFailureLogged = false;
|
|
4044
|
+
function readRulesCacheResilient(cacheFile) {
|
|
4045
|
+
let existed = false;
|
|
4046
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
4047
|
+
let content;
|
|
4048
|
+
try {
|
|
4049
|
+
content = fs4.readFileSync(cacheFile, "utf-8");
|
|
4050
|
+
existed = true;
|
|
4051
|
+
} catch (err) {
|
|
4052
|
+
if (err.code === "ENOENT") return {};
|
|
4053
|
+
continue;
|
|
4054
|
+
}
|
|
4055
|
+
try {
|
|
4056
|
+
return JSON.parse(content);
|
|
4057
|
+
} catch {
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
if (existed) {
|
|
4061
|
+
const backup = path4.join(path4.dirname(cacheFile), "rules-cache.last-good.json");
|
|
4062
|
+
if (backup !== cacheFile) {
|
|
4063
|
+
try {
|
|
4064
|
+
const raw = JSON.parse(fs4.readFileSync(backup, "utf-8"));
|
|
4065
|
+
logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
|
|
4066
|
+
return raw;
|
|
4067
|
+
} catch {
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
logCacheReadIssue(cacheFile, "RULES_CACHE_UNREADABLE");
|
|
4071
|
+
}
|
|
4072
|
+
return {};
|
|
4073
|
+
}
|
|
4074
|
+
function logCacheReadIssue(cacheFile, kind) {
|
|
4075
|
+
if (cacheReadFailureLogged) return;
|
|
4076
|
+
cacheReadFailureLogged = true;
|
|
4077
|
+
try {
|
|
4078
|
+
fs4.appendFileSync(
|
|
4079
|
+
path4.join(os4.homedir(), ".node9", "hook-debug.log"),
|
|
4080
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
|
|
4081
|
+
`
|
|
4082
|
+
);
|
|
4083
|
+
} catch {
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4041
4086
|
function getConfig(cwd) {
|
|
4042
4087
|
if (!cwd && cachedConfig) return cachedConfig;
|
|
4043
4088
|
const globalPath = path4.join(os4.homedir(), ".node9", "config.json");
|
|
@@ -4105,6 +4150,7 @@ function getConfig(cwd) {
|
|
|
4105
4150
|
if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
|
|
4106
4151
|
if (s.mcpReconcileIntervalMinutes !== void 0)
|
|
4107
4152
|
mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
|
|
4153
|
+
if (s.mcpStaleAfterDays !== void 0) mergedSettings.mcpStaleAfterDays = s.mcpStaleAfterDays;
|
|
4108
4154
|
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
4109
4155
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
4110
4156
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -4114,12 +4160,13 @@ function getConfig(cwd) {
|
|
|
4114
4160
|
if (p.smartRules) {
|
|
4115
4161
|
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
4116
4162
|
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
4117
|
-
const
|
|
4163
|
+
const localRules = p.smartRules.map(({ pinned: _pinned, ...r }) => r);
|
|
4164
|
+
const userRuleNames = new Set(localRules.filter((r) => r.name).map((r) => r.name));
|
|
4118
4165
|
const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
|
|
4119
4166
|
const filteredNonBlocks = defaultNonBlocks.filter(
|
|
4120
4167
|
(r) => !r.name || !userRuleNames.has(r.name)
|
|
4121
4168
|
);
|
|
4122
|
-
mergedPolicy.smartRules = [...filteredBlocks, ...
|
|
4169
|
+
mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
|
|
4123
4170
|
}
|
|
4124
4171
|
if (p.snapshot) {
|
|
4125
4172
|
const s2 = p.snapshot;
|
|
@@ -4184,10 +4231,12 @@ function getConfig(cwd) {
|
|
|
4184
4231
|
applyLayer(globalConfig);
|
|
4185
4232
|
applyLayer(projectConfig);
|
|
4186
4233
|
let cloudManagedShields = [];
|
|
4234
|
+
let modeCloudControlled = false;
|
|
4235
|
+
let modeCloudStaged = false;
|
|
4187
4236
|
{
|
|
4188
4237
|
const cacheFile = path4.join(os4.homedir(), ".node9", "rules-cache.json");
|
|
4189
4238
|
try {
|
|
4190
|
-
const raw =
|
|
4239
|
+
const raw = readRulesCacheResilient(cacheFile);
|
|
4191
4240
|
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
4192
4241
|
applyLayer({ policy: { smartRules: raw.rules } });
|
|
4193
4242
|
}
|
|
@@ -4204,6 +4253,9 @@ function getConfig(cwd) {
|
|
|
4204
4253
|
locked.includes("mode")
|
|
4205
4254
|
);
|
|
4206
4255
|
}
|
|
4256
|
+
if (typeof mc.mode === "string" || locked.includes("mode")) {
|
|
4257
|
+
modeCloudControlled = true;
|
|
4258
|
+
}
|
|
4207
4259
|
if (mc.egress && typeof mc.egress === "object") {
|
|
4208
4260
|
const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : void 0;
|
|
4209
4261
|
mergedPolicy.egress = applyManagedEgress(
|
|
@@ -4302,19 +4354,28 @@ function getConfig(cwd) {
|
|
|
4302
4354
|
}
|
|
4303
4355
|
if (raw.shadowMode === true) {
|
|
4304
4356
|
mergedSettings.mode = "observe";
|
|
4357
|
+
modeCloudStaged = true;
|
|
4305
4358
|
}
|
|
4306
4359
|
} catch {
|
|
4307
4360
|
}
|
|
4308
4361
|
}
|
|
4309
4362
|
const shieldOverrides = readShieldOverrides();
|
|
4310
4363
|
const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
|
|
4364
|
+
const cloudManagedSet = new Set(cloudManagedShields);
|
|
4311
4365
|
for (const shieldName of activeShieldNames) {
|
|
4312
|
-
const
|
|
4366
|
+
const isCloudMandated = cloudManagedSet.has(shieldName);
|
|
4367
|
+
const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
|
|
4313
4368
|
if (!shield) continue;
|
|
4314
4369
|
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4315
|
-
const ruleOverrides = shieldOverrides[shieldName] ?? {};
|
|
4370
|
+
const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
|
|
4316
4371
|
for (const rule of shield.smartRules) {
|
|
4317
|
-
|
|
4372
|
+
const collides = rule.name ? existingRuleNames.has(rule.name) : false;
|
|
4373
|
+
if (isCloudMandated) {
|
|
4374
|
+
if (collides) {
|
|
4375
|
+
mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
|
|
4376
|
+
}
|
|
4377
|
+
mergedPolicy.smartRules.push({ ...rule, pinned: true });
|
|
4378
|
+
} else if (!collides) {
|
|
4318
4379
|
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
4319
4380
|
mergedPolicy.smartRules.push(
|
|
4320
4381
|
overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
|
|
@@ -4330,7 +4391,27 @@ function getConfig(cwd) {
|
|
|
4330
4391
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4331
4392
|
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
4332
4393
|
}
|
|
4333
|
-
|
|
4394
|
+
const envMode = process.env.NODE9_MODE;
|
|
4395
|
+
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
4396
|
+
mergedSettings.mode = envMode;
|
|
4397
|
+
}
|
|
4398
|
+
if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4399
|
+
mergedSettings.mode = "standard";
|
|
4400
|
+
}
|
|
4401
|
+
const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4402
|
+
if (modeCloudControlled && mergedSettings.mode === "strict") {
|
|
4403
|
+
for (const name of Object.keys(mergedEnvironments)) {
|
|
4404
|
+
if (mergedEnvironments[name]?.requireApproval === false) {
|
|
4405
|
+
const cleaned = { ...mergedEnvironments[name] };
|
|
4406
|
+
delete cleaned.requireApproval;
|
|
4407
|
+
mergedEnvironments[name] = cleaned;
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
if (managedFloorActive) {
|
|
4412
|
+
mergedPolicy.ignoredTools = [...DEFAULT_CONFIG.policy.ignoredTools];
|
|
4413
|
+
mergedPolicy.sandboxPaths = [...DEFAULT_CONFIG.policy.sandboxPaths];
|
|
4414
|
+
}
|
|
4334
4415
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
4335
4416
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
4336
4417
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
@@ -4718,6 +4799,18 @@ function isDaemonRunning() {
|
|
|
4718
4799
|
}
|
|
4719
4800
|
return false;
|
|
4720
4801
|
}
|
|
4802
|
+
async function daemonHasInteractiveApprover(timeoutMs = 400) {
|
|
4803
|
+
try {
|
|
4804
|
+
const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/approver`, {
|
|
4805
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
4806
|
+
});
|
|
4807
|
+
if (!res.ok) return false;
|
|
4808
|
+
const body = await res.json();
|
|
4809
|
+
return body.interactive === true;
|
|
4810
|
+
} catch {
|
|
4811
|
+
return false;
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4721
4814
|
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
|
|
4722
4815
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
4723
4816
|
const ctrl = new AbortController();
|
|
@@ -5414,6 +5507,12 @@ function isNetworkTool(toolName, args) {
|
|
|
5414
5507
|
function notifyActivity(data) {
|
|
5415
5508
|
return notifyActivitySocket(data);
|
|
5416
5509
|
}
|
|
5510
|
+
async function hasReachableHumanApprover(opts) {
|
|
5511
|
+
const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
5512
|
+
const nativeReachable = !opts.calledFromDaemon && opts.approvers.native !== false && hasDisplay;
|
|
5513
|
+
if (nativeReachable) return true;
|
|
5514
|
+
return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
|
|
5515
|
+
}
|
|
5417
5516
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
5418
5517
|
if (!options?.calledFromDaemon) {
|
|
5419
5518
|
const actId = randomUUID();
|
|
@@ -5728,6 +5827,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5728
5827
|
return { approved: true, checkedBy: "local-policy" };
|
|
5729
5828
|
}
|
|
5730
5829
|
if (policyResult.decision === "block") {
|
|
5830
|
+
const daemonUp = isDaemonRunning();
|
|
5831
|
+
let humanApproverReachable = false;
|
|
5832
|
+
if (!policyResult.dependsOnStatePredicates?.length && daemonUp && !isTestEnv2) {
|
|
5833
|
+
humanApproverReachable = await hasReachableHumanApprover({
|
|
5834
|
+
approvers,
|
|
5835
|
+
calledFromDaemon: options?.calledFromDaemon
|
|
5836
|
+
});
|
|
5837
|
+
}
|
|
5838
|
+
const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
|
|
5839
|
+
const hardBlock = () => {
|
|
5840
|
+
if (!isManual)
|
|
5841
|
+
appendLocalAudit(
|
|
5842
|
+
toolName,
|
|
5843
|
+
args,
|
|
5844
|
+
"deny",
|
|
5845
|
+
"smart-rule-block",
|
|
5846
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
5847
|
+
hashAuditArgs
|
|
5848
|
+
);
|
|
5849
|
+
return {
|
|
5850
|
+
approved: false,
|
|
5851
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
5852
|
+
blockedBy: "local-config",
|
|
5853
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
5854
|
+
ruleHit: policyResult.ruleName,
|
|
5855
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
5856
|
+
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
5857
|
+
};
|
|
5858
|
+
};
|
|
5731
5859
|
if (policyResult.dependsOnStatePredicates?.length) {
|
|
5732
5860
|
const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
|
|
5733
5861
|
const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
|
|
@@ -5744,7 +5872,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5744
5872
|
if (predicatesMet && policyResult.recoveryCommand) {
|
|
5745
5873
|
statefulRecoveryCommand = policyResult.recoveryCommand;
|
|
5746
5874
|
}
|
|
5747
|
-
} else if (
|
|
5875
|
+
} else if (mayDowngrade) {
|
|
5748
5876
|
if (!isManual)
|
|
5749
5877
|
appendLocalAudit(
|
|
5750
5878
|
toolName,
|
|
@@ -5766,36 +5894,13 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5766
5894
|
};
|
|
5767
5895
|
}
|
|
5768
5896
|
} else {
|
|
5769
|
-
|
|
5770
|
-
appendLocalAudit(
|
|
5771
|
-
toolName,
|
|
5772
|
-
args,
|
|
5773
|
-
"deny",
|
|
5774
|
-
"smart-rule-block",
|
|
5775
|
-
// Include policyResult.ruleName so the [2] Report SHIELDS
|
|
5776
|
-
// panel can attribute this block to its specific shield
|
|
5777
|
-
// (e.g. `shield:project-jail:block-read-ssh`) via the
|
|
5778
|
-
// rule→shield map. checkedBy stays as the generic
|
|
5779
|
-
// `smart-rule-block` for backward compat with existing
|
|
5780
|
-
// log readers.
|
|
5781
|
-
{ ...meta, ruleName: policyResult.ruleName },
|
|
5782
|
-
hashAuditArgs
|
|
5783
|
-
);
|
|
5784
|
-
return {
|
|
5785
|
-
approved: false,
|
|
5786
|
-
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
5787
|
-
blockedBy: "local-config",
|
|
5788
|
-
blockedByLabel: policyResult.blockedByLabel,
|
|
5789
|
-
ruleHit: policyResult.ruleName,
|
|
5790
|
-
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
5791
|
-
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
5792
|
-
};
|
|
5897
|
+
return hardBlock();
|
|
5793
5898
|
}
|
|
5794
5899
|
}
|
|
5795
5900
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
5796
5901
|
policyMatchedField = policyResult.matchedField;
|
|
5797
5902
|
policyMatchedWord = policyResult.matchedWord;
|
|
5798
|
-
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
5903
|
+
if (policyResult.ruleName || policyResult.tier === 7) localSmartRuleMatched = true;
|
|
5799
5904
|
if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
|
|
5800
5905
|
else if (policyResult.reason) policyRuleDescription = policyResult.reason;
|
|
5801
5906
|
riskMetadata = computeRiskMetadata(
|
|
@@ -5807,7 +5912,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5807
5912
|
policyResult.ruleName
|
|
5808
5913
|
);
|
|
5809
5914
|
if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
|
|
5810
|
-
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
5915
|
+
const persistent = policyResult.ruleName || policyResult.tier === 7 ? null : getPersistentDecision(toolName);
|
|
5811
5916
|
if (persistent === "allow" && !appPermReview) {
|
|
5812
5917
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
5813
5918
|
return { approved: true, checkedBy: "persistent" };
|
package/dist/scan-ink.mjs
CHANGED
|
@@ -1500,6 +1500,7 @@ var redis_default = {
|
|
|
1500
1500
|
name: "redis",
|
|
1501
1501
|
description: "Protects Redis instances from destructive AI operations",
|
|
1502
1502
|
aliases: [],
|
|
1503
|
+
_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.",
|
|
1503
1504
|
smartRules: [
|
|
1504
1505
|
{
|
|
1505
1506
|
name: "shield:redis:block-flushall",
|
|
@@ -1508,7 +1509,7 @@ var redis_default = {
|
|
|
1508
1509
|
{
|
|
1509
1510
|
field: "command",
|
|
1510
1511
|
op: "matches",
|
|
1511
|
-
value: "\\bFLUSHALL\\b",
|
|
1512
|
+
value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
|
|
1512
1513
|
flags: "i"
|
|
1513
1514
|
}
|
|
1514
1515
|
],
|
|
@@ -1522,7 +1523,7 @@ var redis_default = {
|
|
|
1522
1523
|
{
|
|
1523
1524
|
field: "command",
|
|
1524
1525
|
op: "matches",
|
|
1525
|
-
value: "\\bFLUSHDB\\b",
|
|
1526
|
+
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
1526
1527
|
flags: "i"
|
|
1527
1528
|
}
|
|
1528
1529
|
],
|
|
@@ -1536,7 +1537,7 @@ var redis_default = {
|
|
|
1536
1537
|
{
|
|
1537
1538
|
field: "command",
|
|
1538
1539
|
op: "matches",
|
|
1539
|
-
value: "\\
|
|
1540
|
+
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
1540
1541
|
flags: "i"
|
|
1541
1542
|
}
|
|
1542
1543
|
],
|
|
@@ -1550,7 +1551,7 @@ var redis_default = {
|
|
|
1550
1551
|
{
|
|
1551
1552
|
field: "command",
|
|
1552
1553
|
op: "matches",
|
|
1553
|
-
value: "\\bCONFIG\\s+SET\\b",
|
|
1554
|
+
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
1554
1555
|
flags: "i"
|
|
1555
1556
|
}
|
|
1556
1557
|
],
|
|
@@ -1564,7 +1565,7 @@ var redis_default = {
|
|
|
1564
1565
|
{
|
|
1565
1566
|
field: "command",
|
|
1566
1567
|
op: "matches",
|
|
1567
|
-
value: "
|
|
1568
|
+
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
1568
1569
|
flags: "i"
|
|
1569
1570
|
}
|
|
1570
1571
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.64.0",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|