@node9/policy-engine 1.61.0 → 1.62.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.d.mts CHANGED
@@ -712,6 +712,13 @@ declare function computeSecurityScore(opts: {
712
712
  */
713
713
  declare function classifyScanSignal(key: keyof ScanSignals): Severity;
714
714
  /**
715
+ * @deprecated Score-1 redesign (2026-07): the Report score is now LIVE +
716
+ * POSTURE via {@link computeAgentDeviceScore}; session-scan findings moved to
717
+ * the onboarding forecast and no longer feed a "current risk" number (they
718
+ * are lifetime data — blending them inflated the score with pre-install
719
+ * history). Kept exported for npm back-compat (removal = breaking major);
720
+ * do not add new callers.
721
+ *
715
722
  * Compute a 0-100 risk-posture score that blends audit-log severity counts
716
723
  * with forward-only scan signal counts.
717
724
  *
@@ -752,6 +759,44 @@ declare function computeBlendedSecurityScore(opts: {
752
759
  score: number;
753
760
  tier: ScoreTier;
754
761
  };
762
+ /**
763
+ * The Report's "how at-risk am I RIGHT NOW?" score: a 0.5/0.5 blend of
764
+ *
765
+ * runtime = computeSecurityScore(live audit counts) — what node9 CAUGHT
766
+ * in the window (rate-based, unchanged math);
767
+ * posture = the fleet-average machine posture score — how exposed the
768
+ * machines are NOW (already includes disk/secret exposure via
769
+ * the posture scorecard's secrets check).
770
+ *
771
+ * Session-scan (lifetime) findings deliberately do NOT feed this score —
772
+ * they are pre-install history, not current risk (they surface in the
773
+ * onboarding forecast instead).
774
+ *
775
+ * Missing-data semantics (the day-1 false-100 fix):
776
+ * - no activity AND no posture → { score: null, tier: null } — the caller
777
+ * renders "not enough data", NEVER an invented 100/healthy;
778
+ * - no activity, posture known → posture IS the risk (a fresh machine that
779
+ * can read ~/.aws scores badly regardless of activity);
780
+ * - activity, no posture yet → the pure runtime score;
781
+ * - both → round(0.5·runtime + 0.5·posture).
782
+ *
783
+ * An EARNED 100 (real activity, nothing caught) blends normally — only the
784
+ * no-data 100 is suppressed. Pure; posture defensively clamped to [0,100].
785
+ */
786
+ declare function computeAgentDeviceScore(opts: {
787
+ /** LIVE audit-window severity counts — scan never feeds this score. */
788
+ audit: {
789
+ critical: number;
790
+ high: number;
791
+ medium: number;
792
+ total: number;
793
+ };
794
+ /** Fleet-average machine posture score, or null when no machine reports. */
795
+ posture: number | null;
796
+ }): {
797
+ score: number | null;
798
+ tier: ScoreTier | null;
799
+ };
755
800
 
756
801
  /**
757
802
  * One sensitive path that the blast walker found readable on disk.
@@ -1055,4 +1100,4 @@ declare function previewArgs(input: Record<string, unknown>, max: number): strin
1055
1100
  /** Engine version stamped on audit entries for future drift detection. */
1056
1101
  declare const ENGINE_VERSION = "1.4.0";
1057
1102
 
1058
- export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTRUCTIVE_OP_RE, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShieldVerdict, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, summarizeBlast, summarizeScan, toScanFinding, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
1103
+ export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTRUCTIVE_OP_RE, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShieldVerdict, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, summarizeBlast, summarizeScan, toScanFinding, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
package/dist/index.d.ts CHANGED
@@ -712,6 +712,13 @@ declare function computeSecurityScore(opts: {
712
712
  */
713
713
  declare function classifyScanSignal(key: keyof ScanSignals): Severity;
714
714
  /**
715
+ * @deprecated Score-1 redesign (2026-07): the Report score is now LIVE +
716
+ * POSTURE via {@link computeAgentDeviceScore}; session-scan findings moved to
717
+ * the onboarding forecast and no longer feed a "current risk" number (they
718
+ * are lifetime data — blending them inflated the score with pre-install
719
+ * history). Kept exported for npm back-compat (removal = breaking major);
720
+ * do not add new callers.
721
+ *
715
722
  * Compute a 0-100 risk-posture score that blends audit-log severity counts
716
723
  * with forward-only scan signal counts.
717
724
  *
@@ -752,6 +759,44 @@ declare function computeBlendedSecurityScore(opts: {
752
759
  score: number;
753
760
  tier: ScoreTier;
754
761
  };
762
+ /**
763
+ * The Report's "how at-risk am I RIGHT NOW?" score: a 0.5/0.5 blend of
764
+ *
765
+ * runtime = computeSecurityScore(live audit counts) — what node9 CAUGHT
766
+ * in the window (rate-based, unchanged math);
767
+ * posture = the fleet-average machine posture score — how exposed the
768
+ * machines are NOW (already includes disk/secret exposure via
769
+ * the posture scorecard's secrets check).
770
+ *
771
+ * Session-scan (lifetime) findings deliberately do NOT feed this score —
772
+ * they are pre-install history, not current risk (they surface in the
773
+ * onboarding forecast instead).
774
+ *
775
+ * Missing-data semantics (the day-1 false-100 fix):
776
+ * - no activity AND no posture → { score: null, tier: null } — the caller
777
+ * renders "not enough data", NEVER an invented 100/healthy;
778
+ * - no activity, posture known → posture IS the risk (a fresh machine that
779
+ * can read ~/.aws scores badly regardless of activity);
780
+ * - activity, no posture yet → the pure runtime score;
781
+ * - both → round(0.5·runtime + 0.5·posture).
782
+ *
783
+ * An EARNED 100 (real activity, nothing caught) blends normally — only the
784
+ * no-data 100 is suppressed. Pure; posture defensively clamped to [0,100].
785
+ */
786
+ declare function computeAgentDeviceScore(opts: {
787
+ /** LIVE audit-window severity counts — scan never feeds this score. */
788
+ audit: {
789
+ critical: number;
790
+ high: number;
791
+ medium: number;
792
+ total: number;
793
+ };
794
+ /** Fleet-average machine posture score, or null when no machine reports. */
795
+ posture: number | null;
796
+ }): {
797
+ score: number | null;
798
+ tier: ScoreTier | null;
799
+ };
755
800
 
756
801
  /**
757
802
  * One sensitive path that the blast walker found readable on disk.
@@ -1055,4 +1100,4 @@ declare function previewArgs(input: Record<string, unknown>, max: number): strin
1055
1100
  /** Engine version stamped on audit entries for future drift detection. */
1056
1101
  declare const ENGINE_VERSION = "1.4.0";
1057
1102
 
1058
- export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTRUCTIVE_OP_RE, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShieldVerdict, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, summarizeBlast, summarizeScan, toScanFinding, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
1103
+ export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTRUCTIVE_OP_RE, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShieldVerdict, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, summarizeBlast, summarizeScan, toScanFinding, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
package/dist/index.js CHANGED
@@ -57,6 +57,7 @@ __export(src_exports, {
57
57
  classifyAuditEntry: () => classifyAuditEntry,
58
58
  classifyRuleSeverity: () => classifyRuleSeverity,
59
59
  classifyScanSignal: () => classifyScanSignal,
60
+ computeAgentDeviceScore: () => computeAgentDeviceScore,
60
61
  computeArgsHash: () => computeArgsHash,
61
62
  computeBlendedSecurityScore: () => computeBlendedSecurityScore,
62
63
  computeScanScore: () => computeScanScore,
@@ -1140,7 +1141,7 @@ function analyzeSqlDestructive(command) {
1140
1141
  description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
1141
1142
  };
1142
1143
  }
1143
- var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
1144
+ var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
1144
1145
  var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
1145
1146
  "sudo",
1146
1147
  "doas",
@@ -1460,6 +1461,97 @@ function analyzeFsOperation(command) {
1460
1461
  fsOpCache.set(normalized, computed);
1461
1462
  return computed;
1462
1463
  }
1464
+ var stripDotSlash = (p) => p.replace(/^\.\//, "");
1465
+ function isSensitiveCleanupName(p) {
1466
+ const base = p.replace(/^.*[\\/]/, "");
1467
+ return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
1468
+ }
1469
+ function isWaivableCleanupTarget(p) {
1470
+ if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
1471
+ if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
1472
+ if (/[*?[{]/.test(p)) return false;
1473
+ if (isSensitiveCleanupName(p)) return false;
1474
+ return true;
1475
+ }
1476
+ function deriveRedirOp(sample) {
1477
+ try {
1478
+ const f = sharedParser.Parse(sample, "cmd");
1479
+ let op = -1;
1480
+ syntax.Walk(f, (node) => {
1481
+ const n = node;
1482
+ if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
1483
+ return true;
1484
+ });
1485
+ return op;
1486
+ } catch {
1487
+ return -1;
1488
+ }
1489
+ }
1490
+ var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
1491
+ var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
1492
+ deriveRedirOp("cat <<X\nX"),
1493
+ deriveRedirOp("cat <<-X\nX")
1494
+ ]);
1495
+ function collectSameCommandCreations(f) {
1496
+ const created = /* @__PURE__ */ new Set();
1497
+ try {
1498
+ const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
1499
+ for (const stmt of stmts) {
1500
+ if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
1501
+ const redirs = stmt.Redirs || [];
1502
+ if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
1503
+ for (const r of redirs) {
1504
+ if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
1505
+ const w = resolveWordLiteral(r.Word);
1506
+ if (w) created.add(stripDotSlash(w));
1507
+ }
1508
+ }
1509
+ }
1510
+ } catch {
1511
+ return created;
1512
+ }
1513
+ return created;
1514
+ }
1515
+ function isRmCreatedInCommandCleanup(command) {
1516
+ if (!/\brm\b/.test(command)) return false;
1517
+ const f = parseShared(command);
1518
+ if (f === PARSE_FAIL) return false;
1519
+ const created = collectSameCommandCreations(f);
1520
+ if (created.size === 0) return false;
1521
+ let sawRm = false;
1522
+ let ok = true;
1523
+ try {
1524
+ syntax.Walk(f, (node) => {
1525
+ if (!node || !ok) return false;
1526
+ const n = node;
1527
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1528
+ const args = n.Args || [];
1529
+ const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
1530
+ if (name !== "rm") return true;
1531
+ sawRm = true;
1532
+ const { flags, paths } = extractLiteralArgs(n);
1533
+ if (args.length - 1 > flags.length + paths.length) {
1534
+ ok = false;
1535
+ return false;
1536
+ }
1537
+ if (paths.length === 0) {
1538
+ ok = false;
1539
+ return false;
1540
+ }
1541
+ for (const p of paths) {
1542
+ const np = stripDotSlash(p);
1543
+ if (!created.has(np) || !isWaivableCleanupTarget(np)) {
1544
+ ok = false;
1545
+ return false;
1546
+ }
1547
+ }
1548
+ return true;
1549
+ });
1550
+ } catch {
1551
+ return false;
1552
+ }
1553
+ return sawRm && ok;
1554
+ }
1463
1555
  function analyzeFsOperationImpl(command) {
1464
1556
  const f = parseShared(command);
1465
1557
  if (f === PARSE_FAIL) return null;
@@ -2218,8 +2310,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2218
2310
  }
2219
2311
  }
2220
2312
  if (config.policy.smartRules.length > 0) {
2313
+ const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
2221
2314
  const matches = config.policy.smartRules.filter(
2222
- (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
2315
+ (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
2223
2316
  );
2224
2317
  const matchedRule = resolvePinned(matches);
2225
2318
  if (matchedRule) {
@@ -3456,6 +3549,17 @@ function computeBlendedSecurityScore(opts) {
3456
3549
  }
3457
3550
  return computeSecurityScore({ critical, high, medium, total });
3458
3551
  }
3552
+ function computeAgentDeviceScore(opts) {
3553
+ const { audit } = opts;
3554
+ const posture = Number.isFinite(opts.posture) ? Math.max(0, Math.min(100, opts.posture)) : null;
3555
+ const tierOf = (s) => s >= 80 ? "good" : s >= 50 ? "at-risk" : "critical";
3556
+ if (audit.total === 0 && posture === null) return { score: null, tier: null };
3557
+ if (audit.total === 0) return { score: posture, tier: tierOf(posture) };
3558
+ const runtime = computeSecurityScore(audit);
3559
+ if (posture === null) return runtime;
3560
+ const score = Math.round(0.5 * runtime.score + 0.5 * posture);
3561
+ return { score, tier: tierOf(score) };
3562
+ }
3459
3563
 
3460
3564
  // src/blast/index.ts
3461
3565
  function truncateBlastPath(full) {
@@ -3909,6 +4013,7 @@ var ENGINE_VERSION = "1.4.0";
3909
4013
  classifyAuditEntry,
3910
4014
  classifyRuleSeverity,
3911
4015
  classifyScanSignal,
4016
+ computeAgentDeviceScore,
3912
4017
  computeArgsHash,
3913
4018
  computeBlendedSecurityScore,
3914
4019
  computeScanScore,
package/dist/index.mjs CHANGED
@@ -1031,7 +1031,7 @@ function analyzeSqlDestructive(command) {
1031
1031
  description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
1032
1032
  };
1033
1033
  }
1034
- var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
1034
+ var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
1035
1035
  var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
1036
1036
  "sudo",
1037
1037
  "doas",
@@ -1351,6 +1351,97 @@ function analyzeFsOperation(command) {
1351
1351
  fsOpCache.set(normalized, computed);
1352
1352
  return computed;
1353
1353
  }
1354
+ var stripDotSlash = (p) => p.replace(/^\.\//, "");
1355
+ function isSensitiveCleanupName(p) {
1356
+ const base = p.replace(/^.*[\\/]/, "");
1357
+ return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
1358
+ }
1359
+ function isWaivableCleanupTarget(p) {
1360
+ if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
1361
+ if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
1362
+ if (/[*?[{]/.test(p)) return false;
1363
+ if (isSensitiveCleanupName(p)) return false;
1364
+ return true;
1365
+ }
1366
+ function deriveRedirOp(sample) {
1367
+ try {
1368
+ const f = sharedParser.Parse(sample, "cmd");
1369
+ let op = -1;
1370
+ syntax.Walk(f, (node) => {
1371
+ const n = node;
1372
+ if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
1373
+ return true;
1374
+ });
1375
+ return op;
1376
+ } catch {
1377
+ return -1;
1378
+ }
1379
+ }
1380
+ var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
1381
+ var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
1382
+ deriveRedirOp("cat <<X\nX"),
1383
+ deriveRedirOp("cat <<-X\nX")
1384
+ ]);
1385
+ function collectSameCommandCreations(f) {
1386
+ const created = /* @__PURE__ */ new Set();
1387
+ try {
1388
+ const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
1389
+ for (const stmt of stmts) {
1390
+ if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
1391
+ const redirs = stmt.Redirs || [];
1392
+ if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
1393
+ for (const r of redirs) {
1394
+ if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
1395
+ const w = resolveWordLiteral(r.Word);
1396
+ if (w) created.add(stripDotSlash(w));
1397
+ }
1398
+ }
1399
+ }
1400
+ } catch {
1401
+ return created;
1402
+ }
1403
+ return created;
1404
+ }
1405
+ function isRmCreatedInCommandCleanup(command) {
1406
+ if (!/\brm\b/.test(command)) return false;
1407
+ const f = parseShared(command);
1408
+ if (f === PARSE_FAIL) return false;
1409
+ const created = collectSameCommandCreations(f);
1410
+ if (created.size === 0) return false;
1411
+ let sawRm = false;
1412
+ let ok = true;
1413
+ try {
1414
+ syntax.Walk(f, (node) => {
1415
+ if (!node || !ok) return false;
1416
+ const n = node;
1417
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1418
+ const args = n.Args || [];
1419
+ const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
1420
+ if (name !== "rm") return true;
1421
+ sawRm = true;
1422
+ const { flags, paths } = extractLiteralArgs(n);
1423
+ if (args.length - 1 > flags.length + paths.length) {
1424
+ ok = false;
1425
+ return false;
1426
+ }
1427
+ if (paths.length === 0) {
1428
+ ok = false;
1429
+ return false;
1430
+ }
1431
+ for (const p of paths) {
1432
+ const np = stripDotSlash(p);
1433
+ if (!created.has(np) || !isWaivableCleanupTarget(np)) {
1434
+ ok = false;
1435
+ return false;
1436
+ }
1437
+ }
1438
+ return true;
1439
+ });
1440
+ } catch {
1441
+ return false;
1442
+ }
1443
+ return sawRm && ok;
1444
+ }
1354
1445
  function analyzeFsOperationImpl(command) {
1355
1446
  const f = parseShared(command);
1356
1447
  if (f === PARSE_FAIL) return null;
@@ -2109,8 +2200,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2109
2200
  }
2110
2201
  }
2111
2202
  if (config.policy.smartRules.length > 0) {
2203
+ const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
2112
2204
  const matches = config.policy.smartRules.filter(
2113
- (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
2205
+ (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
2114
2206
  );
2115
2207
  const matchedRule = resolvePinned(matches);
2116
2208
  if (matchedRule) {
@@ -3347,6 +3439,17 @@ function computeBlendedSecurityScore(opts) {
3347
3439
  }
3348
3440
  return computeSecurityScore({ critical, high, medium, total });
3349
3441
  }
3442
+ function computeAgentDeviceScore(opts) {
3443
+ const { audit } = opts;
3444
+ const posture = Number.isFinite(opts.posture) ? Math.max(0, Math.min(100, opts.posture)) : null;
3445
+ const tierOf = (s) => s >= 80 ? "good" : s >= 50 ? "at-risk" : "critical";
3446
+ if (audit.total === 0 && posture === null) return { score: null, tier: null };
3447
+ if (audit.total === 0) return { score: posture, tier: tierOf(posture) };
3448
+ const runtime = computeSecurityScore(audit);
3449
+ if (posture === null) return runtime;
3450
+ const score = Math.round(0.5 * runtime.score + 0.5 * posture);
3451
+ return { score, tier: tierOf(score) };
3452
+ }
3350
3453
 
3351
3454
  // src/blast/index.ts
3352
3455
  function truncateBlastPath(full) {
@@ -3799,6 +3902,7 @@ export {
3799
3902
  classifyAuditEntry,
3800
3903
  classifyRuleSeverity,
3801
3904
  classifyScanSignal,
3905
+ computeAgentDeviceScore,
3802
3906
  computeArgsHash,
3803
3907
  computeBlendedSecurityScore,
3804
3908
  computeScanScore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/policy-engine",
3
- "version": "1.61.0",
3
+ "version": "1.62.0",
4
4
  "description": "Shared policy evaluation engine for node9 — DLP, smart rules, AST shell parsing, shields, loop detection. Pure functions, no I/O. Used by both node9-proxy and the node9 SaaS firewall.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://node9.ai",