@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.d.mts CHANGED
@@ -313,7 +313,8 @@ declare function isIgnoredTool(toolName: string, config: PolicyConfig): boolean;
313
313
  declare function matchesPattern(text: string, patterns: string[] | string): boolean;
314
314
  /**
315
315
  * Reads `obj.a.b.c` style nested keys. Returns null when any segment is
316
- * missing or the parent isn't an object.
316
+ * missing, the parent isn't an object, or the path attempts to walk the
317
+ * prototype chain (`__proto__`, `constructor`, `prototype`).
317
318
  */
318
319
  declare function getNestedValue(obj: unknown, path: string): unknown;
319
320
  /**
@@ -409,7 +410,328 @@ interface LoopWindowEvaluation {
409
410
  */
410
411
  declare function evaluateLoopWindow(records: ToolCallRecord[], tool: string, args: unknown, threshold: number, windowMs: number, now: number): LoopWindowEvaluation;
411
412
 
413
+ /**
414
+ * One finding extracted from a JSONL delta scan. The host produces these
415
+ * per-line; the engine aggregates them into a summary. `lineIndex` is local
416
+ * to the JSONL file and not exfiltrated outside this struct — only the
417
+ * count of findings matters at the workspace level.
418
+ */
419
+ interface ScanFinding {
420
+ /** sessionId from the Claude Code JSONL line, used to bucket findings. */
421
+ sessionId: string;
422
+ /**
423
+ * What kind of finding. New extractors should add their own type here
424
+ * rather than overloading existing ones.
425
+ */
426
+ type: 'dlp' | 'pii' | 'sensitive-file-read' | 'privilege-escalation' | 'network-exfil' | 'pipe-to-shell' | 'eval-of-remote' | 'destructive-op' | 'loop' | 'long-output-redacted';
427
+ /** DLP / PII pattern that matched, e.g. "GitHub Token" or "Email". */
428
+ patternName?: string;
429
+ /** Local line index within the source JSONL — never exfiltrated. */
430
+ lineIndex: number;
431
+ }
432
+ /**
433
+ * Per-signal counts. Adding a new signal extractor means adding a new key
434
+ * here; the FE will render it from this dict without code changes once
435
+ * the chart is wired up.
436
+ */
437
+ interface ScanSignals {
438
+ dlpFindings: number;
439
+ piiFindings: number;
440
+ sensitiveFileReads: number;
441
+ privilegeEscalation: number;
442
+ networkExfil: number;
443
+ pipeToShell: number;
444
+ evalOfRemote: number;
445
+ destructiveOps: number;
446
+ loops: number;
447
+ longOutputRedactions: number;
448
+ }
449
+ /**
450
+ * Compact, network-safe summary of a scan delta. This is the shape the
451
+ * proxy sends to the SaaS on every policy-sync tick. The SaaS persists it
452
+ * per-machine (1:1 with apiKey) and aggregates across the workspace for
453
+ * the dashboard's Recent Exposure card.
454
+ *
455
+ * `score` follows the same 0-100 scale as blast: higher is cleaner. We
456
+ * deduct per finding type based on severity weights (see `computeScanScore`
457
+ * below), capped so a noisy session doesn't bottom out the score on its own.
458
+ */
459
+ interface ScanSummary {
460
+ /** Number of distinct sessionIds touched by this scan delta. */
461
+ totalSessions: number;
462
+ /** Total tool-call lines parsed across all deltas. */
463
+ totalToolCalls: number;
464
+ /** Per-signal counts. */
465
+ signals: ScanSignals;
466
+ /**
467
+ * Top DLP/PII pattern names by count, descending. Truncated to topN to
468
+ * keep payload small. Only pattern *names*; samples never surface here.
469
+ */
470
+ topPatterns: Array<{
471
+ patternName: string;
472
+ count: number;
473
+ }>;
474
+ /** 0-100 cleanliness score. */
475
+ score: number;
476
+ }
477
+ /**
478
+ * Per-finding-type score deduction. Tuned so:
479
+ * - One credential leak (-30) drops the score from 100 to 70 — at-risk
480
+ * territory, demands attention.
481
+ * - One destructive op (-15) is a yellow flag.
482
+ * - One loop (-3) is mild noise; many loops still add up.
483
+ * Total deduction is capped at 100 so the score never goes negative.
484
+ *
485
+ * Exported so the SaaS Report can reuse the same severity ladder when
486
+ * blending scan signals into the workspace risk score (see
487
+ * `classifyScanSignal` in ../severity).
488
+ */
489
+ declare const SCAN_SIGNAL_WEIGHTS: Record<keyof ScanSignals, number>;
490
+ /**
491
+ * Compute the 0-100 cleanliness score. Public so other engine consumers
492
+ * can use the same weights without round-tripping through summarizeScan.
493
+ */
494
+ declare function computeScanScore(signals: ScanSignals): number;
495
+ declare const LOOP_THRESHOLD_FOR_WASTE = 3;
496
+ declare const COST_PER_LOOP_ITER_USD = 0.006;
497
+ /**
498
+ * Build the network-safe summary from a list of findings + total tool-call
499
+ * count. Deterministic: given the same input the output is identical
500
+ * (important for SaaS-side dedup and ETag-style caching of subsequent
501
+ * tick payloads).
502
+ *
503
+ * Top patterns are sorted by count desc, then alphabetically for stable
504
+ * ordering across calls. topN defaults to 10.
505
+ */
506
+ declare function summarizeScan(findings: ScanFinding[], opts?: {
507
+ totalToolCalls?: number;
508
+ topN?: number;
509
+ }): ScanSummary;
510
+
511
+ type Severity = 'critical' | 'high' | 'medium';
512
+ type ScoreTier = 'good' | 'at-risk' | 'critical';
513
+ /**
514
+ * Classify a rule by its name + verdict. Used by the proxy when scanning a
515
+ * Claude Code session — the rule that matched is known by name.
516
+ *
517
+ * Tiers:
518
+ * - critical: irreversible damage or credential exfiltration
519
+ * (rm -rf $HOME, eval-of-remote, AWS/SSH/GCP credential reads,
520
+ * repo deletion, helm uninstall, drop-table, drop-database, flushall,
521
+ * curl | bash, pipe-shell)
522
+ * - high: significant damage, recoverable
523
+ * (force push, git reset --hard, rebase, branch deletion, all other
524
+ * block-verdict rules)
525
+ * - medium: workflow / cost risk, not security
526
+ * (rm review, sudo review, redis config-set, dynamic eval, all other
527
+ * review-verdict rules)
528
+ */
529
+ declare function classifyRuleSeverity(name: string, verdict: 'block' | 'review' | 'allow'): Severity;
530
+ /**
531
+ * Map a rule slug to a friendly label suitable for narrative output.
532
+ *
533
+ * "block-read-aws" → "AWS credentials read"
534
+ * "shield:k8s:block-helm-uninstall" → "helm uninstall"
535
+ * "review-force-push" → "force pushes"
536
+ *
537
+ * Strips common prefixes (block-, review-, allow-, shield:..., org:) before
538
+ * matching, so cloud-tagged rules ("org:block-read-aws") map the same way.
539
+ */
540
+ declare function narrativeRuleLabel(name: string): string;
541
+ /**
542
+ * Audit-log entry for backend classification. Mirrors the relevant subset of
543
+ * AuditLog rows so backend code can pass them in without a Prisma dependency
544
+ * here.
545
+ */
546
+ interface AuditEntryForClassify {
547
+ checkedBy?: string | null;
548
+ toolName: string;
549
+ action: string;
550
+ riskMetadata?: {
551
+ ruleName?: string;
552
+ dlpPattern?: string;
553
+ [k: string]: unknown;
554
+ } | null;
555
+ }
556
+ /**
557
+ * Classify a single audit-log entry by what fired and which tool ran. Used by
558
+ * the SaaS /report endpoint to bucket audit events into severity tiers.
559
+ *
560
+ * Resolution order — first hit wins:
561
+ * 1. riskMetadata.ruleName → defer to classifyRuleSeverity (best signal)
562
+ * 2. checkedBy === 'dlp-block' or starts with 'dlp-saas:' → critical
563
+ * (any credential leak is critical regardless of which pattern matched)
564
+ * 3. checkedBy starts with 'eval-saas' or 'pipe-chain-saas:critical' → critical
565
+ * 4. checkedBy === 'loop-detected' → medium (cost / workflow, not security)
566
+ * 5. Block-status entries with no rule name → high (default for unattributed
567
+ * blocks; better than dropping the signal)
568
+ * 6. Otherwise → null (allowed actions don't have a severity)
569
+ */
570
+ declare function classifyAuditEntry(entry: AuditEntryForClassify): Severity | null;
571
+ /**
572
+ * Compute a 0-100 risk-posture score from severity counts + total events.
573
+ *
574
+ * Heuristic: each severity tier has a "cost" against a clean 100 score.
575
+ * Critical findings deduct the most, medium the least. Counts are normalised
576
+ * by total events so a workspace with 1 critical out of 10 events scores
577
+ * worse than one with 1 critical out of 10,000 — exposure rate matters more
578
+ * than absolute count.
579
+ *
580
+ * Tiers:
581
+ * - good : score ≥ 80
582
+ * - at-risk : 50 ≤ score < 80
583
+ * - critical : score < 50
584
+ *
585
+ * Empty workspaces (total === 0) score 100/good — no evidence of exposure
586
+ * is the only honest answer.
587
+ */
588
+ declare function computeSecurityScore(opts: {
589
+ critical: number;
590
+ high: number;
591
+ medium: number;
592
+ total: number;
593
+ }): {
594
+ score: number;
595
+ tier: ScoreTier;
596
+ };
597
+ /**
598
+ * Map a ScanSignals key to its severity tier. Uses the existing
599
+ * SCAN_SIGNAL_WEIGHTS so adding a new scan signal type only requires
600
+ * updating the weights table; classification follows automatically.
601
+ *
602
+ * Thresholds:
603
+ * - ≥ 25 → critical (dlp, pipeToShell, evalOfRemote, networkExfil)
604
+ * - ≥ 11 → high (sensitiveFileReads, privilegeEscalation,
605
+ * destructiveOps)
606
+ * - else → medium (piiFindings, loops, longOutputRedactions)
607
+ */
608
+ declare function classifyScanSignal(key: keyof ScanSignals): Severity;
609
+ /**
610
+ * Compute a 0-100 risk-posture score that blends audit-log severity counts
611
+ * with forward-only scan signal counts.
612
+ *
613
+ * Why this exists: the live audit log answers "what did the firewall block
614
+ * in this window?" and the scan answers "what's sitting in past sessions?".
615
+ * Both are real risk; surfacing them as two separate scores forced users
616
+ * to reconcile two numbers. This function bins scan signals into the same
617
+ * critical/high/medium buckets via classifyScanSignal, sums them with the
618
+ * audit counts, and runs the existing computeSecurityScore math.
619
+ *
620
+ * Denominator handling: a workspace with zero audit traffic but non-zero
621
+ * scan findings would otherwise hit the `total === 0` short-circuit and
622
+ * return 100/good — a false-healthy reading. We add the scan contribution
623
+ * to `total` so the rate-based math runs:
624
+ *
625
+ * - If `scan.totalToolCalls` is provided, use it as the scan-side
626
+ * denominator (best signal — "1 finding per 10000 calls" should
627
+ * score better than "1 per 10").
628
+ * - Otherwise fall back to the count of scan findings, so a scan-only
629
+ * workspace with one credential leak resolves to 1/1 = 100% bad
630
+ * rate and lands in critical, not 0/0 = 100/good.
631
+ *
632
+ * Backwards compatible: calling with `audit` only and no `scan` produces
633
+ * the exact same result as `computeSecurityScore(audit)`.
634
+ */
635
+ declare function computeBlendedSecurityScore(opts: {
636
+ audit: {
637
+ critical: number;
638
+ high: number;
639
+ medium: number;
640
+ total: number;
641
+ };
642
+ scan?: {
643
+ signals: ScanSignals;
644
+ totalToolCalls?: number;
645
+ };
646
+ }): {
647
+ score: number;
648
+ tier: ScoreTier;
649
+ };
650
+
651
+ /**
652
+ * One sensitive path that the blast walker found readable on disk.
653
+ * `score` is the per-finding deduction this path contributes to the
654
+ * machine's overall blast-radius score (100 = clean).
655
+ */
656
+ interface BlastFinding {
657
+ /** Absolute path on disk. May be home-relative ("~/.aws/credentials"). */
658
+ full: string;
659
+ /** Display label — short form for UI ("~/.ssh/id_rsa", ".env (cwd)"). */
660
+ label: string;
661
+ /** One-line explanation of why this path matters. */
662
+ description: string;
663
+ /** Points deducted from the 100-point score when this path is reachable. */
664
+ score: number;
665
+ }
666
+ /** One environment variable the DLP scanner flagged as a credential. */
667
+ interface BlastEnvFinding {
668
+ /** Variable name, e.g. "AWS_SECRET_ACCESS_KEY". */
669
+ key: string;
670
+ /** DLP pattern that matched, e.g. "AWS Access Key". */
671
+ patternName: string;
672
+ }
673
+ /** Full result of a blast walk on one machine. */
674
+ interface BlastResult {
675
+ reachable: BlastFinding[];
676
+ envFindings: BlastEnvFinding[];
677
+ /** 0-100. Higher is better. */
678
+ score: number;
679
+ }
680
+ /**
681
+ * Compact, network-safe summary of a blast result. This is the shape the
682
+ * proxy sends to the SaaS and the SaaS persists per machine. We deliberately
683
+ * DO NOT send file contents, full paths, or sample values — only:
684
+ * - the score (already aggregate)
685
+ * - a count of how many things were exposed
686
+ * - the top-N worst paths' sanitised labels (truncated to 2 segments)
687
+ *
688
+ * The sanitisation step lives here in the engine so both the proxy (before
689
+ * send) and the SaaS (when validating) reference identical logic.
690
+ */
691
+ interface BlastSummary {
692
+ /** 0-100. Same as BlastResult.score. */
693
+ score: number;
694
+ /** reachable.length + envFindings.length — total exposure count. */
695
+ exposureCount: number;
696
+ /**
697
+ * Top-N worst findings (sorted by individual score deduction desc).
698
+ * Paths are truncated to the last 2 segments so we never exfiltrate
699
+ * project-layout details ("payments-prod/.env.production") — only the
700
+ * basename + parent ("payments-prod/.env.production" → ".env.production"
701
+ * if 1-segment, "payments-prod/.env.production" if 2-segment).
702
+ */
703
+ worstPaths: Array<{
704
+ path: string;
705
+ score: number;
706
+ }>;
707
+ /** Number of env vars flagged as credentials. No keys included. */
708
+ envExposureCount: number;
709
+ }
710
+ /**
711
+ * Sanitise a sensitive path for transmission. Keeps only the trailing 2
712
+ * segments — enough to identify the kind of file ("~/.aws/credentials"
713
+ * stays useful, "/Users/alice/Code/payments-prod/.env" becomes
714
+ * "payments-prod/.env" which doesn't reveal the home dir or directory tree).
715
+ *
716
+ * Edge cases:
717
+ * - Already short paths (≤2 segments) are returned as-is.
718
+ * - Paths with a leading "~" are kept as-is up to 2 segments.
719
+ * - Empty strings return "".
720
+ *
721
+ * Exported for unit tests + reuse anywhere a path needs the same treatment.
722
+ */
723
+ declare function truncateBlastPath(full: string): string;
724
+ /**
725
+ * Build the network-safe summary from a full BlastResult. Deterministic:
726
+ * given the same input the output is identical (important for caching /
727
+ * deduplication on the SaaS side). Top-N defaults to 5, configurable for
728
+ * tests.
729
+ */
730
+ declare function summarizeBlast(result: BlastResult, opts?: {
731
+ topN?: number;
732
+ }): BlastSummary;
733
+
412
734
  /** Engine version stamped on audit entries for future drift detection. */
