@node9/proxy 1.65.1 → 1.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +290 -51
- package/dist/cli.mjs +290 -51
- package/dist/dashboard.mjs +44 -21
- package/dist/index.js +167 -24
- package/dist/index.mjs +167 -24
- package/package.json +1 -1
package/dist/dashboard.mjs
CHANGED
|
@@ -2292,8 +2292,10 @@ var init_config_schema = __esm({
|
|
|
2292
2292
|
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
2293
2293
|
// Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
|
|
2294
2294
|
// inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
|
|
2295
|
-
// node9's own approver (terminal/native/cloud). Unset →
|
|
2296
|
-
//
|
|
2295
|
+
// node9's own approver (terminal/native/cloud). Unset → default ASK for
|
|
2296
|
+
// ask-capable agents (v2: cloud no longer disables inline — the outcome
|
|
2297
|
+
// ships to the dashboard as audit; admins force routing via managed
|
|
2298
|
+
// reviewChannel, which outranks the local --ask flag).
|
|
2297
2299
|
reviewChannel: z.enum(["ask", "approver"]).optional(),
|
|
2298
2300
|
// When true, agents may call WEAKENING node9 MCP tools (shield_disable,
|
|
2299
2301
|
// approver_set). Default (unset/false): those tools refuse over MCP — a human
|
|
@@ -2331,7 +2333,18 @@ var init_config_schema = __esm({
|
|
|
2331
2333
|
dlp: z.object({
|
|
2332
2334
|
enabled: z.boolean().optional(),
|
|
2333
2335
|
scanIgnoredTools: z.boolean().optional(),
|
|
2334
|
-
pii: z.enum(["off", "block"]).optional()
|
|
2336
|
+
pii: z.enum(["off", "block"]).optional(),
|
|
2337
|
+
reviewAction: z.enum(["review", "block"]).optional()
|
|
2338
|
+
}).optional(),
|
|
2339
|
+
// Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
|
|
2340
|
+
// deliberately exclude 'off' — tighten-only.
|
|
2341
|
+
commandChecks: z.object({
|
|
2342
|
+
inlineExec: z.enum(["off", "review", "block"]).optional(),
|
|
2343
|
+
rmAdvisory: z.enum(["off", "review", "block"]).optional(),
|
|
2344
|
+
chmod: z.enum(["off", "review", "block"]).optional(),
|
|
2345
|
+
sqlDdl: z.enum(["off", "review", "block"]).optional(),
|
|
2346
|
+
evalDynamic: z.enum(["review", "block"]).optional(),
|
|
2347
|
+
pipeChainHigh: z.enum(["review", "block"]).optional()
|
|
2335
2348
|
}).optional(),
|
|
2336
2349
|
egress: z.object({
|
|
2337
2350
|
enabled: z.boolean().optional(),
|
|
@@ -2960,17 +2973,6 @@ var init_costSync = __esm({
|
|
|
2960
2973
|
}
|
|
2961
2974
|
});
|
|
2962
2975
|
|
|
2963
|
-
// src/daemon/scan-watermark.ts
|
|
2964
|
-
var MAX_LINE_BYTES;
|
|
2965
|
-
var init_scan_watermark = __esm({
|
|
2966
|
-
"src/daemon/scan-watermark.ts"() {
|
|
2967
|
-
"use strict";
|
|
2968
|
-
init_dlp();
|
|
2969
|
-
init_dist();
|
|
2970
|
-
MAX_LINE_BYTES = 2 * 1024 * 1024;
|
|
2971
|
-
}
|
|
2972
|
-
});
|
|
2973
|
-
|
|
2974
2976
|
// src/audit/decision.ts
|
|
2975
2977
|
function classifyDecision(a, b) {
|
|
2976
2978
|
const isRow = !!a && typeof a === "object";
|
|
@@ -3005,16 +3007,38 @@ function classifyDecision(a, b) {
|
|
|
3005
3007
|
}
|
|
3006
3008
|
return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
|
|
3007
3009
|
}
|
|
3008
|
-
var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
|
|
3010
|
+
var HUMAN_SOURCES, TIMEOUT_SOURCES, NON_DECISION_SOURCES, has;
|
|
3009
3011
|
var init_decision = __esm({
|
|
3010
3012
|
"src/audit/decision.ts"() {
|
|
3011
3013
|
"use strict";
|
|
3012
|
-
HUMAN_SOURCES = /* @__PURE__ */ new Set([
|
|
3014
|
+
HUMAN_SOURCES = /* @__PURE__ */ new Set([
|
|
3015
|
+
"daemon",
|
|
3016
|
+
"cloud",
|
|
3017
|
+
"local-decision",
|
|
3018
|
+
"inline-review-approved",
|
|
3019
|
+
"inline-review"
|
|
3020
|
+
]);
|
|
3013
3021
|
TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
|
|
3022
|
+
NON_DECISION_SOURCES = /* @__PURE__ */ new Set([
|
|
3023
|
+
"post-hook",
|
|
3024
|
+
"inline-review-approved",
|
|
3025
|
+
"response-dlp"
|
|
3026
|
+
]);
|
|
3014
3027
|
has = (s, needle) => s.includes(needle);
|
|
3015
3028
|
}
|
|
3016
3029
|
});
|
|
3017
3030
|
|
|
3031
|
+
// src/daemon/scan-watermark.ts
|
|
3032
|
+
var MAX_LINE_BYTES;
|
|
3033
|
+
var init_scan_watermark = __esm({
|
|
3034
|
+
"src/daemon/scan-watermark.ts"() {
|
|
3035
|
+
"use strict";
|
|
3036
|
+
init_dlp();
|
|
3037
|
+
init_dist();
|
|
3038
|
+
MAX_LINE_BYTES = 2 * 1024 * 1024;
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3018
3042
|
// src/cli/aggregate/report-audit.ts
|
|
3019
3043
|
import fs5 from "fs";
|
|
3020
3044
|
import os6 from "os";
|
|
@@ -3574,8 +3598,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3574
3598
|
const priorEnd = new Date(start.getTime() - 1);
|
|
3575
3599
|
const priorStart = new Date(start.getTime() - periodMs);
|
|
3576
3600
|
const priorEntries = allEntries.filter((e) => {
|
|
3577
|
-
if (e.source === "
|
|
3578
|
-
if (e.source === "response-dlp") return false;
|
|
3601
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
3579
3602
|
if (typeof e.decision !== "string") return false;
|
|
3580
3603
|
const ts = new Date(e.ts);
|
|
3581
3604
|
return ts >= priorStart && ts <= priorEnd;
|
|
@@ -3586,8 +3609,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3586
3609
|
const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
|
|
3587
3610
|
let excludedTests = 0;
|
|
3588
3611
|
const entries = allEntries.filter((e) => {
|
|
3589
|
-
if (e.source === "
|
|
3590
|
-
if (e.source === "response-dlp") return false;
|
|
3612
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
3591
3613
|
if (typeof e.decision !== "string") return false;
|
|
3592
3614
|
const ts = new Date(e.ts);
|
|
3593
3615
|
if (ts < start || ts > end) return false;
|
|
@@ -4915,7 +4937,7 @@ function readAuditEntriesAsync(chunkSize = 1e3, customPath) {
|
|
|
4915
4937
|
}
|
|
4916
4938
|
function aggregateAudit(entries, startMs, endMs = Date.now()) {
|
|
4917
4939
|
const inWindow = entries.filter((e) => {
|
|
4918
|
-
if (e.source === "
|
|
4940
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
4919
4941
|
const t = Date.parse(e.ts);
|
|
4920
4942
|
return t >= startMs && t <= endMs;
|
|
4921
4943
|
});
|
|
@@ -5361,6 +5383,7 @@ var init_data = __esm({
|
|
|
5361
5383
|
init_daemon();
|
|
5362
5384
|
init_costSync();
|
|
5363
5385
|
init_shields();
|
|
5386
|
+
init_decision();
|
|
5364
5387
|
init_scan_watermark();
|
|
5365
5388
|
init_report_audit();
|
|
5366
5389
|
init_litellm();
|
package/dist/index.js
CHANGED
|
@@ -91,7 +91,7 @@ function redactSecrets(text) {
|
|
|
91
91
|
if (!text) return text;
|
|
92
92
|
let redacted = text;
|
|
93
93
|
redacted = redacted.replace(
|
|
94
|
-
/(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._
|
|
94
|
+
/(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/=]+/gi,
|
|
95
95
|
"$1********"
|
|
96
96
|
);
|
|
97
97
|
redacted = redacted.replace(
|
|
@@ -152,7 +152,7 @@ function filePathFromArgs(args) {
|
|
|
152
152
|
return void 0;
|
|
153
153
|
}
|
|
154
154
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
155
|
-
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
|
|
155
|
+
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern) || /dlp|taint/i.test(String(meta?.ruleName ?? ""));
|
|
156
156
|
const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
|
|
157
157
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview ? { argsPreview: preview } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
158
158
|
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
@@ -314,8 +314,10 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
314
314
|
agentPolicy: import_zod.z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
315
315
|
// Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
|
|
316
316
|
// inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
|
|
317
|
-
// node9's own approver (terminal/native/cloud). Unset →
|
|
318
|
-
//
|
|
317
|
+
// node9's own approver (terminal/native/cloud). Unset → default ASK for
|
|
318
|
+
// ask-capable agents (v2: cloud no longer disables inline — the outcome
|
|
319
|
+
// ships to the dashboard as audit; admins force routing via managed
|
|
320
|
+
// reviewChannel, which outranks the local --ask flag).
|
|
319
321
|
reviewChannel: import_zod.z.enum(["ask", "approver"]).optional(),
|
|
320
322
|
// When true, agents may call WEAKENING node9 MCP tools (shield_disable,
|
|
321
323
|
// approver_set). Default (unset/false): those tools refuse over MCP — a human
|
|
@@ -353,7 +355,18 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
353
355
|
dlp: import_zod.z.object({
|
|
354
356
|
enabled: import_zod.z.boolean().optional(),
|
|
355
357
|
scanIgnoredTools: import_zod.z.boolean().optional(),
|
|
356
|
-
pii: import_zod.z.enum(["off", "block"]).optional()
|
|
358
|
+
pii: import_zod.z.enum(["off", "block"]).optional(),
|
|
359
|
+
reviewAction: import_zod.z.enum(["review", "block"]).optional()
|
|
360
|
+
}).optional(),
|
|
361
|
+
// Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
|
|
362
|
+
// deliberately exclude 'off' — tighten-only.
|
|
363
|
+
commandChecks: import_zod.z.object({
|
|
364
|
+
inlineExec: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
365
|
+
rmAdvisory: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
366
|
+
chmod: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
367
|
+
sqlDdl: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
368
|
+
evalDynamic: import_zod.z.enum(["review", "block"]).optional(),
|
|
369
|
+
pipeChainHigh: import_zod.z.enum(["review", "block"]).optional()
|
|
357
370
|
}).optional(),
|
|
358
371
|
egress: import_zod.z.object({
|
|
359
372
|
enabled: import_zod.z.boolean().optional(),
|
|
@@ -2365,6 +2378,47 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2365
2378
|
});
|
|
2366
2379
|
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
2367
2380
|
}
|
|
2381
|
+
function resolveCheck(v) {
|
|
2382
|
+
return v === "off" || v === "block" ? v : "review";
|
|
2383
|
+
}
|
|
2384
|
+
function resolveCheckTight(v) {
|
|
2385
|
+
return v === "block" ? "block" : "review";
|
|
2386
|
+
}
|
|
2387
|
+
var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
|
|
2388
|
+
var INLINE_SHELL = /^(bash|sh|zsh)$/i;
|
|
2389
|
+
function detectInlineExec(command) {
|
|
2390
|
+
const pipeFed = command.includes("|");
|
|
2391
|
+
const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
|
|
2392
|
+
for (const rawSeg of segments) {
|
|
2393
|
+
const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
|
|
2394
|
+
let i = 0;
|
|
2395
|
+
while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
|
|
2396
|
+
if (i >= tokens.length) continue;
|
|
2397
|
+
const base = tokens[i].split("/").pop() ?? tokens[i];
|
|
2398
|
+
if (!INLINE_INTERP.test(base)) continue;
|
|
2399
|
+
const args = tokens.slice(i + 1);
|
|
2400
|
+
let hadRedirect = false;
|
|
2401
|
+
const positionals = [];
|
|
2402
|
+
for (let j = 0; j < args.length; j++) {
|
|
2403
|
+
const a = args[j];
|
|
2404
|
+
if (a === "-") return true;
|
|
2405
|
+
if (a.startsWith("<")) {
|
|
2406
|
+
hadRedirect = true;
|
|
2407
|
+
if (a === "<" || a === "<<") j++;
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
if (a.startsWith("-")) {
|
|
2411
|
+
if (/^-(c|e|eval)$/i.test(a)) return true;
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
positionals.push(a);
|
|
2415
|
+
}
|
|
2416
|
+
if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
|
|
2417
|
+
return true;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
return false;
|
|
2421
|
+
}
|
|
2368
2422
|
var VERDICT_RANK = {
|
|
2369
2423
|
allow: 0,
|
|
2370
2424
|
review: 1,
|
|
@@ -2397,7 +2451,7 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
2397
2451
|
return fieldName === "sql" || fieldName === "query";
|
|
2398
2452
|
}
|
|
2399
2453
|
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2400
|
-
function pipeChainVerdict(command, isTrustedHost2) {
|
|
2454
|
+
function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
2401
2455
|
const pipeAnalysis = analyzePipeChain(command);
|
|
2402
2456
|
if (!pipeAnalysis.isPipeline) return null;
|
|
2403
2457
|
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
@@ -2428,7 +2482,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
|
|
|
2428
2482
|
};
|
|
2429
2483
|
}
|
|
2430
2484
|
return {
|
|
2431
|
-
decision:
|
|
2485
|
+
decision: highAction,
|
|
2432
2486
|
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
2433
2487
|
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
2434
2488
|
tier: 3
|
|
@@ -2442,7 +2496,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2442
2496
|
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
2443
2497
|
if (dlpMatch) {
|
|
2444
2498
|
return {
|
|
2445
|
-
|
|
2499
|
+
// reviewAction:'block' (inline-ask v2): the admin upgraded
|
|
2500
|
+
// review-severity matches to a hard block — every evaluatePolicy
|
|
2501
|
+
// caller (orchestrator, explain, gateway) must agree with the gate.
|
|
2502
|
+
decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
|
|
2446
2503
|
blockedByLabel: `DLP: ${dlpMatch.patternName}`,
|
|
2447
2504
|
reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
|
|
2448
2505
|
};
|
|
@@ -2451,7 +2508,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2451
2508
|
if (wouldBeIgnored) return { decision: "allow" };
|
|
2452
2509
|
const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
|
|
2453
2510
|
if (bashCommand !== null) {
|
|
2454
|
-
const pipeVerdict = pipeChainVerdict(
|
|
2511
|
+
const pipeVerdict = pipeChainVerdict(
|
|
2512
|
+
bashCommand,
|
|
2513
|
+
isTrustedHost2,
|
|
2514
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2515
|
+
);
|
|
2455
2516
|
if (pipeVerdict) return pipeVerdict;
|
|
2456
2517
|
const fsVerdict = analyzeFsOperation(bashCommand);
|
|
2457
2518
|
if (fsVerdict) {
|
|
@@ -2466,10 +2527,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2466
2527
|
ruleDescription: fsVerdict.reason
|
|
2467
2528
|
};
|
|
2468
2529
|
}
|
|
2469
|
-
const
|
|
2530
|
+
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
2531
|
+
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
2470
2532
|
if (sqlVerdict) {
|
|
2471
2533
|
return {
|
|
2472
|
-
|
|
2534
|
+
// analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
|
|
2535
|
+
decision: sqlAction === "block" ? "block" : "review",
|
|
2473
2536
|
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
2474
2537
|
reason: sqlVerdict.reason,
|
|
2475
2538
|
tier: 2,
|
|
@@ -2477,10 +2540,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2477
2540
|
ruleDescription: sqlVerdict.description
|
|
2478
2541
|
};
|
|
2479
2542
|
}
|
|
2480
|
-
const
|
|
2543
|
+
const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
|
|
2544
|
+
const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
|
|
2481
2545
|
if (chmodVerdict) {
|
|
2482
2546
|
return {
|
|
2483
|
-
|
|
2547
|
+
// analyzeChmod777 is typed review-only, so the knob maps 1:1.
|
|
2548
|
+
decision: chmodAction === "block" ? "block" : "review",
|
|
2484
2549
|
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
2485
2550
|
reason: chmodVerdict.reason,
|
|
2486
2551
|
tier: 2,
|
|
@@ -2523,10 +2588,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2523
2588
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2524
2589
|
allTokens = analyzed.allTokens;
|
|
2525
2590
|
pathTokens = analyzed.paths;
|
|
2526
|
-
const
|
|
2527
|
-
if (
|
|
2591
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2592
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2528
2593
|
return {
|
|
2529
|
-
decision: "review",
|
|
2594
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2530
2595
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2531
2596
|
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2532
2597
|
tier: 3
|
|
@@ -2544,14 +2609,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2544
2609
|
}
|
|
2545
2610
|
if (evalVerdict === "review") {
|
|
2546
2611
|
return {
|
|
2547
|
-
|
|
2612
|
+
// Class B tighten-only: commandChecks.evalDynamic may upgrade to
|
|
2613
|
+
// block but can never turn this off (eval-remote above is Class A —
|
|
2614
|
+
// no knob at all).
|
|
2615
|
+
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2548
2616
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2549
2617
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2550
2618
|
ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
|
|
2551
2619
|
tier: 3
|
|
2552
2620
|
};
|
|
2553
2621
|
}
|
|
2554
|
-
const ptVerdict = pipeChainVerdict(
|
|
2622
|
+
const ptVerdict = pipeChainVerdict(
|
|
2623
|
+
shellCommand,
|
|
2624
|
+
isTrustedHost2,
|
|
2625
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2626
|
+
);
|
|
2555
2627
|
if (ptVerdict) return ptVerdict;
|
|
2556
2628
|
if (config.policy.egress?.enabled) {
|
|
2557
2629
|
const dests = extractShellDestinations(shellCommand);
|
|
@@ -3638,6 +3710,7 @@ function applyManagedEgress(local, managed, locked) {
|
|
|
3638
3710
|
return next;
|
|
3639
3711
|
}
|
|
3640
3712
|
var DLP_PII_ORDER = ["off", "block"];
|
|
3713
|
+
var DLP_REVIEW_ACTION_ORDER = ["review", "block"];
|
|
3641
3714
|
function applyManagedDlp(local, managed, locked) {
|
|
3642
3715
|
const next = { ...local };
|
|
3643
3716
|
if (typeof managed.enabled === "boolean") {
|
|
@@ -3651,6 +3724,40 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
3651
3724
|
locked.includes("dlpPii")
|
|
3652
3725
|
);
|
|
3653
3726
|
}
|
|
3727
|
+
if (typeof managed.reviewAction === "string") {
|
|
3728
|
+
next.reviewAction = resolveByOrder(
|
|
3729
|
+
DLP_REVIEW_ACTION_ORDER,
|
|
3730
|
+
local.reviewAction ?? "review",
|
|
3731
|
+
managed.reviewAction,
|
|
3732
|
+
locked.includes("dlpReviewAction")
|
|
3733
|
+
);
|
|
3734
|
+
}
|
|
3735
|
+
return next;
|
|
3736
|
+
}
|
|
3737
|
+
var COMMAND_CHECK_ORDER = ["off", "review", "block"];
|
|
3738
|
+
var COMMAND_CHECK_KEYS = [
|
|
3739
|
+
"inlineExec",
|
|
3740
|
+
"rmAdvisory",
|
|
3741
|
+
"chmod",
|
|
3742
|
+
"sqlDdl",
|
|
3743
|
+
"evalDynamic",
|
|
3744
|
+
"pipeChainHigh"
|
|
3745
|
+
];
|
|
3746
|
+
function applyManagedCommandChecks(local, managed, locked) {
|
|
3747
|
+
const next = { ...local };
|
|
3748
|
+
for (const key of COMMAND_CHECK_KEYS) {
|
|
3749
|
+
const m = managed[key];
|
|
3750
|
+
if (typeof m !== "string") continue;
|
|
3751
|
+
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
3752
|
+
const resolved = resolveByOrder(
|
|
3753
|
+
COMMAND_CHECK_ORDER,
|
|
3754
|
+
local[key] ?? "review",
|
|
3755
|
+
m,
|
|
3756
|
+
locked.includes(lockKey)
|
|
3757
|
+
);
|
|
3758
|
+
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
3759
|
+
next[key] = resolved;
|
|
3760
|
+
}
|
|
3654
3761
|
return next;
|
|
3655
3762
|
}
|
|
3656
3763
|
function applyManagedApprovers(local, managed) {
|
|
@@ -4221,6 +4328,22 @@ function getConfig(cwd) {
|
|
|
4221
4328
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
4222
4329
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
4223
4330
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4331
|
+
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4332
|
+
}
|
|
4333
|
+
if (p.commandChecks && typeof p.commandChecks === "object") {
|
|
4334
|
+
const src = p.commandChecks;
|
|
4335
|
+
const cc2 = {
|
|
4336
|
+
...mergedPolicy.commandChecks
|
|
4337
|
+
};
|
|
4338
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4339
|
+
const v = src[k];
|
|
4340
|
+
if (v === "off" || v === "review" || v === "block") cc2[k] = v;
|
|
4341
|
+
}
|
|
4342
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4343
|
+
const v = src[k];
|
|
4344
|
+
if (v === "review" || v === "block") cc2[k] = v;
|
|
4345
|
+
}
|
|
4346
|
+
if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
|
|
4224
4347
|
}
|
|
4225
4348
|
if (p.egress) {
|
|
4226
4349
|
const e = p.egress;
|
|
@@ -4317,11 +4440,19 @@ function getConfig(cwd) {
|
|
|
4317
4440
|
mergedPolicy.dlp,
|
|
4318
4441
|
{
|
|
4319
4442
|
enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
|
|
4320
|
-
pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
|
|
4443
|
+
pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
|
|
4444
|
+
reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
|
|
4321
4445
|
},
|
|
4322
4446
|
locked
|
|
4323
4447
|
);
|
|
4324
4448
|
}
|
|
4449
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
4450
|
+
mergedPolicy.commandChecks = applyManagedCommandChecks(
|
|
4451
|
+
mergedPolicy.commandChecks ?? {},
|
|
4452
|
+
mc.commandChecks,
|
|
4453
|
+
locked
|
|
4454
|
+
);
|
|
4455
|
+
}
|
|
4325
4456
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4326
4457
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
4327
4458
|
mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
|
|
@@ -4333,6 +4464,7 @@ function getConfig(cwd) {
|
|
|
4333
4464
|
}
|
|
4334
4465
|
if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
|
|
4335
4466
|
mergedSettings.reviewChannel = mc.reviewChannel;
|
|
4467
|
+
mergedSettings.reviewChannelManaged = true;
|
|
4336
4468
|
}
|
|
4337
4469
|
if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
|
|
4338
4470
|
mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
|
|
@@ -4430,8 +4562,17 @@ function getConfig(cwd) {
|
|
|
4430
4562
|
}
|
|
4431
4563
|
}
|
|
4432
4564
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4565
|
+
const cc = mergedPolicy.commandChecks ?? {};
|
|
4566
|
+
const advisoryKnob = (name) => {
|
|
4567
|
+
if (name === "review-rm") return cc.rmAdvisory;
|
|
4568
|
+
if (name?.endsWith("-sql")) return cc.sqlDdl;
|
|
4569
|
+
return void 0;
|
|
4570
|
+
};
|
|
4433
4571
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4434
|
-
if (
|
|
4572
|
+
if (existingAdvisoryNames.has(rule.name)) continue;
|
|
4573
|
+
const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
|
|
4574
|
+
if (knob === "off") continue;
|
|
4575
|
+
mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
|
|
4435
4576
|
}
|
|
4436
4577
|
const envMode = process.env.NODE9_MODE;
|
|
4437
4578
|
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
@@ -5691,7 +5832,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5691
5832
|
const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
|
|
5692
5833
|
if (dlpMatch) {
|
|
5693
5834
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
5694
|
-
if (dlpMatch.severity === "block") {
|
|
5835
|
+
if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
|
|
5695
5836
|
let dlpEid;
|
|
5696
5837
|
if (!isManual)
|
|
5697
5838
|
dlpEid = appendLocalAudit(
|
|
@@ -6029,12 +6170,14 @@ ${appPermReview}`
|
|
|
6029
6170
|
);
|
|
6030
6171
|
}
|
|
6031
6172
|
}
|
|
6032
|
-
|
|
6033
|
-
if (options?.deferReview && !hardBlockDowngraded && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
|
|
6173
|
+
if (options?.deferReview && !hardBlockDowngraded) {
|
|
6034
6174
|
return {
|
|
6035
6175
|
approved: false,
|
|
6036
6176
|
review: true,
|
|
6037
|
-
|
|
6177
|
+
// The prompt must say WHY: the taint sentence beats the bare label so
|
|
6178
|
+
// the dev sees the actual risk context inline. (No appPermReview term:
|
|
6179
|
+
// deferReview and serverKey never co-occur in production — see above.)
|
|
6180
|
+
reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
|
|
6038
6181
|
ruleDescription: policyRuleDescription,
|
|
6039
6182
|
blockedByLabel: explainableLabel
|
|
6040
6183
|
};
|