@node9/policy-engine 1.0.0 → 1.4.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/index.js CHANGED
@@ -31,15 +31,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  BUILTIN_SHIELDS: () => BUILTIN_SHIELDS,
34
+ COST_PER_LOOP_ITER_USD: () => COST_PER_LOOP_ITER_USD,
34
35
  DLP_PATTERNS: () => DLP_PATTERNS,
35
36
  ENGINE_VERSION: () => ENGINE_VERSION,
36
37
  FLAGS_WITH_VALUES: () => FLAGS_WITH_VALUES,
37
38
  LOOP_MAX_RECORDS: () => LOOP_MAX_RECORDS,
39
+ LOOP_THRESHOLD_FOR_WASTE: () => LOOP_THRESHOLD_FOR_WASTE,
40
+ SCAN_SIGNAL_WEIGHTS: () => SCAN_SIGNAL_WEIGHTS,
38
41
  SENSITIVE_PATH_REGEXES: () => SENSITIVE_PATH_REGEXES,
39
42
  analyzePipeChain: () => analyzePipeChain,
40
43
  analyzeShellCommand: () => analyzeShellCommand,
41
44
  checkDangerousSql: () => checkDangerousSql,
45
+ classifyAuditEntry: () => classifyAuditEntry,
46
+ classifyRuleSeverity: () => classifyRuleSeverity,
47
+ classifyScanSignal: () => classifyScanSignal,
42
48
  computeArgsHash: () => computeArgsHash,
49
+ computeBlendedSecurityScore: () => computeBlendedSecurityScore,
50
+ computeScanScore: () => computeScanScore,
51
+ computeSecurityScore: () => computeSecurityScore,
43
52
  detectDangerousEval: () => detectDangerousEval,
44
53
  detectDangerousShellExec: () => detectDangerousShellExec,
45
54
  evaluateLoopWindow: () => evaluateLoopWindow,
