@node9/proxy 1.1.3 → 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 +345 -35
- package/dist/cli.mjs +345 -35
- 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;
|
|
@@ -4515,9 +4628,15 @@ data: ${JSON.stringify(item.data)}
|
|
|
4515
4628
|
}
|
|
4516
4629
|
}
|
|
4517
4630
|
if (req.method === "GET" && pathname === "/settings") {
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4631
|
+
try {
|
|
4632
|
+
const s = getGlobalSettings();
|
|
4633
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4634
|
+
return res.end(JSON.stringify({ ...s, autoStarted }));
|
|
4635
|
+
} catch (err) {
|
|
4636
|
+
console.error(import_chalk4.default.red("[node9 daemon] GET /settings failed:"), err);
|
|
4637
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4638
|
+
return res.end(JSON.stringify({ error: "internal" }));
|
|
4639
|
+
}
|
|
4521
4640
|
}
|
|
4522
4641
|
if (req.method === "POST" && pathname === "/settings") {
|
|
4523
4642
|
if (!validToken(req)) return res.writeHead(403).end();
|
|
@@ -4540,9 +4659,15 @@ data: ${JSON.stringify(item.data)}
|
|
|
4540
4659
|
}
|
|
4541
4660
|
}
|
|
4542
4661
|
if (req.method === "GET" && pathname === "/slack-status") {
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4662
|
+
try {
|
|
4663
|
+
const s = getGlobalSettings();
|
|
4664
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4665
|
+
return res.end(JSON.stringify({ hasKey: hasStoredSlackKey(), enabled: s.slackEnabled }));
|
|
4666
|
+
} catch (err) {
|
|
4667
|
+
console.error(import_chalk4.default.red("[node9 daemon] GET /slack-status failed:"), err);
|
|
4668
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4669
|
+
return res.end(JSON.stringify({ error: "internal" }));
|
|
4670
|
+
}
|
|
4546
4671
|
}
|
|
4547
4672
|
if (req.method === "POST" && pathname === "/slack-key") {
|
|
4548
4673
|
if (!validToken(req)) return res.writeHead(403).end();
|
|
@@ -4691,6 +4816,13 @@ data: ${JSON.stringify(item.data)}
|
|
|
4691
4816
|
console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
4692
4817
|
process.exit(1);
|
|
4693
4818
|
});
|
|
4819
|
+
if (!daemonRejectionHandlerRegistered) {
|
|
4820
|
+
daemonRejectionHandlerRegistered = true;
|
|
4821
|
+
process.on("unhandledRejection", (reason) => {
|
|
4822
|
+
const stack = reason instanceof Error ? reason.stack : String(reason);
|
|
4823
|
+
console.error(import_chalk4.default.red("[node9 daemon] unhandled rejection \u2014 keeping daemon alive:"), stack);
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4694
4826
|
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
4695
4827
|
atomicWriteSync2(
|
|
4696
4828
|
DAEMON_PID_FILE,
|
|
@@ -4787,7 +4919,7 @@ function daemonStatus() {
|
|
|
4787
4919
|
console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
|
|
4788
4920
|
}
|
|
4789
4921
|
}
|
|
4790
|
-
var import_http, import_net2, import_fs5, import_path7, import_os4, import_child_process3, import_crypto3, import_chalk4, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
|
|
4922
|
+
var import_http, import_net2, import_fs5, import_path7, import_os4, import_child_process3, import_crypto3, import_chalk4, daemonRejectionHandlerRegistered, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
|
|
4791
4923
|
var init_daemon = __esm({
|
|
4792
4924
|
"src/daemon/index.ts"() {
|
|
4793
4925
|
"use strict";
|
|
@@ -4802,6 +4934,7 @@ var init_daemon = __esm({
|
|
|
4802
4934
|
import_chalk4 = __toESM(require("chalk"));
|
|
4803
4935
|
init_core();
|
|
4804
4936
|
init_shields();
|
|
4937
|
+
daemonRejectionHandlerRegistered = false;
|
|
4805
4938
|
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path7.default.join(import_os4.default.tmpdir(), "node9-activity.sock");
|
|
4806
4939
|
DAEMON_PORT2 = 7391;
|
|
4807
4940
|
DAEMON_HOST2 = "127.0.0.1";
|
|
@@ -6903,42 +7036,219 @@ shieldCmd.command("list").description("Show all available shields").action(() =>
|
|
|
6903
7036
|
}
|
|
6904
7037
|
console.log("");
|
|
6905
7038
|
});
|
|
6906
|
-
shieldCmd.command("status").description("Show
|
|
7039
|
+
shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
|
|
6907
7040
|
const active = readActiveShields();
|
|
6908
7041
|
if (active.length === 0) {
|
|
6909
|
-
console.
|
|
6910
|
-
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.
|
|
6911
7044
|
`);
|
|
6912
7045
|
return;
|
|
6913
7046
|
}
|
|
6914
|
-
|
|
7047
|
+
const overrides = readShieldOverrides();
|
|
7048
|
+
console.error(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
6915
7049
|
for (const name of active) {
|
|
6916
7050
|
const shield = getShield(name);
|
|
6917
7051
|
if (!shield) continue;
|
|
6918
|
-
console.
|
|
6919
|
-
|
|
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(
|
|
6920
7071
|
import_chalk6.default.gray(
|
|
6921
|
-
`
|
|
7072
|
+
` Tip: run ${import_chalk6.default.cyan("node9 shield unset <shield> <rule>")} to remove an override.
|
|
7073
|
+
`
|
|
6922
7074
|
)
|
|
6923
7075
|
);
|
|
6924
7076
|
}
|
|
6925
|
-
console.log("");
|
|
6926
7077
|
});
|
|
6927
|
-
|
|
6928
|
-
const
|
|
6929
|
-
if (
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
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.
|
|
6934
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)}`);
|
|
6935
7125
|
}
|
|
6936
|
-
|
|
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
|
+
);
|
|
6937
7140
|
} else {
|
|
6938
|
-
console.error(
|
|
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
|
+
`));
|
|
6939
7163
|
process.exit(1);
|
|
6940
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
|
+
`);
|
|
6941
7234
|
});
|
|
7235
|
+
if (process.argv[2] !== "daemon") {
|
|
7236
|
+
process.on("unhandledRejection", (reason) => {
|
|
7237
|
+
const isCheckHook = process.argv[2] === "check";
|
|
7238
|
+
if (isCheckHook) {
|
|
7239
|
+
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
7240
|
+
const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
7241
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
7242
|
+
import_fs8.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
7243
|
+
`);
|
|
7244
|
+
}
|
|
7245
|
+
process.exit(0);
|
|
7246
|
+
} else {
|
|
7247
|
+
console.error("[Node9] Unhandled error:", reason);
|
|
7248
|
+
process.exit(1);
|
|
7249
|
+
}
|
|
7250
|
+
});
|
|
7251
|
+
}
|
|
6942
7252
|
var knownSubcommands = new Set(program.commands.map((c) => c.name()));
|
|
6943
7253
|
var firstArg = process.argv[2];
|
|
6944
7254
|
if (firstArg && firstArg !== "--" && !firstArg.startsWith("-") && !knownSubcommands.has(firstArg)) {
|
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;
|
|
@@ -4502,9 +4615,15 @@ data: ${JSON.stringify(item.data)}
|
|
|
4502
4615
|
}
|
|
4503
4616
|
}
|
|
4504
4617
|
if (req.method === "GET" && pathname === "/settings") {
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4618
|
+
try {
|
|
4619
|
+
const s = getGlobalSettings();
|
|
4620
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4621
|
+
return res.end(JSON.stringify({ ...s, autoStarted }));
|
|
4622
|
+
} catch (err) {
|
|
4623
|
+
console.error(chalk4.red("[node9 daemon] GET /settings failed:"), err);
|
|
4624
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4625
|
+
return res.end(JSON.stringify({ error: "internal" }));
|
|
4626
|
+
}
|
|
4508
4627
|
}
|
|
4509
4628
|
if (req.method === "POST" && pathname === "/settings") {
|
|
4510
4629
|
if (!validToken(req)) return res.writeHead(403).end();
|
|
@@ -4527,9 +4646,15 @@ data: ${JSON.stringify(item.data)}
|
|
|
4527
4646
|
}
|
|
4528
4647
|
}
|
|
4529
4648
|
if (req.method === "GET" && pathname === "/slack-status") {
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4649
|
+
try {
|
|
4650
|
+
const s = getGlobalSettings();
|
|
4651
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4652
|
+
return res.end(JSON.stringify({ hasKey: hasStoredSlackKey(), enabled: s.slackEnabled }));
|
|
4653
|
+
} catch (err) {
|
|
4654
|
+
console.error(chalk4.red("[node9 daemon] GET /slack-status failed:"), err);
|
|
4655
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4656
|
+
return res.end(JSON.stringify({ error: "internal" }));
|
|
4657
|
+
}
|
|
4533
4658
|
}
|
|
4534
4659
|
if (req.method === "POST" && pathname === "/slack-key") {
|
|
4535
4660
|
if (!validToken(req)) return res.writeHead(403).end();
|
|
@@ -4678,6 +4803,13 @@ data: ${JSON.stringify(item.data)}
|
|
|
4678
4803
|
console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
4679
4804
|
process.exit(1);
|
|
4680
4805
|
});
|
|
4806
|
+
if (!daemonRejectionHandlerRegistered) {
|
|
4807
|
+
daemonRejectionHandlerRegistered = true;
|
|
4808
|
+
process.on("unhandledRejection", (reason) => {
|
|
4809
|
+
const stack = reason instanceof Error ? reason.stack : String(reason);
|
|
4810
|
+
console.error(chalk4.red("[node9 daemon] unhandled rejection \u2014 keeping daemon alive:"), stack);
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4681
4813
|
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
4682
4814
|
atomicWriteSync2(
|
|
4683
4815
|
DAEMON_PID_FILE,
|
|
@@ -4774,13 +4906,14 @@ function daemonStatus() {
|
|
|
4774
4906
|
console.log(chalk4.yellow("Node9 daemon: not running"));
|
|
4775
4907
|
}
|
|
4776
4908
|
}
|
|
4777
|
-
var ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
|
|
4909
|
+
var daemonRejectionHandlerRegistered, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
|
|
4778
4910
|
var init_daemon = __esm({
|
|
4779
4911
|
"src/daemon/index.ts"() {
|
|
4780
4912
|
"use strict";
|
|
4781
4913
|
init_ui2();
|
|
4782
4914
|
init_core();
|
|
4783
4915
|
init_shields();
|
|
4916
|
+
daemonRejectionHandlerRegistered = false;
|
|
4784
4917
|
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path7.join(os4.tmpdir(), "node9-activity.sock");
|
|
4785
4918
|
DAEMON_PORT2 = 7391;
|
|
4786
4919
|
DAEMON_HOST2 = "127.0.0.1";
|
|
@@ -6882,42 +7015,219 @@ shieldCmd.command("list").description("Show all available shields").action(() =>
|
|
|
6882
7015
|
}
|
|
6883
7016
|
console.log("");
|
|
6884
7017
|
});
|
|
6885
|
-
shieldCmd.command("status").description("Show
|
|
7018
|
+
shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
|
|
6886
7019
|
const active = readActiveShields();
|
|
6887
7020
|
if (active.length === 0) {
|
|
6888
|
-
console.
|
|
6889
|
-
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.
|
|
6890
7023
|
`);
|
|
6891
7024
|
return;
|
|
6892
7025
|
}
|
|
6893
|
-
|
|
7026
|
+
const overrides = readShieldOverrides();
|
|
7027
|
+
console.error(chalk6.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
6894
7028
|
for (const name of active) {
|
|
6895
7029
|
const shield = getShield(name);
|
|
6896
7030
|
if (!shield) continue;
|
|
6897
|
-
console.
|
|
6898
|
-
|
|
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(
|
|
6899
7050
|
chalk6.gray(
|
|
6900
|
-
`
|
|
7051
|
+
` Tip: run ${chalk6.cyan("node9 shield unset <shield> <rule>")} to remove an override.
|
|
7052
|
+
`
|
|
6901
7053
|
)
|
|
6902
7054
|
);
|
|
6903
7055
|
}
|
|
6904
|
-
console.log("");
|
|
6905
7056
|
});
|
|
6906
|
-
|
|
6907
|
-
const
|
|
6908
|
-
if (
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
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.
|
|
6913
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)}`);
|
|
6914
7104
|
}
|
|
6915
|
-
|
|
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
|
+
);
|
|
6916
7119
|
} else {
|
|
6917
|
-
console.error(
|
|
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
|
+
`));
|
|
6918
7142
|
process.exit(1);
|
|
6919
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
|
+
`);
|
|
6920
7213
|
});
|
|
7214
|
+
if (process.argv[2] !== "daemon") {
|
|
7215
|
+
process.on("unhandledRejection", (reason) => {
|
|
7216
|
+
const isCheckHook = process.argv[2] === "check";
|
|
7217
|
+
if (isCheckHook) {
|
|
7218
|
+
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
7219
|
+
const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
7220
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
7221
|
+
fs8.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
7222
|
+
`);
|
|
7223
|
+
}
|
|
7224
|
+
process.exit(0);
|
|
7225
|
+
} else {
|
|
7226
|
+
console.error("[Node9] Unhandled error:", reason);
|
|
7227
|
+
process.exit(1);
|
|
7228
|
+
}
|
|
7229
|
+
});
|
|
7230
|
+
}
|
|
6921
7231
|
var knownSubcommands = new Set(program.commands.map((c) => c.name()));
|
|
6922
7232
|
var firstArg = process.argv[2];
|
|
6923
7233
|
if (firstArg && firstArg !== "--" && !firstArg.startsWith("-") && !knownSubcommands.has(firstArg)) {
|
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) {
|