@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 +325 -3
- package/dist/index.d.ts +325 -3
- package/dist/index.js +312 -14
- package/dist/index.mjs +299 -14
- package/package.json +2 -2
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
|
|
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.
|
|
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
|
|
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.
|
|
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 };
|