@@ -54,12 +63,16 @@ __export(src_exports, {
54
63
  isShieldVerdict: () => isShieldVerdict,
55
64
  matchSensitivePath: () => matchSensitivePath,
56
65
  matchesPattern: () => matchesPattern,
66
+ narrativeRuleLabel: () => narrativeRuleLabel,
57
67
  normalizeCommandForPolicy: () => normalizeCommandForPolicy,
58
68
  parseAllSshHostsFromCommand: () => parseAllSshHostsFromCommand,
59
69
  redactText: () => redactText,
60
70
  scanArgs: () => scanArgs,
61
71
  scanText: () => scanText,
62
72
  sensitivePathMatch: () => sensitivePathMatch,
73
+ summarizeBlast: () => summarizeBlast,
74
+ summarizeScan: () => summarizeScan,
75
+ truncateBlastPath: () => truncateBlastPath,
63
76
  validateOverrides: () => validateOverrides,
64
77
  validateRegex: () => validateRegex,
65
78
  validateShieldDefinition: () => validateShieldDefinition
@@ -67,6 +80,7 @@ __export(src_exports, {
67
80
  module.exports = __toCommonJS(src_exports);
68
81
 
69
82
  // src/dlp/index.ts
83
+ var import_safe_regex2 = __toESM(require("safe-regex2"));
70
84
  var ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
71
85
  function isAssignmentContext(text) {
72
86
  return ASSIGNMENT_CONTEXT_RE.test(text);
@@ -550,6 +564,23 @@ function sensitivePathMatch(originalPath) {
550
564
  };
551
565
  }
552
566
  var SENSITIVE_PATH_REGEXES = SENSITIVE_PATH_PATTERNS;
567
+ function assertBuiltinPatternsAreSafe() {
568
+ for (const p of DLP_PATTERNS) {
569
+ if (!(0, import_safe_regex2.default)(p.regex.source)) {
570
+ throw new Error(
571
+ `[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
572
+ );
573
+ }
574
+ }
575
+ for (const re of SENSITIVE_PATH_PATTERNS) {
576
+ if (!(0, import_safe_regex2.default)(re.source)) {
577
+ throw new Error(
578
+ `[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
579
+ );
580
+ }
581
+ }
582
+ }
583
+ assertBuiltinPatternsAreSafe();
553
584
  function maskSecret(raw, pattern) {
554
585
  const match = raw.match(pattern);
555
586
  if (!match) return "****";
@@ -1141,7 +1172,7 @@ function parseAllSshHostsFromCommand(command) {
1141
1172
  var import_picomatch = __toESM(require("picomatch"));
1142
1173
 
1143
1174
  // src/utils/regex.ts
1144
- var import_safe_regex2 = __toESM(require("safe-regex2"));
1175
+ var import_safe_regex22 = __toESM(require("safe-regex2"));
1145
1176
  var MAX_REGEX_LENGTH = 100;
1146
1177
  var REGEX_CACHE_MAX = 500;
1147
1178
  var regexCache = /* @__PURE__ */ new Map();
@@ -1154,7 +1185,7 @@ function validateRegex(pattern) {
1154
1185
  return `Invalid regex syntax: ${e.message}`;
1155
1186
  }
1156
1187
  if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
1157
- if (!(0, import_safe_regex2.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
1188
+ if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
1158
1189
  return null;
1159
1190
  }
1160
1191
  function getCompiledRegex(pattern, flags = "") {
@@ -1191,9 +1222,14 @@ function matchesPattern(text, patterns) {
1191
1222
  const withoutDotSlash = text.replace(/^\.\//, "");
1192
1223
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1193
1224
  }
1225
+ var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1194
1226
  function getNestedValue(obj, path) {
1195
1227
  if (!obj || typeof obj !== "object") return null;
1196
- return path.split(".").reduce((prev, curr) => prev?.[curr], obj);
1228
+ const segments = path.split(".");
1229
+ for (const seg of segments) {
1230
+ if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
1231
+ }
1232
+ return segments.reduce((prev, curr) => prev?.[curr], obj);
1197
1233
  }
1198
1234
  function evaluateSmartConditions(args, rule) {
1199
1235
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -1479,6 +1515,9 @@ function isIgnoredTool(toolName, config) {
1479
1515
  return matchesPattern(toolName, config.policy.ignoredTools);
1480
1516
  }
1481
1517
 
1518
+ // src/shields/index.ts
1519
+ var import_safe_regex23 = __toESM(require("safe-regex2"));
1520
+
1482
1521
  // src/shields/builtin/aws.json
1483
1522
  var aws_default = {
1484
1523
  name: "aws",
@@ -1910,15 +1949,6 @@ var k8s_default = {
1910
1949
  dangerousWords: []
1911
1950
  };
1912
1951
 
1913
- // src/shields/builtin/mcp-tool-gating.json
1914
- var mcp_tool_gating_default = {
1915
- name: "mcp-tool-gating",
1916
- description: "Intercept MCP tool lists and require user approval before the agent can use any tools from a new server",
1917
- aliases: ["mcp-gating", "mcp-tools"],
1918
- smartRules: [],
1919
- dangerousWords: []
1920
- };
1921
-
1922
1952
  // src/shields/builtin/mongodb.json
1923
1953
  var mongodb_default = {
1924
1954
  name: "mongodb",
@@ -2233,12 +2263,29 @@ var BUILTIN_SHIELDS = {
2233
2263
  [filesystem_default.name]: filesystem_default,
2234
2264
  [github_default.name]: github_default,
2235
2265
  [k8s_default.name]: k8s_default,
2236
- [mcp_tool_gating_default.name]: mcp_tool_gating_default,
2237
2266
  [mongodb_default.name]: mongodb_default,
2238
2267
  [postgres_default.name]: postgres_default,
2239
2268
  [project_jail_default.name]: project_jail_default,
2240
2269
  [redis_default.name]: redis_default
2241
2270
  };
2271
+ function assertBuiltinShieldRegexesAreSafe() {
2272
+ for (const shield of Object.values(BUILTIN_SHIELDS)) {
2273
+ for (const rule of shield.smartRules) {
2274
+ const conditions = rule.conditions ?? [];
2275
+ for (const cond of conditions) {
2276
+ if (cond.op !== "matches" && cond.op !== "notMatches") continue;
2277
+ const pattern = cond.value;
2278
+ if (!pattern) continue;
2279
+ if (!(0, import_safe_regex23.default)(pattern)) {
2280
+ throw new Error(
2281
+ `[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has unsafe regex: ${pattern}`
2282
+ );
2283
+ }
2284
+ }
2285
+ }
2286
+ }
2287
+ }
2288
+ assertBuiltinShieldRegexesAreSafe();
2242
2289
 
2243
2290
  // src/loop/index.ts
2244
2291
  var import_crypto = __toESM(require("crypto"));
@@ -2257,20 +2304,267 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
2257
2304
  return { nextRecords, count, looping: count >= threshold };
2258
2305
  }
2259
2306
 
2307
+ // src/scan/index.ts
2308
+ function emptySignals() {
2309
+ return {
2310
+ dlpFindings: 0,
2311
+ piiFindings: 0,
2312
+ sensitiveFileReads: 0,
2313
+ privilegeEscalation: 0,
2314
+ networkExfil: 0,
2315
+ pipeToShell: 0,
2316
+ evalOfRemote: 0,
2317
+ destructiveOps: 0,
2318
+ loops: 0,
2319
+ longOutputRedactions: 0
2320
+ };
2321
+ }
2322
+ var FINDING_TO_SIGNAL = {
2323
+ dlp: "dlpFindings",
2324
+ pii: "piiFindings",
2325
+ "sensitive-file-read": "sensitiveFileReads",
2326
+ "privilege-escalation": "privilegeEscalation",
2327
+ "network-exfil": "networkExfil",
2328
+ "pipe-to-shell": "pipeToShell",
2329
+ "eval-of-remote": "evalOfRemote",
2330
+ "destructive-op": "destructiveOps",
2331
+ loop: "loops",
2332
+ "long-output-redacted": "longOutputRedactions"
2333
+ };
2334
+ var SCAN_SIGNAL_WEIGHTS = {
2335
+ dlpFindings: 30,
2336
+ piiFindings: 10,
2337
+ sensitiveFileReads: 20,
2338
+ privilegeEscalation: 15,
2339
+ networkExfil: 25,
2340
+ pipeToShell: 30,
2341
+ evalOfRemote: 30,
2342
+ destructiveOps: 15,
2343
+ loops: 3,
2344
+ longOutputRedactions: 1
2345
+ };
2346
+ function computeScanScore(signals) {
2347
+ let deduction = 0;
2348
+ for (const key of Object.keys(signals)) {
2349
+ deduction += signals[key] * SCAN_SIGNAL_WEIGHTS[key];
2350
+ }
2351
+ return Math.max(0, Math.min(100, 100 - deduction));
2352
+ }
2353
+ var LOOP_THRESHOLD_FOR_WASTE = 3;
2354
+ var COST_PER_LOOP_ITER_USD = 6e-3;
2355
+ function summarizeScan(findings, opts = {}) {
2356
+ const totalToolCalls = opts.totalToolCalls ?? 0;
2357
+ const topN = opts.topN ?? 10;
2358
+ const signals = emptySignals();
2359
+ const sessionIds = /* @__PURE__ */ new Set();
2360
+ const patternCounts = /* @__PURE__ */ new Map();
2361
+ for (const f of findings) {
2362
+ sessionIds.add(f.sessionId);
2363
+ const key = FINDING_TO_SIGNAL[f.type];
2364
+ signals[key]++;
2365
+ if (f.patternName) {
2366
+ patternCounts.set(f.patternName, (patternCounts.get(f.patternName) ?? 0) + 1);
2367
+ }
2368
+ }
2369
+ const topPatterns = [...patternCounts.entries()].sort((a, b) => {
2370
+ if (b[1] !== a[1]) return b[1] - a[1];
2371
+ return a[0].localeCompare(b[0]);
2372
+ }).slice(0, topN).map(([patternName, count]) => ({ patternName, count }));
2373
+ return {
2374
+ totalSessions: sessionIds.size,
2375
+ totalToolCalls,
2376
+ signals,
2377
+ topPatterns,
2378
+ score: computeScanScore(signals)
2379
+ };
2380
+ }
2381
+
2382
+ // src/severity/index.ts
2383
+ function classifyRuleSeverity(name, verdict) {
2384
+ const n = name.toLowerCase();
2385
+ const criticalPatterns = [
2386
+ "rm-rf",
2387
+ "eval-remote",
2388
+ "eval-curl",
2389
+ "read-aws",
2390
+ "read-ssh",
2391
+ "read-gcp",
2392
+ "read-cred",
2393
+ "delete-repo",
2394
+ "helm-uninstall",
2395
+ "drop-table",
2396
+ "drop-database",
2397
+ "drop-collection",
2398
+ "truncate",
2399
+ "flushall",
2400
+ "flushdb",
2401
+ "pipe-shell"
2402
+ ];
2403
+ if (criticalPatterns.some((p) => n.includes(p))) return "critical";
2404
+ const highPatterns = [
2405
+ "force-push",
2406
+ "force_push",
2407
+ "git-destructive",
2408
+ "reset-hard",
2409
+ "rebase",
2410
+ "delete-branch",
2411
+ "delete-remote"
2412
+ ];
2413
+ if (highPatterns.some((p) => n.includes(p))) return "high";
2414
+ if (verdict === "block") return "high";
2415
+ return "medium";
2416
+ }
2417
+ function narrativeRuleLabel(name) {
2418
+ const stripped = stripRulePrefixes(name);
2419
+ const map = {
2420
+ "read-aws": "AWS credentials read",
2421
+ "read-ssh": "SSH private key read",
2422
+ "read-gcp": "GCP credentials read",
2423
+ "read-cred": "credential file read",
2424
+ "delete-repo": "GitHub repository deletion",
2425
+ "helm-uninstall": "helm uninstall",
2426
+ "rm-rf-home": "rm -rf on home directory",
2427
+ "rm-rf": "rm -rf",
2428
+ "eval-remote": "eval of remote download",
2429
+ "eval-curl": "eval of curl output",
2430
+ "pipe-shell": "curl | bash",
2431
+ "drop-table": "DROP TABLE",
2432
+ "drop-database": "DROP DATABASE",
2433
+ "drop-collection": "DROP COLLECTION",
2434
+ truncate: "TRUNCATE",
2435
+ flushall: "Redis FLUSHALL",
2436
+ flushdb: "Redis FLUSHDB",
2437
+ "force-push": "force pushes",
2438
+ force_push: "force pushes",
2439
+ "reset-hard": "git reset --hard",
2440
+ "git-destructive": "destructive git operations",
2441
+ "delete-branch": "branch deletion",
2442
+ "delete-remote": "remote deletion",
2443
+ rebase: "git rebase",
2444
+ rm: "rm calls",
2445
+ sudo: "sudo calls",
2446
+ "eval-dynamic": "dynamic eval",
2447
+ "config-set": "Redis CONFIG SET"
2448
+ };
2449
+ for (const [key, label] of Object.entries(map)) {
2450
+ if (stripped.includes(key)) return label;
2451
+ }
2452
+ return stripped;
2453
+ }
2454
+ function stripRulePrefixes(name) {
2455
+ let n = name.toLowerCase();
2456
+ if (n.startsWith("org:")) n = n.slice(4);
2457
+ const shieldMatch = /^shield:[^:]+:(.+)$/.exec(n);
2458
+ if (shieldMatch) n = shieldMatch[1];
2459
+ n = n.replace(/^(block|review|allow)-/, "");
2460
+ return n;
2461
+ }
2462
+ function classifyAuditEntry(entry) {
2463
+ const ruleName = entry.riskMetadata?.ruleName;
2464
+ if (typeof ruleName === "string" && ruleName.length > 0) {
2465
+ const verdict = entry.action === "AUTO_BLOCKED" || entry.action === "DENIED" ? "block" : entry.action === "APPROVED" || entry.action === "AUTO_ALLOWED" ? "allow" : "review";
2466
+ return classifyRuleSeverity(ruleName, verdict);
2467
+ }
2468
+ const cb = entry.checkedBy ?? "";
2469
+ if (cb === "dlp-block" || cb.startsWith("dlp-saas:")) return "critical";
2470
+ if (cb.startsWith("eval-saas") || cb === "pipe-chain-saas:critical") {
2471
+ return "critical";
2472
+ }
2473
+ if (cb === "loop-detected") return "medium";
2474
+ const isBlocked = entry.action === "AUTO_BLOCKED" || entry.action === "DENIED";
2475
+ if (isBlocked) return "high";
2476
+ return null;
2477
+ }
2478
+ function computeSecurityScore(opts) {
2479
+ const { critical, high, medium, total } = opts;
2480
+ if (total === 0) return { score: 100, tier: "good" };
2481
+ const criticalRate = critical / total;
2482
+ const highRate = high / total;
2483
+ const mediumRate = medium / total;
2484
+ const deduction = Math.min(criticalRate * 3e3, 60) + Math.min(highRate * 500, 30) + Math.min(mediumRate * 100, 15);
2485
+ const score = Math.max(0, Math.min(100, Math.round(100 - deduction)));
2486
+ const tier = score >= 80 ? "good" : score >= 50 ? "at-risk" : "critical";
2487
+ return { score, tier };
2488
+ }
2489
+ function classifyScanSignal(key) {
2490
+ const w = SCAN_SIGNAL_WEIGHTS[key];
2491
+ if (w >= 25) return "critical";
2492
+ if (w >= 11) return "high";
2493
+ return "medium";
2494
+ }
2495
+ function computeBlendedSecurityScore(opts) {
2496
+ const { audit, scan } = opts;
2497
+ let critical = audit.critical;
2498
+ let high = audit.high;
2499
+ let medium = audit.medium;
2500
+ let total = audit.total;
2501
+ if (scan) {
2502
+ let scanFindingSum = 0;
2503
+ for (const key of Object.keys(scan.signals)) {
2504
+ const count = scan.signals[key];
2505
+ if (count <= 0) continue;
2506
+ const tier = classifyScanSignal(key);
2507
+ if (tier === "critical") critical += count;
2508
+ else if (tier === "high") high += count;
2509
+ else medium += count;
2510
+ scanFindingSum += count;
2511
+ }
2512
+ const scanContribution = Math.max(scan.totalToolCalls ?? 0, scanFindingSum);
2513
+ total += scanContribution;
2514
+ }
2515
+ return computeSecurityScore({ critical, high, medium, total });
2516
+ }
2517
+
2518
+ // src/blast/index.ts
2519
+ function truncateBlastPath(full) {
2520
+ if (!full) return "";
2521
+ const cleaned = full.replace(/[/\\]+$/, "");
2522
+ const parts = cleaned.split(/[/\\]+/).filter((p) => p.length > 0);
2523
+ if (parts.length <= 2) {
2524
+ return cleaned.startsWith("~") && !cleaned.startsWith("~/") ? cleaned : cleaned.startsWith("~/") ? cleaned : parts.join("/");
2525
+ }
2526
+ return parts.slice(-2).join("/");
2527
+ }
2528
+ function summarizeBlast(result, opts = {}) {
2529
+ const topN = opts.topN ?? 5;
2530
+ const sorted = [...result.reachable].sort((a, b) => {
2531
+ if (b.score !== a.score) return b.score - a.score;
2532
+ return a.label.localeCompare(b.label);
2533
+ });
2534
+ return {
2535
+ score: result.score,
2536
+ exposureCount: result.reachable.length + result.envFindings.length,
2537
+ envExposureCount: result.envFindings.length,
2538
+ worstPaths: sorted.slice(0, topN).map((f) => ({
2539
+ path: truncateBlastPath(f.label),
2540
+ score: f.score
2541
+ }))
2542
+ };
2543
+ }
2544
+
2260
2545
  // src/index.ts
2261
- var ENGINE_VERSION = "1.0.0";
2546
+ var ENGINE_VERSION = "1.4.0";
2262
2547
  // Annotate the CommonJS export names for ESM import in node:
2263
2548
  0 && (module.exports = {
2264
2549
  BUILTIN_SHIELDS,
2550
+ COST_PER_LOOP_ITER_USD,
2265
2551
  DLP_PATTERNS,
2266
2552
  ENGINE_VERSION,
2267
2553
  FLAGS_WITH_VALUES,
2268
2554
  LOOP_MAX_RECORDS,
2555
+ LOOP_THRESHOLD_FOR_WASTE,
2556
+ SCAN_SIGNAL_WEIGHTS,
2269
2557
  SENSITIVE_PATH_REGEXES,
2270
2558
  analyzePipeChain,
2271
2559
  analyzeShellCommand,
2272
2560
  checkDangerousSql,
2561
+ classifyAuditEntry,
2562
+ classifyRuleSeverity,
2563
+ classifyScanSignal,
2273
2564
  computeArgsHash,
2565
+ computeBlendedSecurityScore,
2566
+ computeScanScore,
2567
+ computeSecurityScore,
2274
2568
  detectDangerousEval,
2275
2569
  detectDangerousShellExec,
2276
2570
  evaluateLoopWindow,
@@ -2285,12 +2579,16 @@ var ENGINE_VERSION = "1.0.0";
2285
2579
  isShieldVerdict,
2286
2580
  matchSensitivePath,
2287
2581
  matchesPattern,
2582
+ narrativeRuleLabel,
2288
2583
  normalizeCommandForPolicy,
2289
2584
  parseAllSshHostsFromCommand,
2290
2585
  redactText,
2291
2586
  scanArgs,
2292
2587
  scanText,
2293
2588
  sensitivePathMatch,
2589
+ summarizeBlast,
2590
+ summarizeScan,
2591
+ truncateBlastPath,
2294
2592
  validateOverrides,
2295
2593
  validateRegex,
2296
2594
  validateShieldDefinition