feat: add Attestation Chain and Triage Evidence API clients and models

- Implemented Attestation Chain API client with methods for verifying, fetching, and managing attestation chains.
- Created models for Attestation Chain, including DSSE envelope structures and verification results.
- Developed Triage Evidence API client for fetching finding evidence, including methods for evidence retrieval by CVE and component.
- Added models for Triage Evidence, encapsulating evidence responses, entry points, boundary proofs, and VEX evidence.
- Introduced mock implementations for both API clients to facilitate testing and development.
This commit is contained in:
master
2025-12-18 13:15:13 +02:00
parent 7d5250238c
commit 00d2c99af9
118 changed files with 13463 additions and 151 deletions

View File

@@ -0,0 +1,228 @@
using Microsoft.EntityFrameworkCore;
using StellaOps.Scanner.Triage.Entities;
namespace StellaOps.Scanner.Triage;
/// <summary>
/// Entity Framework Core DbContext for the Triage schema.
/// </summary>
public sealed class TriageDbContext : DbContext
{
/// <summary>
/// Initializes a new instance of the <see cref="TriageDbContext"/> class.
/// </summary>
public TriageDbContext(DbContextOptions<TriageDbContext> options)
: base(options)
{
}
/// <summary>
/// Triage findings (cases).
/// </summary>
public DbSet<TriageFinding> Findings => Set<TriageFinding>();
/// <summary>
/// Effective VEX records.
/// </summary>
public DbSet<TriageEffectiveVex> EffectiveVex => Set<TriageEffectiveVex>();
/// <summary>
/// Reachability analysis results.
/// </summary>
public DbSet<TriageReachabilityResult> ReachabilityResults => Set<TriageReachabilityResult>();
/// <summary>
/// Risk/lattice evaluation results.
/// </summary>
public DbSet<TriageRiskResult> RiskResults => Set<TriageRiskResult>();
/// <summary>
/// Triage decisions.
/// </summary>
public DbSet<TriageDecision> Decisions => Set<TriageDecision>();
/// <summary>
/// Evidence artifacts.
/// </summary>
public DbSet<TriageEvidenceArtifact> EvidenceArtifacts => Set<TriageEvidenceArtifact>();
/// <summary>
/// Snapshots for Smart-Diff.
/// </summary>
public DbSet<TriageSnapshot> Snapshots => Set<TriageSnapshot>();
/// <summary>
/// Current case view (read-only).
/// </summary>
public DbSet<TriageCaseCurrent> CurrentCases => Set<TriageCaseCurrent>();
/// <inheritdoc/>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure PostgreSQL enums
modelBuilder.HasPostgresEnum<TriageLane>("triage_lane");
modelBuilder.HasPostgresEnum<TriageVerdict>("triage_verdict");
modelBuilder.HasPostgresEnum<TriageReachability>("triage_reachability");
modelBuilder.HasPostgresEnum<TriageVexStatus>("triage_vex_status");
modelBuilder.HasPostgresEnum<TriageDecisionKind>("triage_decision_kind");
modelBuilder.HasPostgresEnum<TriageSnapshotTrigger>("triage_snapshot_trigger");
modelBuilder.HasPostgresEnum<TriageEvidenceType>("triage_evidence_type");
// Configure TriageFinding
modelBuilder.Entity<TriageFinding>(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<TriageEffectiveVex>(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<TriageReachabilityResult>(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<TriageRiskResult>(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<TriageDecision>(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<TriageEvidenceArtifact>(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<TriageSnapshot>(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<TriageCaseCurrent>(entity =>
{
entity.ToView("v_triage_case_current");
entity.HasNoKey();
});
}
}