@node9/proxy 1.1.4 → 1.1.5
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 +13 -13
- package/dist/cli.js +307 -19
- package/dist/cli.mjs +307 -19
- package/dist/index.js +76 -9
- package/dist/index.mjs +76 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,13 +138,13 @@ Node9 has two layers of protection. You get Layer 1 automatically. Layer 2 is on
|
|
|
138
138
|
|
|
139
139
|
Built into the binary. Zero configuration required. Protects the tools every developer uses.
|
|
140
140
|
|
|
141
|
-
| What it protects | Example blocked action
|
|
142
|
-
| :---------------- |
|
|
143
|
-
| **Git** | `git push --force`, `git reset --hard`, `git clean -fd`
|
|
144
|
-
| **Shell** | `curl ... \| bash`, `sudo` commands
|
|
145
|
-
| **SQL** | `DELETE` / `UPDATE` without
|
|
146
|
-
| **Filesystem** | `rm -rf` targeting home directory
|
|
147
|
-
| **Secrets (DLP)** | AWS keys, GitHub tokens, Stripe keys, PEM private keys
|
|
141
|
+
| What it protects | Example blocked action |
|
|
142
|
+
| :---------------- | :--------------------------------------------------------------------------------- |
|
|
143
|
+
| **Git** | `git push --force`, `git reset --hard`, `git clean -fd` |
|
|
144
|
+
| **Shell** | `curl ... \| bash`, `sudo` commands |
|
|
145
|
+
| **SQL** | `DELETE` / `UPDATE` without `WHERE`; `DROP TABLE`, `TRUNCATE TABLE`, `DROP COLUMN` |
|
|
146
|
+
| **Filesystem** | `rm -rf` targeting home directory |
|
|
147
|
+
| **Secrets (DLP)** | AWS keys, GitHub tokens, Stripe keys, PEM private keys |
|
|
148
148
|
|
|
149
149
|
### 🔍 DLP — Content Scanner (Always On)
|
|
150
150
|
|
|
@@ -188,12 +188,12 @@ Secrets are **never logged in full** — the audit trail stores only a redacted
|
|
|
188
188
|
|
|
189
189
|
Shields add protection for specific infrastructure and services — only relevant if you actually use them.
|
|
190
190
|
|
|
191
|
-
| Shield | What it protects
|
|
192
|
-
| :----------- |
|
|
193
|
-
| `postgres` |
|
|
194
|
-
| `github` | Blocks `gh repo delete`; reviews remote branch deletion
|
|
195
|
-
| `aws` | Blocks S3 bucket deletion, EC2 termination; reviews IAM changes, RDS deletion
|
|
196
|
-
| `filesystem` | Reviews `chmod 777`, writes to `/etc/`
|
|
191
|
+
| Shield | What it protects |
|
|
192
|
+
| :----------- | :-------------------------------------------------------------------------------------------------------------- |
|
|
193
|
+
| `postgres` | Hard-blocks `DROP TABLE`, `TRUNCATE`, `DROP COLUMN` (upgrades Layer 1 review → block); reviews `GRANT`/`REVOKE` |
|
|
194
|
+
| `github` | Blocks `gh repo delete`; reviews remote branch deletion |
|
|
195
|
+
| `aws` | Blocks S3 bucket deletion, EC2 termination; reviews IAM changes, RDS deletion |
|
|
196
|
+
| `filesystem` | Reviews `chmod 777`, writes to `/etc/` |
|
|
197
197
|
|
|
198
198
|
```bash
|
|
199
199
|
node9 shield enable postgres # protect your database
|
package/dist/cli.js
CHANGED
|
@@ -492,30 +492,97 @@ function getShield(name) {
|
|
|
492
492
|
function listShields() {
|
|
493
493
|
return Object.values(SHIELDS);
|
|
494
494
|
}
|
|
495
|
-
function
|
|
495
|
+
function isShieldVerdict(v) {
|
|
496
|
+
return v === "allow" || v === "review" || v === "block";
|
|
497
|
+
}
|
|
498
|
+
function validateOverrides(raw) {
|
|
499
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
500
|
+
const result = {};
|
|
501
|
+
for (const [shieldName, rules] of Object.entries(raw)) {
|
|
502
|
+
if (!rules || typeof rules !== "object" || Array.isArray(rules)) continue;
|
|
503
|
+
const validRules = {};
|
|
504
|
+
for (const [ruleName, verdict] of Object.entries(rules)) {
|
|
505
|
+
if (isShieldVerdict(verdict)) {
|
|
506
|
+
validRules[ruleName] = verdict;
|
|
507
|
+
} else {
|
|
508
|
+
process.stderr.write(
|
|
509
|
+
`[node9] Warning: shields.json contains invalid verdict "${String(verdict)}" for ${shieldName}/${ruleName} \u2014 entry ignored. File may be corrupted or tampered with.
|
|
510
|
+
`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (Object.keys(validRules).length > 0) result[shieldName] = validRules;
|
|
515
|
+
}
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
function readShieldsFile() {
|
|
496
519
|
try {
|
|
497
520
|
const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
498
|
-
if (!raw.trim()) return [];
|
|
521
|
+
if (!raw.trim()) return { active: [] };
|
|
499
522
|
const parsed = JSON.parse(raw);
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
523
|
+
const active = Array.isArray(parsed.active) ? parsed.active.filter(
|
|
524
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
525
|
+
) : [];
|
|
526
|
+
return { active, overrides: validateOverrides(parsed.overrides) };
|
|
505
527
|
} catch (err) {
|
|
506
528
|
if (err.code !== "ENOENT") {
|
|
507
529
|
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
508
530
|
`);
|
|
509
531
|
}
|
|
532
|
+
return { active: [] };
|
|
510
533
|
}
|
|
511
|
-
return [];
|
|
512
534
|
}
|
|
513
|
-
function
|
|
535
|
+
function writeShieldsFile(data) {
|
|
514
536
|
import_fs.default.mkdirSync(import_path3.default.dirname(SHIELDS_STATE_FILE), { recursive: true });
|
|
515
537
|
const tmp = `${SHIELDS_STATE_FILE}.${import_crypto.default.randomBytes(6).toString("hex")}.tmp`;
|
|
516
|
-
|
|
538
|
+
const toWrite = { active: data.active };
|
|
539
|
+
if (data.overrides && Object.keys(data.overrides).length > 0) toWrite.overrides = data.overrides;
|
|
540
|
+
import_fs.default.writeFileSync(tmp, JSON.stringify(toWrite, null, 2), { mode: 384 });
|
|
517
541
|
import_fs.default.renameSync(tmp, SHIELDS_STATE_FILE);
|
|
518
542
|
}
|
|
543
|
+
function readActiveShields() {
|
|
544
|
+
return readShieldsFile().active;
|
|
545
|
+
}
|
|
546
|
+
function writeActiveShields(active) {
|
|
547
|
+
const current = readShieldsFile();
|
|
548
|
+
writeShieldsFile({ ...current, active });
|
|
549
|
+
}
|
|
550
|
+
function readShieldOverrides() {
|
|
551
|
+
return readShieldsFile().overrides ?? {};
|
|
552
|
+
}
|
|
553
|
+
function writeShieldOverride(shieldName, ruleName, verdict) {
|
|
554
|
+
const current = readShieldsFile();
|
|
555
|
+
const overrides = { ...current.overrides ?? {} };
|
|
556
|
+
overrides[shieldName] = { ...overrides[shieldName] ?? {}, [ruleName]: verdict };
|
|
557
|
+
writeShieldsFile({ ...current, overrides });
|
|
558
|
+
}
|
|
559
|
+
function clearShieldOverride(shieldName, ruleName) {
|
|
560
|
+
const current = readShieldsFile();
|
|
561
|
+
if (!current.overrides?.[shieldName]?.[ruleName]) return;
|
|
562
|
+
const overrides = { ...current.overrides };
|
|
563
|
+
const updated = { ...overrides[shieldName] };
|
|
564
|
+
delete updated[ruleName];
|
|
565
|
+
if (Object.keys(updated).length === 0) {
|
|
566
|
+
delete overrides[shieldName];
|
|
567
|
+
} else {
|
|
568
|
+
overrides[shieldName] = updated;
|
|
569
|
+
}
|
|
570
|
+
writeShieldsFile({ ...current, overrides });
|
|
571
|
+
}
|
|
572
|
+
function resolveShieldRule(shieldName, identifier) {
|
|
573
|
+
const shield = SHIELDS[shieldName];
|
|
574
|
+
if (!shield) return null;
|
|
575
|
+
const id = identifier.toLowerCase();
|
|
576
|
+
for (const rule of shield.smartRules) {
|
|
577
|
+
if (!rule.name) continue;
|
|
578
|
+
if (rule.name === id) return rule.name;
|
|
579
|
+
const withoutShieldPrefix = rule.name.replace(`shield:${shieldName}:`, "");
|
|
580
|
+
if (withoutShieldPrefix === id) return rule.name;
|
|
581
|
+
const operation = withoutShieldPrefix.replace(/^(block|review|allow)-/, "");
|
|
582
|
+
if (operation === id) return rule.name;
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
519
586
|
var import_fs, import_path3, import_os, import_crypto, SHIELDS, SHIELDS_STATE_FILE;
|
|
520
587
|
var init_shields = __esm({
|
|
521
588
|
"src/shields.ts"() {
|
|
@@ -1152,6 +1219,13 @@ function redactSecrets(text) {
|
|
|
1152
1219
|
function _resetConfigCache() {
|
|
1153
1220
|
cachedConfig = null;
|
|
1154
1221
|
}
|
|
1222
|
+
function appendConfigAudit(entry) {
|
|
1223
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
1224
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1225
|
+
...entry,
|
|
1226
|
+
hostname: import_os2.default.hostname()
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1155
1229
|
function getGlobalSettings() {
|
|
1156
1230
|
try {
|
|
1157
1231
|
const globalConfigPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "config.json");
|
|
@@ -2167,12 +2241,19 @@ function getConfig(cwd) {
|
|
|
2167
2241
|
};
|
|
2168
2242
|
applyLayer(globalConfig);
|
|
2169
2243
|
applyLayer(projectConfig);
|
|
2244
|
+
const shieldOverrides = readShieldOverrides();
|
|
2170
2245
|
for (const shieldName of readActiveShields()) {
|
|
2171
2246
|
const shield = getShield(shieldName);
|
|
2172
2247
|
if (!shield) continue;
|
|
2173
2248
|
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2249
|
+
const ruleOverrides = shieldOverrides[shieldName] ?? {};
|
|
2174
2250
|
for (const rule of shield.smartRules) {
|
|
2175
|
-
if (!existingRuleNames.has(rule.name))
|
|
2251
|
+
if (!existingRuleNames.has(rule.name)) {
|
|
2252
|
+
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
2253
|
+
mergedPolicy.smartRules.push(
|
|
2254
|
+
overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2176
2257
|
}
|
|
2177
2258
|
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
2178
2259
|
for (const word of shield.dangerousWords) {
|
|
@@ -2593,6 +2674,10 @@ var init_core = __esm({
|
|
|
2593
2674
|
environments: {}
|
|
2594
2675
|
};
|
|
2595
2676
|
ADVISORY_SMART_RULES = [
|
|
2677
|
+
// ── rm safety ─────────────────────────────────────────────────────────────
|
|
2678
|
+
// tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
|
|
2679
|
+
// Pattern '(^|&&|\|\||;)\s*rm\b' matches rm as a shell command (including in
|
|
2680
|
+
// chained commands like 'cat foo && rm bar') but avoids false-positives on 'docker rm'.
|
|
2596
2681
|
{
|
|
2597
2682
|
name: "allow-rm-safe-paths",
|
|
2598
2683
|
tool: "*",
|
|
@@ -2615,6 +2700,34 @@ var init_core = __esm({
|
|
|
2615
2700
|
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
2616
2701
|
verdict: "review",
|
|
2617
2702
|
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
2703
|
+
},
|
|
2704
|
+
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
2705
|
+
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
2706
|
+
// mcp__postgres__query) with a destructive SQL statement in the 'sql' field.
|
|
2707
|
+
// The postgres shield upgrades these from 'review' → 'block' for stricter teams;
|
|
2708
|
+
// without a shield, users still get a human-approval gate on every destructive op.
|
|
2709
|
+
{
|
|
2710
|
+
name: "review-drop-table-sql",
|
|
2711
|
+
tool: "*",
|
|
2712
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
2713
|
+
verdict: "review",
|
|
2714
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead"
|
|
2715
|
+
},
|
|
2716
|
+
{
|
|
2717
|
+
name: "review-truncate-sql",
|
|
2718
|
+
tool: "*",
|
|
2719
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
2720
|
+
verdict: "review",
|
|
2721
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead"
|
|
2722
|
+
},
|
|
2723
|
+
{
|
|
2724
|
+
name: "review-drop-column-sql",
|
|
2725
|
+
tool: "*",
|
|
2726
|
+
conditions: [
|
|
2727
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
2728
|
+
],
|
|
2729
|
+
verdict: "review",
|
|
2730
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead"
|
|
2618
2731
|
}
|
|
2619
2732
|
];
|
|
2620
2733
|
cachedConfig = null;
|
|
@@ -6923,26 +7036,201 @@ shieldCmd.command("list").description("Show all available shields").action(() =>
|
|
|
6923
7036
|
}
|
|
6924
7037
|
console.log("");
|
|
6925
7038
|
});
|
|
6926
|
-
shieldCmd.command("status").description("Show
|
|
7039
|
+
shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
|
|
6927
7040
|
const active = readActiveShields();
|
|
6928
7041
|
if (active.length === 0) {
|
|
6929
|
-
console.
|
|
6930
|
-
console.
|
|
7042
|
+
console.error(import_chalk6.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
|
|
7043
|
+
console.error(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
|
|
6931
7044
|
`);
|
|
6932
7045
|
return;
|
|
6933
7046
|
}
|
|
6934
|
-
|
|
7047
|
+
const overrides = readShieldOverrides();
|
|
7048
|
+
console.error(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
6935
7049
|
for (const name of active) {
|
|
6936
7050
|
const shield = getShield(name);
|
|
6937
7051
|
if (!shield) continue;
|
|
6938
|
-
console.
|
|
6939
|
-
|
|
7052
|
+
console.error(` ${import_chalk6.default.green("\u25CF")} ${import_chalk6.default.cyan(name)} \u2014 ${shield.description}`);
|
|
7053
|
+
const ruleOverrides = overrides[name] ?? {};
|
|
7054
|
+
for (const rule of shield.smartRules) {
|
|
7055
|
+
const shortName = rule.name ? rule.name.replace(`shield:${name}:`, "") : "(unnamed)";
|
|
7056
|
+
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
7057
|
+
const effectiveVerdict = overrideVerdict ?? rule.verdict;
|
|
7058
|
+
const verdictLabel = effectiveVerdict === "block" ? import_chalk6.default.red("block ") : effectiveVerdict === "review" ? import_chalk6.default.yellow("review") : import_chalk6.default.green("allow ");
|
|
7059
|
+
const overrideNote = overrideVerdict ? import_chalk6.default.gray(` \u2190 overridden (was: ${rule.verdict})`) : "";
|
|
7060
|
+
console.error(
|
|
7061
|
+
` ${verdictLabel} ${shortName.padEnd(24)} ${import_chalk6.default.gray(rule.reason ?? "")}${overrideNote}`
|
|
7062
|
+
);
|
|
7063
|
+
}
|
|
7064
|
+
if (shield.dangerousWords.length > 0) {
|
|
7065
|
+
console.error(import_chalk6.default.gray(` words: ${shield.dangerousWords.join(", ")}`));
|
|
7066
|
+
}
|
|
7067
|
+
console.error("");
|
|
7068
|
+
}
|
|
7069
|
+
if (Object.keys(overrides).length > 0) {
|
|
7070
|
+
console.error(
|
|
6940
7071
|
import_chalk6.default.gray(
|
|
6941
|
-
`
|
|
7072
|
+
` Tip: run ${import_chalk6.default.cyan("node9 shield unset <shield> <rule>")} to remove an override.
|
|
7073
|
+
`
|
|
6942
7074
|
)
|
|
6943
7075
|
);
|
|
6944
7076
|
}
|
|
6945
|
-
|
|
7077
|
+
});
|
|
7078
|
+
shieldCmd.command("set <service> <rule> <verdict>").description("Override the verdict for a specific shield rule (block, review, or allow)").option("--force", "Required when setting verdict to allow (silences a block rule)").action((service, rule, verdict, opts) => {
|
|
7079
|
+
const name = resolveShieldName(service);
|
|
7080
|
+
if (!name) {
|
|
7081
|
+
console.error(import_chalk6.default.red(`
|
|
7082
|
+
\u274C Unknown shield: "${service}"
|
|
7083
|
+
`));
|
|
7084
|
+
console.error(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
|
|
7085
|
+
`);
|
|
7086
|
+
process.exit(1);
|
|
7087
|
+
}
|
|
7088
|
+
if (!readActiveShields().includes(name)) {
|
|
7089
|
+
console.error(import_chalk6.default.red(`
|
|
7090
|
+
\u274C Shield "${name}" is not active. Enable it first:
|
|
7091
|
+
`));
|
|
7092
|
+
console.error(` ${import_chalk6.default.cyan(`node9 shield enable ${name}`)}
|
|
7093
|
+
`);
|
|
7094
|
+
process.exit(1);
|
|
7095
|
+
}
|
|
7096
|
+
if (!isShieldVerdict(verdict)) {
|
|
7097
|
+
console.error(import_chalk6.default.red(`
|
|
7098
|
+
\u274C Invalid verdict "${verdict}". Use: block, review, or allow
|
|
7099
|
+
`));
|
|
7100
|
+
process.exit(1);
|
|
7101
|
+
}
|
|
7102
|
+
if (verdict === "allow" && !opts.force) {
|
|
7103
|
+
console.error(
|
|
7104
|
+
import_chalk6.default.red(`
|
|
7105
|
+
\u26A0\uFE0F Setting a verdict to "allow" silences the rule entirely.
|
|
7106
|
+
`) + import_chalk6.default.yellow(
|
|
7107
|
+
` This disables a shield protection. If you are sure, re-run with --force:
|
|
7108
|
+
`
|
|
7109
|
+
) + import_chalk6.default.cyan(`
|
|
7110
|
+
node9 shield set ${service} ${rule} allow --force
|
|
7111
|
+
`)
|
|
7112
|
+
);
|
|
7113
|
+
process.exit(1);
|
|
7114
|
+
}
|
|
7115
|
+
const ruleName = resolveShieldRule(name, rule);
|
|
7116
|
+
if (!ruleName) {
|
|
7117
|
+
const shield = getShield(name);
|
|
7118
|
+
console.error(import_chalk6.default.red(`
|
|
7119
|
+
\u274C Unknown rule "${rule}" for shield "${name}".
|
|
7120
|
+
`));
|
|
7121
|
+
console.error(" Available rules:");
|
|
7122
|
+
for (const r of shield?.smartRules ?? []) {
|
|
7123
|
+
const short = r.name ? r.name.replace(`shield:${name}:`, "") : "";
|
|
7124
|
+
console.error(` ${import_chalk6.default.cyan(short)}`);
|
|
7125
|
+
}
|
|
7126
|
+
console.error("");
|
|
7127
|
+
process.exit(1);
|
|
7128
|
+
}
|
|
7129
|
+
writeShieldOverride(name, ruleName, verdict);
|
|
7130
|
+
if (verdict === "allow") {
|
|
7131
|
+
appendConfigAudit({ event: "shield-override-allow", shield: name, rule: ruleName });
|
|
7132
|
+
}
|
|
7133
|
+
const shortName = ruleName.replace(`shield:${name}:`, "");
|
|
7134
|
+
const verdictLabel = verdict === "block" ? import_chalk6.default.red("block") : verdict === "review" ? import_chalk6.default.yellow("review") : import_chalk6.default.green("allow");
|
|
7135
|
+
if (verdict === "allow") {
|
|
7136
|
+
console.error(
|
|
7137
|
+
import_chalk6.default.yellow(`
|
|
7138
|
+
\u26A0\uFE0F ${name}/${shortName} \u2192 ${verdictLabel}`) + import_chalk6.default.gray(" (rule silenced \u2014 use `node9 shield unset` to restore)\n")
|
|
7139
|
+
);
|
|
7140
|
+
} else {
|
|
7141
|
+
console.error(import_chalk6.default.green(`
|
|
7142
|
+
\u2705 ${name}/${shortName} \u2192 ${verdictLabel}
|
|
7143
|
+
`));
|
|
7144
|
+
}
|
|
7145
|
+
console.error(
|
|
7146
|
+
import_chalk6.default.gray(` Run ${import_chalk6.default.cyan("node9 shield status")} to see all active rules.
|
|
7147
|
+
`)
|
|
7148
|
+
);
|
|
7149
|
+
});
|
|
7150
|
+
shieldCmd.command("unset <service> <rule>").description("Remove a verdict override, restoring the shield default").action((service, rule) => {
|
|
7151
|
+
const name = resolveShieldName(service);
|
|
7152
|
+
if (!name) {
|
|
7153
|
+
console.error(import_chalk6.default.red(`
|
|
7154
|
+
\u274C Unknown shield: "${service}"
|
|
7155
|
+
`));
|
|
7156
|
+
process.exit(1);
|
|
7157
|
+
}
|
|
7158
|
+
const ruleName = resolveShieldRule(name, rule);
|
|
7159
|
+
if (!ruleName) {
|
|
7160
|
+
console.error(import_chalk6.default.red(`
|
|
7161
|
+
\u274C Unknown rule "${rule}" for shield "${name}".
|
|
7162
|
+
`));
|
|
7163
|
+
process.exit(1);
|
|
7164
|
+
}
|
|
7165
|
+
clearShieldOverride(name, ruleName);
|
|
7166
|
+
const shortName = ruleName.replace(`shield:${name}:`, "");
|
|
7167
|
+
console.error(
|
|
7168
|
+
import_chalk6.default.green(`
|
|
7169
|
+
\u2705 Override removed \u2014 ${name}/${shortName} restored to default.
|
|
7170
|
+
`)
|
|
7171
|
+
);
|
|
7172
|
+
});
|
|
7173
|
+
program.command("config show").description("Show the full effective runtime configuration including shields and advisory rules").action(() => {
|
|
7174
|
+
const config = getConfig();
|
|
7175
|
+
const active = readActiveShields();
|
|
7176
|
+
const overrides = readShieldOverrides();
|
|
7177
|
+
console.error(import_chalk6.default.bold("\n\u{1F50D} Node9 Effective Configuration\n"));
|
|
7178
|
+
const modeLabel = config.settings.mode === "audit" ? import_chalk6.default.blue("audit") : config.settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
|
|
7179
|
+
console.error(` Mode: ${modeLabel}
|
|
7180
|
+
`);
|
|
7181
|
+
if (active.length > 0) {
|
|
7182
|
+
console.error(import_chalk6.default.bold(" \u2500\u2500 Active Shields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7183
|
+
for (const name of active) {
|
|
7184
|
+
const shield = getShield(name);
|
|
7185
|
+
if (!shield) continue;
|
|
7186
|
+
const ruleOverrides = overrides[name] ?? {};
|
|
7187
|
+
console.error(`
|
|
7188
|
+
${import_chalk6.default.green("\u25CF")} ${import_chalk6.default.cyan(name)}`);
|
|
7189
|
+
for (const rule of shield.smartRules) {
|
|
7190
|
+
const shortName = rule.name ? rule.name.replace(`shield:${name}:`, "") : "(unnamed)";
|
|
7191
|
+
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
7192
|
+
const effectiveVerdict = overrideVerdict ?? rule.verdict;
|
|
7193
|
+
const vLabel = effectiveVerdict === "block" ? import_chalk6.default.red("block ") : effectiveVerdict === "review" ? import_chalk6.default.yellow("review") : import_chalk6.default.green("allow ");
|
|
7194
|
+
const note = overrideVerdict ? import_chalk6.default.gray(` \u2190 overridden`) : "";
|
|
7195
|
+
console.error(` ${vLabel} ${shortName}${note}`);
|
|
7196
|
+
}
|
|
7197
|
+
}
|
|
7198
|
+
console.error("");
|
|
7199
|
+
} else {
|
|
7200
|
+
console.error(import_chalk6.default.gray(" No shields active. Run `node9 shield list` to see options.\n"));
|
|
7201
|
+
}
|
|
7202
|
+
console.error(import_chalk6.default.bold(" \u2500\u2500 Built-in Rules (always on) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7203
|
+
for (const rule of config.policy.smartRules) {
|
|
7204
|
+
const isShieldRule = rule.name?.startsWith("shield:");
|
|
7205
|
+
const isAdvisory = [
|
|
7206
|
+
"review-rm",
|
|
7207
|
+
"allow-rm-safe-paths",
|
|
7208
|
+
"review-drop-table-sql",
|
|
7209
|
+
"review-truncate-sql",
|
|
7210
|
+
"review-drop-column-sql"
|
|
7211
|
+
].includes(rule.name ?? "");
|
|
7212
|
+
if (isShieldRule || isAdvisory) continue;
|
|
7213
|
+
const vLabel = rule.verdict === "block" ? import_chalk6.default.red("block ") : rule.verdict === "review" ? import_chalk6.default.yellow("review") : import_chalk6.default.green("allow ");
|
|
7214
|
+
console.error(` ${vLabel} ${import_chalk6.default.gray(rule.name ?? rule.tool)}`);
|
|
7215
|
+
}
|
|
7216
|
+
console.error("");
|
|
7217
|
+
console.error(import_chalk6.default.bold(" \u2500\u2500 Safe by Default (advisory, overridable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7218
|
+
const advisoryNames = /* @__PURE__ */ new Set([
|
|
7219
|
+
"review-rm",
|
|
7220
|
+
"allow-rm-safe-paths",
|
|
7221
|
+
"review-drop-table-sql",
|
|
7222
|
+
"review-truncate-sql",
|
|
7223
|
+
"review-drop-column-sql"
|
|
7224
|
+
]);
|
|
7225
|
+
for (const rule of config.policy.smartRules) {
|
|
7226
|
+
if (!advisoryNames.has(rule.name ?? "")) continue;
|
|
7227
|
+
const vLabel = rule.verdict === "block" ? import_chalk6.default.red("block ") : rule.verdict === "review" ? import_chalk6.default.yellow("review") : import_chalk6.default.green("allow ");
|
|
7228
|
+
console.error(` ${vLabel} ${import_chalk6.default.gray(rule.name ?? rule.tool)}`);
|
|
7229
|
+
}
|
|
7230
|
+
console.error("");
|
|
7231
|
+
console.error(import_chalk6.default.bold(" \u2500\u2500 Dangerous Words \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7232
|
+
console.error(` ${import_chalk6.default.gray(config.policy.dangerousWords.join(", "))}
|
|
7233
|
+
`);
|
|
6946
7234
|
});
|
|
6947
7235
|
if (process.argv[2] !== "daemon") {
|
|
6948
7236
|
process.on("unhandledRejection", (reason) => {
|
package/dist/cli.mjs
CHANGED
|
@@ -475,30 +475,97 @@ function getShield(name) {
|
|
|
475
475
|
function listShields() {
|
|
476
476
|
return Object.values(SHIELDS);
|
|
477
477
|
}
|
|
478
|
-
function
|
|
478
|
+
function isShieldVerdict(v) {
|
|
479
|
+
return v === "allow" || v === "review" || v === "block";
|
|
480
|
+
}
|
|
481
|
+
function validateOverrides(raw) {
|
|
482
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
483
|
+
const result = {};
|
|
484
|
+
for (const [shieldName, rules] of Object.entries(raw)) {
|
|
485
|
+
if (!rules || typeof rules !== "object" || Array.isArray(rules)) continue;
|
|
486
|
+
const validRules = {};
|
|
487
|
+
for (const [ruleName, verdict] of Object.entries(rules)) {
|
|
488
|
+
if (isShieldVerdict(verdict)) {
|
|
489
|
+
validRules[ruleName] = verdict;
|
|
490
|
+
} else {
|
|
491
|
+
process.stderr.write(
|
|
492
|
+
`[node9] Warning: shields.json contains invalid verdict "${String(verdict)}" for ${shieldName}/${ruleName} \u2014 entry ignored. File may be corrupted or tampered with.
|
|
493
|
+
`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (Object.keys(validRules).length > 0) result[shieldName] = validRules;
|
|
498
|
+
}
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
function readShieldsFile() {
|
|
479
502
|
try {
|
|
480
503
|
const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
481
|
-
if (!raw.trim()) return [];
|
|
504
|
+
if (!raw.trim()) return { active: [] };
|
|
482
505
|
const parsed = JSON.parse(raw);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
506
|
+
const active = Array.isArray(parsed.active) ? parsed.active.filter(
|
|
507
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
508
|
+
) : [];
|
|
509
|
+
return { active, overrides: validateOverrides(parsed.overrides) };
|
|
488
510
|
} catch (err) {
|
|
489
511
|
if (err.code !== "ENOENT") {
|
|
490
512
|
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
491
513
|
`);
|
|
492
514
|
}
|
|
515
|
+
return { active: [] };
|
|
493
516
|
}
|
|
494
|
-
return [];
|
|
495
517
|
}
|
|
496
|
-
function
|
|
518
|
+
function writeShieldsFile(data) {
|
|
497
519
|
fs.mkdirSync(path3.dirname(SHIELDS_STATE_FILE), { recursive: true });
|
|
498
520
|
const tmp = `${SHIELDS_STATE_FILE}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
499
|
-
|
|
521
|
+
const toWrite = { active: data.active };
|
|
522
|
+
if (data.overrides && Object.keys(data.overrides).length > 0) toWrite.overrides = data.overrides;
|
|
523
|
+
fs.writeFileSync(tmp, JSON.stringify(toWrite, null, 2), { mode: 384 });
|
|
500
524
|
fs.renameSync(tmp, SHIELDS_STATE_FILE);
|
|
501
525
|
}
|
|
526
|
+
function readActiveShields() {
|
|
527
|
+
return readShieldsFile().active;
|
|
528
|
+
}
|
|
529
|
+
function writeActiveShields(active) {
|
|
530
|
+
const current = readShieldsFile();
|
|
531
|
+
writeShieldsFile({ ...current, active });
|
|
532
|
+
}
|
|
533
|
+
function readShieldOverrides() {
|
|
534
|
+
return readShieldsFile().overrides ?? {};
|
|
535
|
+
}
|
|
536
|
+
function writeShieldOverride(shieldName, ruleName, verdict) {
|
|
537
|
+
const current = readShieldsFile();
|
|
538
|
+
const overrides = { ...current.overrides ?? {} };
|
|
539
|
+
overrides[shieldName] = { ...overrides[shieldName] ?? {}, [ruleName]: verdict };
|
|
540
|
+
writeShieldsFile({ ...current, overrides });
|
|
541
|
+
}
|
|
542
|
+
function clearShieldOverride(shieldName, ruleName) {
|
|
543
|
+
const current = readShieldsFile();
|
|
544
|
+
if (!current.overrides?.[shieldName]?.[ruleName]) return;
|
|
545
|
+
const overrides = { ...current.overrides };
|
|
546
|
+
const updated = { ...overrides[shieldName] };
|
|
547
|
+
delete updated[ruleName];
|
|
548
|
+
if (Object.keys(updated).length === 0) {
|
|
549
|
+
delete overrides[shieldName];
|
|
550
|
+
} else {
|
|
551
|
+
overrides[shieldName] = updated;
|
|
552
|
+
}
|
|
553
|
+
writeShieldsFile({ ...current, overrides });
|
|
554
|
+
}
|
|
555
|
+
function resolveShieldRule(shieldName, identifier) {
|
|
556
|
+
const shield = SHIELDS[shieldName];
|
|
557
|
+
if (!shield) return null;
|
|
558
|
+
const id = identifier.toLowerCase();
|
|
559
|
+
for (const rule of shield.smartRules) {
|
|
560
|
+
if (!rule.name) continue;
|
|
561
|
+
if (rule.name === id) return rule.name;
|
|
562
|
+
const withoutShieldPrefix = rule.name.replace(`shield:${shieldName}:`, "");
|
|
563
|
+
if (withoutShieldPrefix === id) return rule.name;
|
|
564
|
+
const operation = withoutShieldPrefix.replace(/^(block|review|allow)-/, "");
|
|
565
|
+
if (operation === id) return rule.name;
|
|
566
|
+
}
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
502
569
|
var SHIELDS, SHIELDS_STATE_FILE;
|
|
503
570
|
var init_shields = __esm({
|
|
504
571
|
"src/shields.ts"() {
|
|
@@ -1142,6 +1209,13 @@ function redactSecrets(text) {
|
|
|
1142
1209
|
function _resetConfigCache() {
|
|
1143
1210
|
cachedConfig = null;
|
|
1144
1211
|
}
|
|
1212
|
+
function appendConfigAudit(entry) {
|
|
1213
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
1214
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1215
|
+
...entry,
|
|
1216
|
+
hostname: os2.hostname()
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1145
1219
|
function getGlobalSettings() {
|
|
1146
1220
|
try {
|
|
1147
1221
|
const globalConfigPath = path5.join(os2.homedir(), ".node9", "config.json");
|
|
@@ -2157,12 +2231,19 @@ function getConfig(cwd) {
|
|
|
2157
2231
|
};
|
|
2158
2232
|
applyLayer(globalConfig);
|
|
2159
2233
|
applyLayer(projectConfig);
|
|
2234
|
+
const shieldOverrides = readShieldOverrides();
|
|
2160
2235
|
for (const shieldName of readActiveShields()) {
|
|
2161
2236
|
const shield = getShield(shieldName);
|
|
2162
2237
|
if (!shield) continue;
|
|
2163
2238
|
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2239
|
+
const ruleOverrides = shieldOverrides[shieldName] ?? {};
|
|
2164
2240
|
for (const rule of shield.smartRules) {
|
|
2165
|
-
if (!existingRuleNames.has(rule.name))
|
|
2241
|
+
if (!existingRuleNames.has(rule.name)) {
|
|
2242
|
+
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
2243
|
+
mergedPolicy.smartRules.push(
|
|
2244
|
+
overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2166
2247
|
}
|
|
2167
2248
|
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
2168
2249
|
for (const word of shield.dangerousWords) {
|
|
@@ -2572,6 +2653,10 @@ var init_core = __esm({
|
|
|
2572
2653
|
environments: {}
|
|
2573
2654
|
};
|
|
2574
2655
|
ADVISORY_SMART_RULES = [
|
|
2656
|
+
// ── rm safety ─────────────────────────────────────────────────────────────
|
|
2657
|
+
// tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
|
|
2658
|
+
// Pattern '(^|&&|\|\||;)\s*rm\b' matches rm as a shell command (including in
|
|
2659
|
+
// chained commands like 'cat foo && rm bar') but avoids false-positives on 'docker rm'.
|
|
2575
2660
|
{
|
|
2576
2661
|
name: "allow-rm-safe-paths",
|
|
2577
2662
|
tool: "*",
|
|
@@ -2594,6 +2679,34 @@ var init_core = __esm({
|
|
|
2594
2679
|
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
2595
2680
|
verdict: "review",
|
|
2596
2681
|
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
2682
|
+
},
|
|
2683
|
+
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
2684
|
+
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
2685
|
+
// mcp__postgres__query) with a destructive SQL statement in the 'sql' field.
|
|
2686
|
+
// The postgres shield upgrades these from 'review' → 'block' for stricter teams;
|
|
2687
|
+
// without a shield, users still get a human-approval gate on every destructive op.
|
|
2688
|
+
{
|
|
2689
|
+
name: "review-drop-table-sql",
|
|
2690
|
+
tool: "*",
|
|
2691
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
2692
|
+
verdict: "review",
|
|
2693
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead"
|
|
2694
|
+
},
|
|
2695
|
+
{
|
|
2696
|
+
name: "review-truncate-sql",
|
|
2697
|
+
tool: "*",
|
|
2698
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
2699
|
+
verdict: "review",
|
|
2700
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead"
|
|
2701
|
+
},
|
|
2702
|
+
{
|
|
2703
|
+
name: "review-drop-column-sql",
|
|
2704
|
+
tool: "*",
|
|
2705
|
+
conditions: [
|
|
2706
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
2707
|
+
],
|
|
2708
|
+
verdict: "review",
|
|
2709
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead"
|
|
2597
2710
|
}
|
|
2598
2711
|
];
|
|
2599
2712
|
cachedConfig = null;
|
|
@@ -6902,26 +7015,201 @@ shieldCmd.command("list").description("Show all available shields").action(() =>
|
|
|
6902
7015
|
}
|
|
6903
7016
|
console.log("");
|
|
6904
7017
|
});
|
|
6905
|
-
shieldCmd.command("status").description("Show
|
|
7018
|
+
shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
|
|
6906
7019
|
const active = readActiveShields();
|
|
6907
7020
|
if (active.length === 0) {
|
|
6908
|
-
console.
|
|
6909
|
-
console.
|
|
7021
|
+
console.error(chalk6.yellow("\n\u2139\uFE0F No shields are active.\n"));
|
|
7022
|
+
console.error(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
|
|
6910
7023
|
`);
|
|
6911
7024
|
return;
|
|
6912
7025
|
}
|
|
6913
|
-
|
|
7026
|
+
const overrides = readShieldOverrides();
|
|
7027
|
+
console.error(chalk6.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
6914
7028
|
for (const name of active) {
|
|
6915
7029
|
const shield = getShield(name);
|
|
6916
7030
|
if (!shield) continue;
|
|
6917
|
-
console.
|
|
6918
|
-
|
|
7031
|
+
console.error(` ${chalk6.green("\u25CF")} ${chalk6.cyan(name)} \u2014 ${shield.description}`);
|
|
7032
|
+
const ruleOverrides = overrides[name] ?? {};
|
|
7033
|
+
for (const rule of shield.smartRules) {
|
|
7034
|
+
const shortName = rule.name ? rule.name.replace(`shield:${name}:`, "") : "(unnamed)";
|
|
7035
|
+
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
7036
|
+
const effectiveVerdict = overrideVerdict ?? rule.verdict;
|
|
7037
|
+
const verdictLabel = effectiveVerdict === "block" ? chalk6.red("block ") : effectiveVerdict === "review" ? chalk6.yellow("review") : chalk6.green("allow ");
|
|
7038
|
+
const overrideNote = overrideVerdict ? chalk6.gray(` \u2190 overridden (was: ${rule.verdict})`) : "";
|
|
7039
|
+
console.error(
|
|
7040
|
+
` ${verdictLabel} ${shortName.padEnd(24)} ${chalk6.gray(rule.reason ?? "")}${overrideNote}`
|
|
7041
|
+
);
|
|
7042
|
+
}
|
|
7043
|
+
if (shield.dangerousWords.length > 0) {
|
|
7044
|
+
console.error(chalk6.gray(` words: ${shield.dangerousWords.join(", ")}`));
|
|
7045
|
+
}
|
|
7046
|
+
console.error("");
|
|
7047
|
+
}
|
|
7048
|
+
if (Object.keys(overrides).length > 0) {
|
|
7049
|
+
console.error(
|
|
6919
7050
|
chalk6.gray(
|
|
6920
|
-
`
|
|
7051
|
+
` Tip: run ${chalk6.cyan("node9 shield unset <shield> <rule>")} to remove an override.
|
|
7052
|
+
`
|
|
6921
7053
|
)
|
|
6922
7054
|
);
|
|
6923
7055
|
}
|
|
6924
|
-
|
|
7056
|
+
});
|
|
7057
|
+
shieldCmd.command("set <service> <rule> <verdict>").description("Override the verdict for a specific shield rule (block, review, or allow)").option("--force", "Required when setting verdict to allow (silences a block rule)").action((service, rule, verdict, opts) => {
|
|
7058
|
+
const name = resolveShieldName(service);
|
|
7059
|
+
if (!name) {
|
|
7060
|
+
console.error(chalk6.red(`
|
|
7061
|
+
\u274C Unknown shield: "${service}"
|
|
7062
|
+
`));
|
|
7063
|
+
console.error(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
|
|
7064
|
+
`);
|
|
7065
|
+
process.exit(1);
|
|
7066
|
+
}
|
|
7067
|
+
if (!readActiveShields().includes(name)) {
|
|
7068
|
+
console.error(chalk6.red(`
|
|
7069
|
+
\u274C Shield "${name}" is not active. Enable it first:
|
|
7070
|
+
`));
|
|
7071
|
+
console.error(` ${chalk6.cyan(`node9 shield enable ${name}`)}
|
|
7072
|
+
`);
|
|
7073
|
+
process.exit(1);
|
|
7074
|
+
}
|
|
7075
|
+
if (!isShieldVerdict(verdict)) {
|
|
7076
|
+
console.error(chalk6.red(`
|
|
7077
|
+
\u274C Invalid verdict "${verdict}". Use: block, review, or allow
|
|
7078
|
+
`));
|
|
7079
|
+
process.exit(1);
|
|
7080
|
+
}
|
|
7081
|
+
if (verdict === "allow" && !opts.force) {
|
|
7082
|
+
console.error(
|
|
7083
|
+
chalk6.red(`
|
|
7084
|
+
\u26A0\uFE0F Setting a verdict to "allow" silences the rule entirely.
|
|
7085
|
+
`) + chalk6.yellow(
|
|
7086
|
+
` This disables a shield protection. If you are sure, re-run with --force:
|
|
7087
|
+
`
|
|
7088
|
+
) + chalk6.cyan(`
|
|
7089
|
+
node9 shield set ${service} ${rule} allow --force
|
|
7090
|
+
`)
|
|
7091
|
+
);
|
|
7092
|
+
process.exit(1);
|
|
7093
|
+
}
|
|
7094
|
+
const ruleName = resolveShieldRule(name, rule);
|
|
7095
|
+
if (!ruleName) {
|
|
7096
|
+
const shield = getShield(name);
|
|
7097
|
+
console.error(chalk6.red(`
|
|
7098
|
+
\u274C Unknown rule "${rule}" for shield "${name}".
|
|
7099
|
+
`));
|
|
7100
|
+
console.error(" Available rules:");
|
|
7101
|
+
for (const r of shield?.smartRules ?? []) {
|
|
7102
|
+
const short = r.name ? r.name.replace(`shield:${name}:`, "") : "";
|
|
7103
|
+
console.error(` ${chalk6.cyan(short)}`);
|
|
7104
|
+
}
|
|
7105
|
+
console.error("");
|
|
7106
|
+
process.exit(1);
|
|
7107
|
+
}
|
|
7108
|
+
writeShieldOverride(name, ruleName, verdict);
|
|
7109
|
+
if (verdict === "allow") {
|
|
7110
|
+
appendConfigAudit({ event: "shield-override-allow", shield: name, rule: ruleName });
|
|
7111
|
+
}
|
|
7112
|
+
const shortName = ruleName.replace(`shield:${name}:`, "");
|
|
7113
|
+
const verdictLabel = verdict === "block" ? chalk6.red("block") : verdict === "review" ? chalk6.yellow("review") : chalk6.green("allow");
|
|
7114
|
+
if (verdict === "allow") {
|
|
7115
|
+
console.error(
|
|
7116
|
+
chalk6.yellow(`
|
|
7117
|
+
\u26A0\uFE0F ${name}/${shortName} \u2192 ${verdictLabel}`) + chalk6.gray(" (rule silenced \u2014 use `node9 shield unset` to restore)\n")
|
|
7118
|
+
);
|
|
7119
|
+
} else {
|
|
7120
|
+
console.error(chalk6.green(`
|
|
7121
|
+
\u2705 ${name}/${shortName} \u2192 ${verdictLabel}
|
|
7122
|
+
`));
|
|
7123
|
+
}
|
|
7124
|
+
console.error(
|
|
7125
|
+
chalk6.gray(` Run ${chalk6.cyan("node9 shield status")} to see all active rules.
|
|
7126
|
+
`)
|
|
7127
|
+
);
|
|
7128
|
+
});
|
|
7129
|
+
shieldCmd.command("unset <service> <rule>").description("Remove a verdict override, restoring the shield default").action((service, rule) => {
|
|
7130
|
+
const name = resolveShieldName(service);
|
|
7131
|
+
if (!name) {
|
|
7132
|
+
console.error(chalk6.red(`
|
|
7133
|
+
\u274C Unknown shield: "${service}"
|
|
7134
|
+
`));
|
|
7135
|
+
process.exit(1);
|
|
7136
|
+
}
|
|
7137
|
+
const ruleName = resolveShieldRule(name, rule);
|
|
7138
|
+
if (!ruleName) {
|
|
7139
|
+
console.error(chalk6.red(`
|
|
7140
|
+
\u274C Unknown rule "${rule}" for shield "${name}".
|
|
7141
|
+
`));
|
|
7142
|
+
process.exit(1);
|
|
7143
|
+
}
|
|
7144
|
+
clearShieldOverride(name, ruleName);
|
|
7145
|
+
const shortName = ruleName.replace(`shield:${name}:`, "");
|
|
7146
|
+
console.error(
|
|
7147
|
+
chalk6.green(`
|
|
7148
|
+
\u2705 Override removed \u2014 ${name}/${shortName} restored to default.
|
|
7149
|
+
`)
|
|
7150
|
+
);
|
|
7151
|
+
});
|
|
7152
|
+
program.command("config show").description("Show the full effective runtime configuration including shields and advisory rules").action(() => {
|
|
7153
|
+
const config = getConfig();
|
|
7154
|
+
const active = readActiveShields();
|
|
7155
|
+
const overrides = readShieldOverrides();
|
|
7156
|
+
console.error(chalk6.bold("\n\u{1F50D} Node9 Effective Configuration\n"));
|
|
7157
|
+
const modeLabel = config.settings.mode === "audit" ? chalk6.blue("audit") : config.settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
|
|
7158
|
+
console.error(` Mode: ${modeLabel}
|
|
7159
|
+
`);
|
|
7160
|
+
if (active.length > 0) {
|
|
7161
|
+
console.error(chalk6.bold(" \u2500\u2500 Active Shields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7162
|
+
for (const name of active) {
|
|
7163
|
+
const shield = getShield(name);
|
|
7164
|
+
if (!shield) continue;
|
|
7165
|
+
const ruleOverrides = overrides[name] ?? {};
|
|
7166
|
+
console.error(`
|
|
7167
|
+
${chalk6.green("\u25CF")} ${chalk6.cyan(name)}`);
|
|
7168
|
+
for (const rule of shield.smartRules) {
|
|
7169
|
+
const shortName = rule.name ? rule.name.replace(`shield:${name}:`, "") : "(unnamed)";
|
|
7170
|
+
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
7171
|
+
const effectiveVerdict = overrideVerdict ?? rule.verdict;
|
|
7172
|
+
const vLabel = effectiveVerdict === "block" ? chalk6.red("block ") : effectiveVerdict === "review" ? chalk6.yellow("review") : chalk6.green("allow ");
|
|
7173
|
+
const note = overrideVerdict ? chalk6.gray(` \u2190 overridden`) : "";
|
|
7174
|
+
console.error(` ${vLabel} ${shortName}${note}`);
|
|
7175
|
+
}
|
|
7176
|
+
}
|
|
7177
|
+
console.error("");
|
|
7178
|
+
} else {
|
|
7179
|
+
console.error(chalk6.gray(" No shields active. Run `node9 shield list` to see options.\n"));
|
|
7180
|
+
}
|
|
7181
|
+
console.error(chalk6.bold(" \u2500\u2500 Built-in Rules (always on) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7182
|
+
for (const rule of config.policy.smartRules) {
|
|
7183
|
+
const isShieldRule = rule.name?.startsWith("shield:");
|
|
7184
|
+
const isAdvisory = [
|
|
7185
|
+
"review-rm",
|
|
7186
|
+
"allow-rm-safe-paths",
|
|
7187
|
+
"review-drop-table-sql",
|
|
7188
|
+
"review-truncate-sql",
|
|
7189
|
+
"review-drop-column-sql"
|
|
7190
|
+
].includes(rule.name ?? "");
|
|
7191
|
+
if (isShieldRule || isAdvisory) continue;
|
|
7192
|
+
const vLabel = rule.verdict === "block" ? chalk6.red("block ") : rule.verdict === "review" ? chalk6.yellow("review") : chalk6.green("allow ");
|
|
7193
|
+
console.error(` ${vLabel} ${chalk6.gray(rule.name ?? rule.tool)}`);
|
|
7194
|
+
}
|
|
7195
|
+
console.error("");
|
|
7196
|
+
console.error(chalk6.bold(" \u2500\u2500 Safe by Default (advisory, overridable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7197
|
+
const advisoryNames = /* @__PURE__ */ new Set([
|
|
7198
|
+
"review-rm",
|
|
7199
|
+
"allow-rm-safe-paths",
|
|
7200
|
+
"review-drop-table-sql",
|
|
7201
|
+
"review-truncate-sql",
|
|
7202
|
+
"review-drop-column-sql"
|
|
7203
|
+
]);
|
|
7204
|
+
for (const rule of config.policy.smartRules) {
|
|
7205
|
+
if (!advisoryNames.has(rule.name ?? "")) continue;
|
|
7206
|
+
const vLabel = rule.verdict === "block" ? chalk6.red("block ") : rule.verdict === "review" ? chalk6.yellow("review") : chalk6.green("allow ");
|
|
7207
|
+
console.error(` ${vLabel} ${chalk6.gray(rule.name ?? rule.tool)}`);
|
|
7208
|
+
}
|
|
7209
|
+
console.error("");
|
|
7210
|
+
console.error(chalk6.bold(" \u2500\u2500 Dangerous Words \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7211
|
+
console.error(` ${chalk6.gray(config.policy.dangerousWords.join(", "))}
|
|
7212
|
+
`);
|
|
6925
7213
|
});
|
|
6926
7214
|
if (process.argv[2] !== "daemon") {
|
|
6927
7215
|
process.on("unhandledRejection", (reason) => {
|
package/dist/index.js
CHANGED
|
@@ -660,23 +660,51 @@ function getShield(name) {
|
|
|
660
660
|
return resolved ? SHIELDS[resolved] : null;
|
|
661
661
|
}
|
|
662
662
|
var SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
|
|
663
|
-
function
|
|
663
|
+
function isShieldVerdict(v) {
|
|
664
|
+
return v === "allow" || v === "review" || v === "block";
|
|
665
|
+
}
|
|
666
|
+
function validateOverrides(raw) {
|
|
667
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
668
|
+
const result = {};
|
|
669
|
+
for (const [shieldName, rules] of Object.entries(raw)) {
|
|
670
|
+
if (!rules || typeof rules !== "object" || Array.isArray(rules)) continue;
|
|
671
|
+
const validRules = {};
|
|
672
|
+
for (const [ruleName, verdict] of Object.entries(rules)) {
|
|
673
|
+
if (isShieldVerdict(verdict)) {
|
|
674
|
+
validRules[ruleName] = verdict;
|
|
675
|
+
} else {
|
|
676
|
+
process.stderr.write(
|
|
677
|
+
`[node9] Warning: shields.json contains invalid verdict "${String(verdict)}" for ${shieldName}/${ruleName} \u2014 entry ignored. File may be corrupted or tampered with.
|
|
678
|
+
`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (Object.keys(validRules).length > 0) result[shieldName] = validRules;
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
}
|
|
686
|
+
function readShieldsFile() {
|
|
664
687
|
try {
|
|
665
688
|
const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
666
|
-
if (!raw.trim()) return [];
|
|
689
|
+
if (!raw.trim()) return { active: [] };
|
|
667
690
|
const parsed = JSON.parse(raw);
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
691
|
+
const active = Array.isArray(parsed.active) ? parsed.active.filter(
|
|
692
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
693
|
+
) : [];
|
|
694
|
+
return { active, overrides: validateOverrides(parsed.overrides) };
|
|
673
695
|
} catch (err) {
|
|
674
696
|
if (err.code !== "ENOENT") {
|
|
675
697
|
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
676
698
|
`);
|
|
677
699
|
}
|
|
700
|
+
return { active: [] };
|
|
678
701
|
}
|
|
679
|
-
|
|
702
|
+
}
|
|
703
|
+
function readActiveShields() {
|
|
704
|
+
return readShieldsFile().active;
|
|
705
|
+
}
|
|
706
|
+
function readShieldOverrides() {
|
|
707
|
+
return readShieldsFile().overrides ?? {};
|
|
680
708
|
}
|
|
681
709
|
|
|
682
710
|
// src/dlp.ts
|
|
@@ -1299,6 +1327,10 @@ var DEFAULT_CONFIG = {
|
|
|
1299
1327
|
environments: {}
|
|
1300
1328
|
};
|
|
1301
1329
|
var ADVISORY_SMART_RULES = [
|
|
1330
|
+
// ── rm safety ─────────────────────────────────────────────────────────────
|
|
1331
|
+
// tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
|
|
1332
|
+
// Pattern '(^|&&|\|\||;)\s*rm\b' matches rm as a shell command (including in
|
|
1333
|
+
// chained commands like 'cat foo && rm bar') but avoids false-positives on 'docker rm'.
|
|
1302
1334
|
{
|
|
1303
1335
|
name: "allow-rm-safe-paths",
|
|
1304
1336
|
tool: "*",
|
|
@@ -1321,6 +1353,34 @@ var ADVISORY_SMART_RULES = [
|
|
|
1321
1353
|
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
1322
1354
|
verdict: "review",
|
|
1323
1355
|
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
1356
|
+
},
|
|
1357
|
+
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
1358
|
+
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
1359
|
+
// mcp__postgres__query) with a destructive SQL statement in the 'sql' field.
|
|
1360
|
+
// The postgres shield upgrades these from 'review' → 'block' for stricter teams;
|
|
1361
|
+
// without a shield, users still get a human-approval gate on every destructive op.
|
|
1362
|
+
{
|
|
1363
|
+
name: "review-drop-table-sql",
|
|
1364
|
+
tool: "*",
|
|
1365
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
1366
|
+
verdict: "review",
|
|
1367
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead"
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
name: "review-truncate-sql",
|
|
1371
|
+
tool: "*",
|
|
1372
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
1373
|
+
verdict: "review",
|
|
1374
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead"
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
name: "review-drop-column-sql",
|
|
1378
|
+
tool: "*",
|
|
1379
|
+
conditions: [
|
|
1380
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
1381
|
+
],
|
|
1382
|
+
verdict: "review",
|
|
1383
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead"
|
|
1324
1384
|
}
|
|
1325
1385
|
];
|
|
1326
1386
|
var cachedConfig = null;
|
|
@@ -2058,12 +2118,19 @@ function getConfig(cwd) {
|
|
|
2058
2118
|
};
|
|
2059
2119
|
applyLayer(globalConfig);
|
|
2060
2120
|
applyLayer(projectConfig);
|
|
2121
|
+
const shieldOverrides = readShieldOverrides();
|
|
2061
2122
|
for (const shieldName of readActiveShields()) {
|
|
2062
2123
|
const shield = getShield(shieldName);
|
|
2063
2124
|
if (!shield) continue;
|
|
2064
2125
|
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2126
|
+
const ruleOverrides = shieldOverrides[shieldName] ?? {};
|
|
2065
2127
|
for (const rule of shield.smartRules) {
|
|
2066
|
-
if (!existingRuleNames.has(rule.name))
|
|
2128
|
+
if (!existingRuleNames.has(rule.name)) {
|
|
2129
|
+
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
2130
|
+
mergedPolicy.smartRules.push(
|
|
2131
|
+
overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2067
2134
|
}
|
|
2068
2135
|
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
2069
2136
|
for (const word of shield.dangerousWords) {
|
package/dist/index.mjs
CHANGED
|
@@ -624,23 +624,51 @@ function getShield(name) {
|
|
|
624
624
|
return resolved ? SHIELDS[resolved] : null;
|
|
625
625
|
}
|
|
626
626
|
var SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
|
|
627
|
-
function
|
|
627
|
+
function isShieldVerdict(v) {
|
|
628
|
+
return v === "allow" || v === "review" || v === "block";
|
|
629
|
+
}
|
|
630
|
+
function validateOverrides(raw) {
|
|
631
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
632
|
+
const result = {};
|
|
633
|
+
for (const [shieldName, rules] of Object.entries(raw)) {
|
|
634
|
+
if (!rules || typeof rules !== "object" || Array.isArray(rules)) continue;
|
|
635
|
+
const validRules = {};
|
|
636
|
+
for (const [ruleName, verdict] of Object.entries(rules)) {
|
|
637
|
+
if (isShieldVerdict(verdict)) {
|
|
638
|
+
validRules[ruleName] = verdict;
|
|
639
|
+
} else {
|
|
640
|
+
process.stderr.write(
|
|
641
|
+
`[node9] Warning: shields.json contains invalid verdict "${String(verdict)}" for ${shieldName}/${ruleName} \u2014 entry ignored. File may be corrupted or tampered with.
|
|
642
|
+
`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (Object.keys(validRules).length > 0) result[shieldName] = validRules;
|
|
647
|
+
}
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
function readShieldsFile() {
|
|
628
651
|
try {
|
|
629
652
|
const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
630
|
-
if (!raw.trim()) return [];
|
|
653
|
+
if (!raw.trim()) return { active: [] };
|
|
631
654
|
const parsed = JSON.parse(raw);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
655
|
+
const active = Array.isArray(parsed.active) ? parsed.active.filter(
|
|
656
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
657
|
+
) : [];
|
|
658
|
+
return { active, overrides: validateOverrides(parsed.overrides) };
|
|
637
659
|
} catch (err) {
|
|
638
660
|
if (err.code !== "ENOENT") {
|
|
639
661
|
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
640
662
|
`);
|
|
641
663
|
}
|
|
664
|
+
return { active: [] };
|
|
642
665
|
}
|
|
643
|
-
|
|
666
|
+
}
|
|
667
|
+
function readActiveShields() {
|
|
668
|
+
return readShieldsFile().active;
|
|
669
|
+
}
|
|
670
|
+
function readShieldOverrides() {
|
|
671
|
+
return readShieldsFile().overrides ?? {};
|
|
644
672
|
}
|
|
645
673
|
|
|
646
674
|
// src/dlp.ts
|
|
@@ -1263,6 +1291,10 @@ var DEFAULT_CONFIG = {
|
|
|
1263
1291
|
environments: {}
|
|
1264
1292
|
};
|
|
1265
1293
|
var ADVISORY_SMART_RULES = [
|
|
1294
|
+
// ── rm safety ─────────────────────────────────────────────────────────────
|
|
1295
|
+
// tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
|
|
1296
|
+
// Pattern '(^|&&|\|\||;)\s*rm\b' matches rm as a shell command (including in
|
|
1297
|
+
// chained commands like 'cat foo && rm bar') but avoids false-positives on 'docker rm'.
|
|
1266
1298
|
{
|
|
1267
1299
|
name: "allow-rm-safe-paths",
|
|
1268
1300
|
tool: "*",
|
|
@@ -1285,6 +1317,34 @@ var ADVISORY_SMART_RULES = [
|
|
|
1285
1317
|
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
1286
1318
|
verdict: "review",
|
|
1287
1319
|
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
1320
|
+
},
|
|
1321
|
+
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
1322
|
+
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
1323
|
+
// mcp__postgres__query) with a destructive SQL statement in the 'sql' field.
|
|
1324
|
+
// The postgres shield upgrades these from 'review' → 'block' for stricter teams;
|
|
1325
|
+
// without a shield, users still get a human-approval gate on every destructive op.
|
|
1326
|
+
{
|
|
1327
|
+
name: "review-drop-table-sql",
|
|
1328
|
+
tool: "*",
|
|
1329
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
1330
|
+
verdict: "review",
|
|
1331
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead"
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
name: "review-truncate-sql",
|
|
1335
|
+
tool: "*",
|
|
1336
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
1337
|
+
verdict: "review",
|
|
1338
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead"
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
name: "review-drop-column-sql",
|
|
1342
|
+
tool: "*",
|
|
1343
|
+
conditions: [
|
|
1344
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
1345
|
+
],
|
|
1346
|
+
verdict: "review",
|
|
1347
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead"
|
|
1288
1348
|
}
|
|
1289
1349
|
];
|
|
1290
1350
|
var cachedConfig = null;
|
|
@@ -2022,12 +2082,19 @@ function getConfig(cwd) {
|
|
|
2022
2082
|
};
|
|
2023
2083
|
applyLayer(globalConfig);
|
|
2024
2084
|
applyLayer(projectConfig);
|
|
2085
|
+
const shieldOverrides = readShieldOverrides();
|
|
2025
2086
|
for (const shieldName of readActiveShields()) {
|
|
2026
2087
|
const shield = getShield(shieldName);
|
|
2027
2088
|
if (!shield) continue;
|
|
2028
2089
|
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2090
|
+
const ruleOverrides = shieldOverrides[shieldName] ?? {};
|
|
2029
2091
|
for (const rule of shield.smartRules) {
|
|
2030
|
-
if (!existingRuleNames.has(rule.name))
|
|
2092
|
+
if (!existingRuleNames.has(rule.name)) {
|
|
2093
|
+
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
2094
|
+
mergedPolicy.smartRules.push(
|
|
2095
|
+
overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2031
2098
|
}
|
|
2032
2099
|
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
2033
2100
|
for (const word of shield.dangerousWords) {
|