using Microsoft.EntityFrameworkCore; using StellaOps.Scanner.Triage.Entities; namespace StellaOps.Scanner.Triage; /// /// Entity Framework Core DbContext for the Triage schema. /// public sealed class TriageDbContext : DbContext { /// /// Initializes a new instance of the class. /// public TriageDbContext(DbContextOptions options) : base(options) { } /// /// Triage findings (cases). /// public DbSet Findings => Set(); /// /// Effective VEX records. /// public DbSet EffectiveVex => Set(); /// /// Reachability analysis results. /// public DbSet ReachabilityResults => Set(); /// /// Risk/lattice evaluation results. /// public DbSet RiskResults => Set(); /// /// Triage decisions. /// public DbSet Decisions => Set(); /// /// Evidence artifacts. /// public DbSet EvidenceArtifacts => Set(); /// /// Snapshots for Smart-Diff. /// public DbSet Snapshots => Set(); /// /// Current case view (read-only). /// public DbSet CurrentCases => Set(); /// protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Configure PostgreSQL enums modelBuilder.HasPostgresEnum("triage_lane"); modelBuilder.HasPostgresEnum("triage_verdict"); modelBuilder.HasPostgresEnum("triage_reachability"); modelBuilder.HasPostgresEnum("triage_vex_status"); modelBuilder.HasPostgresEnum("triage_decision_kind"); modelBuilder.HasPostgresEnum("triage_snapshot_trigger"); modelBuilder.HasPostgresEnum("triage_evidence_type"); // Configure TriageFinding modelBuilder.Entity(entity => { entity.ToTable("triage_finding"); entity.HasKey(e => e.Id); entity.HasIndex(e => e.LastSeenAt) .IsDescending() .HasDatabaseName("ix_triage_finding_last_seen"); entity.HasIndex(e => e.AssetLabel) .HasDatabaseName("ix_triage_finding_asset_label"); entity.HasIndex(e => e.Purl) .HasDatabaseName("ix_triage_finding_purl"); entity.HasIndex(e => e.CveId) .HasDatabaseName("ix_triage_finding_cve"); entity.HasIndex(e => new { e.AssetId, e.EnvironmentId, e.Purl, e.CveId, e.RuleId }) .IsUnique(); }); // Configure TriageEffectiveVex modelBuilder.Entity(entity => { entity.ToTable("triage_effective_vex"); entity.HasKey(e => e.Id); entity.HasIndex(e => new { e.FindingId, e.CollectedAt }) .IsDescending(false, true) .HasDatabaseName("ix_triage_effective_vex_finding"); entity.HasOne(e => e.Finding) .WithMany(f => f.EffectiveVexRecords) .HasForeignKey(e => e.FindingId) .OnDelete(DeleteBehavior.Cascade); }); // Configure TriageReachabilityResult modelBuilder.Entity(entity => { entity.ToTable("triage_reachability_result"); entity.HasKey(e => e.Id); entity.HasIndex(e => new { e.FindingId, e.ComputedAt }) .IsDescending(false, true) .HasDatabaseName("ix_triage_reachability_finding"); entity.HasOne(e => e.Finding) .WithMany(f => f.ReachabilityResults) .HasForeignKey(e => e.FindingId) .OnDelete(DeleteBehavior.Cascade); }); // Configure TriageRiskResult modelBuilder.Entity(entity => { entity.ToTable("triage_risk_result"); entity.HasKey(e => e.Id); entity.HasIndex(e => new { e.FindingId, e.ComputedAt }) .IsDescending(false, true) .HasDatabaseName("ix_triage_risk_finding"); entity.HasIndex(e => new { e.Lane, e.ComputedAt }) .IsDescending(false, true) .HasDatabaseName("ix_triage_risk_lane"); entity.HasIndex(e => new { e.FindingId, e.PolicyId, e.PolicyVersion, e.InputsHash }) .IsUnique(); entity.HasOne(e => e.Finding) .WithMany(f => f.RiskResults) .HasForeignKey(e => e.FindingId) .OnDelete(DeleteBehavior.Cascade); }); // Configure TriageDecision modelBuilder.Entity(entity => { entity.ToTable("triage_decision"); entity.HasKey(e => e.Id); entity.HasIndex(e => new { e.FindingId, e.CreatedAt }) .IsDescending(false, true) .HasDatabaseName("ix_triage_decision_finding"); entity.HasIndex(e => new { e.Kind, e.CreatedAt }) .IsDescending(false, true) .HasDatabaseName("ix_triage_decision_kind"); entity.HasIndex(e => e.FindingId) .HasFilter("revoked_at IS NULL") .HasDatabaseName("ix_triage_decision_active"); entity.HasOne(e => e.Finding) .WithMany(f => f.Decisions) .HasForeignKey(e => e.FindingId) .OnDelete(DeleteBehavior.Cascade); }); // Configure TriageEvidenceArtifact modelBuilder.Entity(entity => { entity.ToTable("triage_evidence_artifact"); entity.HasKey(e => e.Id); entity.HasIndex(e => new { e.FindingId, e.CreatedAt }) .IsDescending(false, true) .HasDatabaseName("ix_triage_evidence_finding"); entity.HasIndex(e => new { e.Type, e.CreatedAt }) .IsDescending(false, true) .HasDatabaseName("ix_triage_evidence_type"); entity.HasIndex(e => new { e.FindingId, e.Type, e.ContentHash }) .IsUnique(); entity.HasOne(e => e.Finding) .WithMany(f => f.EvidenceArtifacts) .HasForeignKey(e => e.FindingId) .OnDelete(DeleteBehavior.Cascade); }); // Configure TriageSnapshot modelBuilder.Entity(entity => { entity.ToTable("triage_snapshot"); entity.HasKey(e => e.Id); entity.HasIndex(e => new { e.FindingId, e.CreatedAt }) .IsDescending(false, true) .HasDatabaseName("ix_triage_snapshot_finding"); entity.HasIndex(e => new { e.Trigger, e.CreatedAt }) .IsDescending(false, true) .HasDatabaseName("ix_triage_snapshot_trigger"); entity.HasIndex(e => new { e.FindingId, e.ToInputsHash, e.CreatedAt }) .IsUnique(); entity.HasOne(e => e.Finding) .WithMany(f => f.Snapshots) .HasForeignKey(e => e.FindingId) .OnDelete(DeleteBehavior.Cascade); }); // Configure the read-only view modelBuilder.Entity(entity => { entity.ToView("v_triage_case_current"); entity.HasNoKey(); }); } }