413
- declare const ENGINE_VERSION = "1.0.0";
735
+ declare const ENGINE_VERSION = "1.4.0";
414
736
 
415
- export { BUILTIN_SHIELDS, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, FLAGS_WITH_VALUES, LOOP_MAX_RECORDS, type LoopWindowEvaluation, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, type RiskMetadata, SENSITIVE_PATH_REGEXES, type ShellCommandAnalysis, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallRecord, analyzePipeChain, analyzeShellCommand, checkDangerousSql, computeArgsHash, detectDangerousEval, detectDangerousShellExec, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractNetworkTargets, extractPositionalArgs, getCompiledRegex, getNestedValue, isIgnoredTool, isShieldVerdict, matchSensitivePath, matchesPattern, normalizeCommandForPolicy, parseAllSshHostsFromCommand, redactText, scanArgs, scanText, sensitivePathMatch, validateOverrides, validateRegex, validateShieldDefinition };
737
+ export { type AuditEntryForClassify, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, COST_PER_LOOP_ITER_USD, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, FLAGS_WITH_VALUES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_REGEXES, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type Severity, type ShellCommandAnalysis, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallRecord, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, detectDangerousEval, detectDangerousShellExec, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractNetworkTargets, extractPositionalArgs, getCompiledRegex, getNestedValue, isIgnoredTool, isShieldVerdict, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, parseAllSshHostsFromCommand, redactText, scanArgs, scanText, sensitivePathMatch, summarizeBlast, summarizeScan, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
package/dist/index.d.ts CHANGED
@@ -313,7 +313,8 @@ declare function isIgnoredTool(toolName: string, config: PolicyConfig): boolean;
313
313
  declare function matchesPattern(text: string, patterns: string[] | string): boolean;
314
314
  /**
315
315
  * Reads `obj.a.b.c` style nested keys. Returns null when any segment is
316
- * missing or the parent isn't an object.
316
+ * missing, the parent isn't an object, or the path attempts to walk the
317
+ * prototype chain (`__proto__`, `constructor`, `prototype`).
317
318
  */
318
319
  declare function getNestedValue(obj: unknown, path: string): unknown;
319
320
  /**
@@ -409,7 +410,328 @@ interface LoopWindowEvaluation {
409
410
  */
410
411
  declare function evaluateLoopWindow(records: ToolCallRecord[], tool: string, args: unknown, threshold: number, windowMs: number, now: number): LoopWindowEvaluation;
411
412
 
413
+ /**
414
+ * One finding extracted from a JSONL delta scan. The host produces these
415
+ * per-line; the engine aggregates them into a summary. `lineIndex` is local
416
+ * to the JSONL file and not exfiltrated outside this struct — only the
417
+ * count of findings matters at the workspace level.
418
+ */
419
+ interface ScanFinding {
420
+ /** sessionId from the Claude Code JSONL line, used to bucket findings. */
421
+ sessionId: string;
422
+ /**
423
+ * What kind of finding. New extractors should add their own type here
424
+ * rather than overloading existing ones.
425
+ */
426
+ type: 'dlp' | 'pii' | 'sensitive-file-read' | 'privilege-escalation' | 'network-exfil' | 'pipe-to-shell' | 'eval-of-remote' | 'destructive-op' | 'loop' | 'long-output-redacted';
427
+ /** DLP / PII pattern that matched, e.g. "GitHub Token" or "Email". */
428
+ patternName?: string;
429
+ /** Local line index within the source JSONL — never exfiltrated. */
430
+ lineIndex: number;
431
+ }
432
+ /**
433
+ * Per-signal counts. Adding a new signal extractor means adding a new key
434
+ * here; the FE will render it from this dict without code changes once
435
+ * the chart is wired up.
436
+ */
437
+ interface ScanSignals {
438
+ dlpFindings: number;
439
+ piiFindings: number;
440
+ sensitiveFileReads: number;
441
+ privilegeEscalation: number;
442
+ networkExfil: number;
443
+ pipeToShell: number;
444
+ evalOfRemote: number;
445
+ destructiveOps: number;
446
+ loops: number;
447
+ longOutputRedactions: number;
448
+ }
449
+ /**
450
+ * Compact, network-safe summary of a scan delta. This is the shape the
451
+ * proxy sends to the SaaS on every policy-sync tick. The SaaS persists it
452
+ * per-machine (1:1 with apiKey) and aggregates across the workspace for
453
+ * the dashboard's Recent Exposure card.
454
+ *
455
+ * `score` follows the same 0-100 scale as blast: higher is cleaner. We
456
+ * deduct per finding type based on severity weights (see `computeScanScore`
457
+ * below), capped so a noisy session doesn't bottom out the score on its own.
458
+ */
459
+ interface ScanSummary {
460
+ /** Number of distinct sessionIds touched by this scan delta. */
461
+ totalSessions: number;
462
+ /** Total tool-call lines parsed across all deltas. */
463
+ totalToolCalls: number;
464
+ /** Per-signal counts. */
465
+ signals: ScanSignals;
466
+ /**
467
+ * Top DLP/PII pattern names by count, descending. Truncated to topN to
468
+ * keep payload small. Only pattern *names*; samples never surface here.
469
+ */
470
+ topPatterns: Array<{
471
+ patternName: string;
472
+ count: number;
473
+ }>;
474
+ /** 0-100 cleanliness score. */
475
+ score: number;
476
+ }
477
+ /**
478
+ * Per-finding-type score deduction. Tuned so:
479
+ * - One credential leak (-30) drops the score from 100 to 70 — at-risk
480
+ * territory, demands attention.
481
+ * - One destructive op (-15) is a yellow flag.
482
+ * - One loop (-3) is mild noise; many loops still add up.
483
+ * Total deduction is capped at 100 so the score never goes negative.
484
+ *
485
+ * Exported so the SaaS Report can reuse the same severity ladder when
486
+ * blending scan signals into the workspace risk score (see
487
+ * `classifyScanSignal` in ../severity).
488
+ */
489
+ declare const SCAN_SIGNAL_WEIGHTS: Record<keyof ScanSignals, number>;
490
+ /**
491
+ * Compute the 0-100 cleanliness score. Public so other engine consumers
492
+ * can use the same weights without round-tripping through summarizeScan.
493
+ */
494
+ declare function computeScanScore(signals: ScanSignals): number;
495
+ declare const LOOP_THRESHOLD_FOR_WASTE = 3;
496
+ declare const COST_PER_LOOP_ITER_USD = 0.006;
497
+ /**
498
+ * Build the network-safe summary from a list of findings + total tool-call
499
+ * count. Deterministic: given the same input the output is identical
500
+ * (important for SaaS-side dedup and ETag-style caching of subsequent
501
+ * tick payloads).
502
+ *
503
+ * Top patterns are sorted by count desc, then alphabetically for stable
504
+ * ordering across calls. topN defaults to 10.
505
+ */
506
+ declare function summarizeScan(findings: ScanFinding[], opts?: {
507
+ totalToolCalls?: number;
508
+ topN?: number;
509
+ }): ScanSummary;
510
+
511
+ type Severity = 'critical' | 'high' | 'medium';
512
+ type ScoreTier = 'good' | 'at-risk' | 'critical';
513
+ /**
514
+ * Classify a rule by its name + verdict. Used by the proxy when scanning a
515
+ * Claude Code session — the rule that matched is known by name.
516
+ *
517
+ * Tiers:
518
+ * - critical: irreversible damage or credential exfiltration
519
+ * (rm -rf $HOME, eval-of-remote, AWS/SSH/GCP credential reads,
520
+ * repo deletion, helm uninstall, drop-table, drop-database, flushall,
521
+ * curl | bash, pipe-shell)
522
+ * - high: significant damage, recoverable
523
+ * (force push, git reset --hard, rebase, branch deletion, all other
524
+ * block-verdict rules)
525
+ * - medium: workflow / cost risk, not security
526
+ * (rm review, sudo review, redis config-set, dynamic eval, all other
527
+ * review-verdict rules)
528
+ */
529
+ declare function classifyRuleSeverity(name: string, verdict: 'block' | 'review' | 'allow'): Severity;
530
+ /**
531
+ * Map a rule slug to a friendly label suitable for narrative output.
532
+ *
533
+ * "block-read-aws" → "AWS credentials read"
534
+ * "shield:k8s:block-helm-uninstall" → "helm uninstall"
535
+ * "review-force-push" → "force pushes"
536
+ *
537
+ * Strips common prefixes (block-, review-, allow-, shield:..., org:) before
538
+ * matching, so cloud-tagged rules ("org:block-read-aws") map the same way.
539
+ */
540
+ declare function narrativeRuleLabel(name: string): string;
541
+ /**
542
+ * Audit-log entry for backend classification. Mirrors the relevant subset of
543
+ * AuditLog rows so backend code can pass them in without a Prisma dependency
544
+ * here.
545
+ */
546
+ interface AuditEntryForClassify {
547
+ checkedBy?: string | null;
548
+ toolName: string;
549
+ action: string;
550
+ riskMetadata?: {
551
+ ruleName?: string;
552
+ dlpPattern?: string;
553
+ [k: string]: unknown;
554
+ } | null;
555
+ }
556
+ /**
557
+ * Classify a single audit-log entry by what fired and which tool ran. Used by
558
+ * the SaaS /report endpoint to bucket audit events into severity tiers.
559
+ *
560
+ * Resolution order — first hit wins:
561
+ * 1. riskMetadata.ruleName → defer to classifyRuleSeverity (best signal)
562
+ * 2. checkedBy === 'dlp-block' or starts with 'dlp-saas:' → critical
563
+ * (any credential leak is critical regardless of which pattern matched)
564
+ * 3. checkedBy starts with 'eval-saas' or 'pipe-chain-saas:critical' → critical
565
+ * 4. checkedBy === 'loop-detected' → medium (cost / workflow, not security)
566
+ * 5. Block-status entries with no rule name → high (default for unattributed
567
+ * blocks; better than dropping the signal)
568
+ * 6. Otherwise → null (allowed actions don't have a severity)
569
+ */
570
+ declare function classifyAuditEntry(entry: AuditEntryForClassify): Severity | null;
571
+ /**
572
+ * Compute a 0-100 risk-posture score from severity counts + total events.
573
+ *
574
+ * Heuristic: each severity tier has a "cost" against a clean 100 score.
575
+ * Critical findings deduct the most, medium the least. Counts are normalised
576
+ * by total events so a workspace with 1 critical out of 10 events scores
577
+ * worse than one with 1 critical out of 10,000 — exposure rate matters more
578
+ * than absolute count.
579
+ *
580
+ * Tiers:
581
+ * - good : score ≥ 80
582
+ * - at-risk : 50 ≤ score < 80
583
+ * - critical : score < 50
584
+ *
585
+ * Empty workspaces (total === 0) score 100/good — no evidence of exposure
586
+ * is the only honest answer.
587
+ */
588
+ declare function computeSecurityScore(opts: {
589
+ critical: number;
590
+ high: number;
591
+ medium: number;
592
+ total: number;
593
+ }): {
594
+ score: number;
595
+ tier: ScoreTier;
596
+ };
597
+ /**
598
+ * Map a ScanSignals key to its severity tier. Uses the existing
599
+ * SCAN_SIGNAL_WEIGHTS so adding a new scan signal type only requires
600
+ * updating the weights table; classification follows automatically.
601
+ *
602
+ * Thresholds:
603
+ * - ≥ 25 → critical (dlp, pipeToShell, evalOfRemote, networkExfil)
604
+ * - ≥ 11 → high (sensitiveFileReads, privilegeEscalation,
605
+ * destructiveOps)
606
+ * - else → medium (piiFindings, loops, longOutputRedactions)
607
+ */
608
+ declare function classifyScanSignal(key: keyof ScanSignals): Severity;
609
+ /**
610
+ * Compute a 0-100 risk-posture score that blends audit-log severity counts
611
+ * with forward-only scan signal counts.
612
+ *
613
+ * Why this exists: the live audit log answers "what did the firewall block
614
+ * in this window?" and the scan answers "what's sitting in past sessions?".
615
+ * Both are real risk; surfacing them as two separate scores forced users
616
+ * to reconcile two numbers. This function bins scan signals into the same
617
+ * critical/high/medium buckets via classifyScanSignal, sums them with the
618
+ * audit counts, and runs the existing computeSecurityScore math.
619
+ *
620
+ * Denominator handling: a workspace with zero audit traffic but non-zero
621
+ * scan findings would otherwise hit the `total === 0` short-circuit and
622
+ * return 100/good — a false-healthy reading. We add the scan contribution
623
+ * to `total` so the rate-based math runs:
624
+ *
625
+ * - If `scan.totalToolCalls` is provided, use it as the scan-side
626
+ * denominator (best signal — "1 finding per 10000 calls" should
627
+ * score better than "1 per 10").
628
+ * - Otherwise fall back to the count of scan findings, so a scan-only
629
+ * workspace with one credential leak resolves to 1/1 = 100% bad
630
+ * rate and lands in critical, not 0/0 = 100/good.
631
+ *
632
+ * Backwards compatible: calling with `audit` only and no `scan` produces
633
+ * the exact same result as `computeSecurityScore(audit)`.
634
+ */
635
+ declare function computeBlendedSecurityScore(opts: {
636
+ audit: {
637
+ critical: number;
638
+ high: number;
639
+ medium: number;
640
+ total: number;
641
+ };
642
+ scan?: {
643
+ signals: ScanSignals;
644
+ totalToolCalls?: number;
645
+ };
646
+ }): {
647
+ score: number;
648
+ tier: ScoreTier;
649
+ };
650
+
651
+ /**
652
+ * One sensitive path that the blast walker found readable on disk.
653
+ * `score` is the per-finding deduction this path contributes to the
654
+ * machine's overall blast-radius score (100 = clean).
655
+ */
656
+ interface BlastFinding {
657
+ /** Absolute path on disk. May be home-relative ("~/.aws/credentials"). */
658
+ full: string;
659
+ /** Display label — short form for UI ("~/.ssh/id_rsa", ".env (cwd)"). */
660
+ label: string;
661
+ /** One-line explanation of why this path matters. */
662
+ description: string;
663
+ /** Points deducted from the 100-point score when this path is reachable. */
664
+ score: number;
665
+ }
666
+ /** One environment variable the DLP scanner flagged as a credential. */
667
+ interface BlastEnvFinding {
668
+ /** Variable name, e.g. "AWS_SECRET_ACCESS_KEY". */
669
+ key: string;
670
+ /** DLP pattern that matched, e.g. "AWS Access Key". */
671
+ patternName: string;
672
+ }
673
+ /** Full result of a blast walk on one machine. */
674
+ interface BlastResult {
675
+ reachable: BlastFinding[];
676
+ envFindings: BlastEnvFinding[];
677
+ /** 0-100. Higher is better. */
678
+ score: number;
679
+ }
680
+ /**
681
+ * Compact, network-safe summary of a blast result. This is the shape the
682
+ * proxy sends to the SaaS and the SaaS persists per machine. We deliberately
683
+ * DO NOT send file contents, full paths, or sample values — only:
684
+ * - the score (already aggregate)
685
+ * - a count of how many things were exposed
686
+ * - the top-N worst paths' sanitised labels (truncated to 2 segments)
687
+ *
688
+ * The sanitisation step lives here in the engine so both the proxy (before
689
+ * send) and the SaaS (when validating) reference identical logic.
690
+ */
691
+ interface BlastSummary {
692
+ /** 0-100. Same as BlastResult.score. */
693
+ score: number;
694
+ /** reachable.length + envFindings.length — total exposure count. */
695
+ exposureCount: number;
696
+ /**
697
+ * Top-N worst findings (sorted by individual score deduction desc).
698
+ * Paths are truncated to the last 2 segments so we never exfiltrate
699
+ * project-layout details ("payments-prod/.env.production") — only the
700
+ * basename + parent ("payments-prod/.env.production" → ".env.production"
701
+ * if 1-segment, "payments-prod/.env.production" if 2-segment).
702
+ */
703
+ worstPaths: Array<{
704
+ path: string;
705
+ score: number;
706
+ }>;
707
+ /** Number of env vars flagged as credentials. No keys included. */
708
+ envExposureCount: number;
709
+ }
710
+ /**
711
+ * Sanitise a sensitive path for transmission. Keeps only the trailing 2
712
+ * segments — enough to identify the kind of file ("~/.aws/credentials"
713
+ * stays useful, "/Users/alice/Code/payments-prod/.env" becomes
714
+ * "payments-prod/.env" which doesn't reveal the home dir or directory tree).
715
+ *
716
+ * Edge cases:
717
+ * - Already short paths (≤2 segments) are returned as-is.
718
+ * - Paths with a leading "~" are kept as-is up to 2 segments.
719
+ * - Empty strings return "".
720
+ *
721
+ * Exported for unit tests + reuse anywhere a path needs the same treatment.
722
+ */
723
+ declare function truncateBlastPath(full: string): string;
724
+ /**
725
+ * Build the network-safe summary from a full BlastResult. Deterministic:
726
+ * given the same input the output is identical (important for caching /
727
+ * deduplication on the SaaS side). Top-N defaults to 5, configurable for
728
+ * tests.
729
+ */
730
+ declare function summarizeBlast(result: BlastResult, opts?: {
731
+ topN?: number;
732
+ }): BlastSummary;
733
+
412
734
  /** Engine version stamped on audit entries for future drift detection. */
413
- declare const ENGINE_VERSION = "1.0.0";
735
+ declare const ENGINE_VERSION = "1.4.0";
414
736
 
415
- export { BUILTIN_SHIELDS, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, FLAGS_WITH_VALUES, LOOP_MAX_RECORDS, type LoopWindowEvaluation, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, type RiskMetadata, SENSITIVE_PATH_REGEXES, type ShellCommandAnalysis, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallRecord, analyzePipeChain, analyzeShellCommand, checkDangerousSql, computeArgsHash, detectDangerousEval, detectDangerousShellExec, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractNetworkTargets, extractPositionalArgs, getCompiledRegex, getNestedValue, isIgnoredTool, isShieldVerdict, matchSensitivePath, matchesPattern, normalizeCommandForPolicy, parseAllSshHostsFromCommand, redactText, scanArgs, scanText, sensitivePathMatch, validateOverrides, validateRegex, validateShieldDefinition };
737
+ export { type AuditEntryForClassify, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, COST_PER_LOOP_ITER_USD, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, FLAGS_WITH_VALUES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_REGEXES, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type Severity, type ShellCommandAnalysis, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallRecord, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, detectDangerousEval, detectDangerousShellExec, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractNetworkTargets, extractPositionalArgs, getCompiledRegex, getNestedValue, isIgnoredTool, isShieldVerdict, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, parseAllSshHostsFromCommand, redactText, scanArgs, scanText, sensitivePathMatch, summarizeBlast, summarizeScan, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };