documentation cleanse, sprints work and planning. remaining non EF DAL migration to EF
This commit is contained in:
@@ -27,6 +27,27 @@ public partial class ScannerDbContext : DbContext
|
||||
public virtual DbSet<CallGraphSnapshotEntity> CallGraphSnapshots { get; set; }
|
||||
public virtual DbSet<ReachabilityResultEntity> ReachabilityResults { get; set; }
|
||||
|
||||
// ----- Reachability drift tables (scanner schema) -----
|
||||
public virtual DbSet<CodeChangeEntity> CodeChanges { get; set; }
|
||||
public virtual DbSet<ReachabilityDriftResultEntity> ReachabilityDriftResults { get; set; }
|
||||
public virtual DbSet<DriftedSinkEntity> DriftedSinks { get; set; }
|
||||
|
||||
// ----- EPSS tables (scanner schema) -----
|
||||
public virtual DbSet<EpssRawEntity> EpssRaw { get; set; }
|
||||
public virtual DbSet<EpssSignalEntity> EpssSignals { get; set; }
|
||||
public virtual DbSet<EpssSignalConfigEntity> EpssSignalConfigs { get; set; }
|
||||
|
||||
// ----- VEX candidates (scanner schema) -----
|
||||
public virtual DbSet<VexCandidateEntity> VexCandidates { get; set; }
|
||||
|
||||
// ----- Function-level proof tables (scanner schema) -----
|
||||
public virtual DbSet<FuncProofEntity> FuncProofs { get; set; }
|
||||
public virtual DbSet<FuncNodeEntity> FuncNodes { get; set; }
|
||||
public virtual DbSet<FuncTraceEntity> FuncTraces { get; set; }
|
||||
|
||||
// ----- Facet seals (scanner schema) -----
|
||||
public virtual DbSet<FacetSealEntity> FacetSeals { get; set; }
|
||||
|
||||
// ----- Public/default schema tables -----
|
||||
public virtual DbSet<ScanManifestEntity> ScanManifests { get; set; }
|
||||
public virtual DbSet<ProofBundleEntity> ProofBundles { get; set; }
|
||||
@@ -390,6 +411,336 @@ public partial class ScannerDbContext : DbContext
|
||||
entity.Property(e => e.RekorTileId).HasColumnName("rekor_tile_id");
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// Reachability drift tables (scanner schema, migration 010)
|
||||
// ======================================================================
|
||||
|
||||
modelBuilder.Entity<CodeChangeEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("code_changes", schema);
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id").HasDefaultValueSql("gen_random_uuid()");
|
||||
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||
entity.Property(e => e.ScanId).HasColumnName("scan_id");
|
||||
entity.Property(e => e.BaseScanId).HasColumnName("base_scan_id");
|
||||
entity.Property(e => e.Language).HasColumnName("language");
|
||||
entity.Property(e => e.NodeId).HasColumnName("node_id");
|
||||
entity.Property(e => e.File).HasColumnName("file");
|
||||
entity.Property(e => e.Symbol).HasColumnName("symbol");
|
||||
entity.Property(e => e.ChangeKind).HasColumnName("change_kind");
|
||||
entity.Property(e => e.Details).HasColumnName("details").HasColumnType("jsonb");
|
||||
entity.Property(e => e.DetectedAt).HasColumnName("detected_at").HasDefaultValueSql("NOW()");
|
||||
|
||||
entity.HasIndex(e => new { e.TenantId, e.ScanId, e.BaseScanId, e.Language, e.Symbol, e.ChangeKind })
|
||||
.IsUnique()
|
||||
.HasDatabaseName("code_changes_unique");
|
||||
entity.HasIndex(e => new { e.TenantId, e.ScanId, e.BaseScanId, e.Language })
|
||||
.HasDatabaseName("idx_code_changes_tenant_scan");
|
||||
entity.HasIndex(e => e.Symbol).HasDatabaseName("idx_code_changes_symbol");
|
||||
entity.HasIndex(e => e.ChangeKind).HasDatabaseName("idx_code_changes_kind");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ReachabilityDriftResultEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("reachability_drift_results", schema);
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id").HasDefaultValueSql("gen_random_uuid()");
|
||||
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||
entity.Property(e => e.BaseScanId).HasColumnName("base_scan_id");
|
||||
entity.Property(e => e.HeadScanId).HasColumnName("head_scan_id");
|
||||
entity.Property(e => e.Language).HasColumnName("language");
|
||||
entity.Property(e => e.NewlyReachableCount).HasColumnName("newly_reachable_count").HasDefaultValue(0);
|
||||
entity.Property(e => e.NewlyUnreachableCount).HasColumnName("newly_unreachable_count").HasDefaultValue(0);
|
||||
entity.Property(e => e.DetectedAt).HasColumnName("detected_at").HasDefaultValueSql("NOW()");
|
||||
entity.Property(e => e.ResultDigest).HasColumnName("result_digest");
|
||||
|
||||
entity.HasIndex(e => new { e.TenantId, e.BaseScanId, e.HeadScanId, e.Language, e.ResultDigest })
|
||||
.IsUnique()
|
||||
.HasDatabaseName("reachability_drift_unique");
|
||||
entity.HasIndex(e => new { e.TenantId, e.HeadScanId, e.Language })
|
||||
.HasDatabaseName("idx_reachability_drift_head");
|
||||
|
||||
entity.HasMany(e => e.DriftedSinks)
|
||||
.WithOne(d => d.DriftResult)
|
||||
.HasForeignKey(d => d.DriftResultId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<DriftedSinkEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("drifted_sinks", schema);
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id").HasDefaultValueSql("gen_random_uuid()");
|
||||
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||
entity.Property(e => e.DriftResultId).HasColumnName("drift_result_id");
|
||||
entity.Property(e => e.SinkNodeId).HasColumnName("sink_node_id");
|
||||
entity.Property(e => e.Symbol).HasColumnName("symbol");
|
||||
entity.Property(e => e.SinkCategory).HasColumnName("sink_category");
|
||||
entity.Property(e => e.Direction).HasColumnName("direction");
|
||||
entity.Property(e => e.CauseKind).HasColumnName("cause_kind");
|
||||
entity.Property(e => e.CauseDescription).HasColumnName("cause_description");
|
||||
entity.Property(e => e.CauseSymbol).HasColumnName("cause_symbol");
|
||||
entity.Property(e => e.CauseFile).HasColumnName("cause_file");
|
||||
entity.Property(e => e.CauseLine).HasColumnName("cause_line");
|
||||
entity.Property(e => e.CodeChangeId).HasColumnName("code_change_id");
|
||||
entity.Property(e => e.CompressedPath).HasColumnName("compressed_path").HasColumnType("jsonb");
|
||||
entity.Property(e => e.AssociatedVulns).HasColumnName("associated_vulns").HasColumnType("jsonb");
|
||||
|
||||
entity.HasIndex(e => new { e.DriftResultId, e.SinkNodeId })
|
||||
.IsUnique()
|
||||
.HasDatabaseName("drifted_sinks_unique");
|
||||
entity.HasIndex(e => e.DriftResultId).HasDatabaseName("idx_drifted_sinks_drift");
|
||||
entity.HasIndex(e => e.Direction).HasDatabaseName("idx_drifted_sinks_direction");
|
||||
entity.HasIndex(e => e.SinkCategory).HasDatabaseName("idx_drifted_sinks_category");
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// EPSS tables (scanner schema, migrations 011/012)
|
||||
// ======================================================================
|
||||
|
||||
modelBuilder.Entity<EpssRawEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("epss_raw", schema);
|
||||
entity.HasKey(e => e.RawId);
|
||||
|
||||
entity.Property(e => e.RawId).HasColumnName("raw_id").UseIdentityAlwaysColumn();
|
||||
entity.Property(e => e.SourceUri).HasColumnName("source_uri");
|
||||
entity.Property(e => e.AsOfDate).HasColumnName("asof_date").HasColumnType("date");
|
||||
entity.Property(e => e.IngestionTs).HasColumnName("ingestion_ts").HasDefaultValueSql("now()");
|
||||
entity.Property(e => e.Payload).HasColumnName("payload").HasColumnType("jsonb");
|
||||
entity.Property(e => e.PayloadSha256).HasColumnName("payload_sha256");
|
||||
entity.Property(e => e.HeaderComment).HasColumnName("header_comment");
|
||||
entity.Property(e => e.ModelVersion).HasColumnName("model_version");
|
||||
entity.Property(e => e.PublishedDate).HasColumnName("published_date").HasColumnType("date");
|
||||
entity.Property(e => e.RowCount).HasColumnName("row_count");
|
||||
entity.Property(e => e.CompressedSize).HasColumnName("compressed_size");
|
||||
entity.Property(e => e.DecompressedSize).HasColumnName("decompressed_size");
|
||||
entity.Property(e => e.ImportRunId).HasColumnName("import_run_id");
|
||||
|
||||
entity.HasIndex(e => new { e.SourceUri, e.AsOfDate, e.PayloadSha256 })
|
||||
.IsUnique()
|
||||
.HasDatabaseName("epss_raw_unique");
|
||||
entity.HasIndex(e => e.AsOfDate).IsDescending().HasDatabaseName("idx_epss_raw_asof");
|
||||
entity.HasIndex(e => e.ModelVersion).HasDatabaseName("idx_epss_raw_model");
|
||||
entity.HasIndex(e => e.ImportRunId).HasDatabaseName("idx_epss_raw_import_run");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<EpssSignalEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("epss_signal", schema);
|
||||
entity.HasKey(e => e.SignalId);
|
||||
|
||||
entity.Property(e => e.SignalId).HasColumnName("signal_id").UseIdentityAlwaysColumn();
|
||||
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||
entity.Property(e => e.ModelDate).HasColumnName("model_date").HasColumnType("date");
|
||||
entity.Property(e => e.CveId).HasColumnName("cve_id");
|
||||
entity.Property(e => e.EventType).HasColumnName("event_type");
|
||||
entity.Property(e => e.RiskBand).HasColumnName("risk_band");
|
||||
entity.Property(e => e.EpssScore).HasColumnName("epss_score");
|
||||
entity.Property(e => e.EpssDelta).HasColumnName("epss_delta");
|
||||
entity.Property(e => e.Percentile).HasColumnName("percentile");
|
||||
entity.Property(e => e.PercentileDelta).HasColumnName("percentile_delta");
|
||||
entity.Property(e => e.IsModelChange).HasColumnName("is_model_change").HasDefaultValue(false);
|
||||
entity.Property(e => e.ModelVersion).HasColumnName("model_version");
|
||||
entity.Property(e => e.DedupeKey).HasColumnName("dedupe_key");
|
||||
entity.Property(e => e.ExplainHash).HasColumnName("explain_hash");
|
||||
entity.Property(e => e.Payload).HasColumnName("payload").HasColumnType("jsonb");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("now()");
|
||||
|
||||
entity.HasIndex(e => new { e.TenantId, e.DedupeKey })
|
||||
.IsUnique()
|
||||
.HasDatabaseName("epss_signal_dedupe");
|
||||
entity.HasIndex(e => new { e.TenantId, e.ModelDate })
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("idx_epss_signal_tenant_date");
|
||||
entity.HasIndex(e => new { e.TenantId, e.CveId, e.ModelDate })
|
||||
.IsDescending(false, false, true)
|
||||
.HasDatabaseName("idx_epss_signal_tenant_cve");
|
||||
entity.HasIndex(e => new { e.TenantId, e.EventType, e.ModelDate })
|
||||
.IsDescending(false, false, true)
|
||||
.HasDatabaseName("idx_epss_signal_event_type");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<EpssSignalConfigEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("epss_signal_config", schema);
|
||||
entity.HasKey(e => e.ConfigId);
|
||||
|
||||
entity.Property(e => e.ConfigId).HasColumnName("config_id").HasDefaultValueSql("gen_random_uuid()");
|
||||
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||
entity.Property(e => e.CriticalPercentile).HasColumnName("critical_percentile").HasDefaultValue(0.995);
|
||||
entity.Property(e => e.HighPercentile).HasColumnName("high_percentile").HasDefaultValue(0.99);
|
||||
entity.Property(e => e.MediumPercentile).HasColumnName("medium_percentile").HasDefaultValue(0.90);
|
||||
entity.Property(e => e.BigJumpDelta).HasColumnName("big_jump_delta").HasDefaultValue(0.10);
|
||||
entity.Property(e => e.SuppressOnModelChange).HasColumnName("suppress_on_model_change").HasDefaultValue(true);
|
||||
entity.Property(e => e.EnabledEventTypes).HasColumnName("enabled_event_types");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("now()");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at").HasDefaultValueSql("now()");
|
||||
|
||||
entity.HasIndex(e => e.TenantId)
|
||||
.IsUnique()
|
||||
.HasDatabaseName("epss_signal_config_tenant_unique");
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// VEX candidates (scanner schema, migration 005)
|
||||
// ======================================================================
|
||||
|
||||
modelBuilder.Entity<VexCandidateEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("vex_candidates", schema);
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id").HasDefaultValueSql("gen_random_uuid()");
|
||||
entity.Property(e => e.CandidateId).HasColumnName("candidate_id");
|
||||
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
|
||||
entity.Property(e => e.VulnId).HasColumnName("vuln_id");
|
||||
entity.Property(e => e.Purl).HasColumnName("purl");
|
||||
entity.Property(e => e.ImageDigest).HasColumnName("image_digest");
|
||||
entity.Property(e => e.SuggestedStatus).HasColumnName("suggested_status");
|
||||
entity.Property(e => e.Justification).HasColumnName("justification");
|
||||
entity.Property(e => e.Rationale).HasColumnName("rationale");
|
||||
entity.Property(e => e.EvidenceLinks).HasColumnName("evidence_links").HasColumnType("jsonb");
|
||||
entity.Property(e => e.Confidence).HasColumnName("confidence").HasColumnType("numeric(4,3)");
|
||||
entity.Property(e => e.GeneratedAt).HasColumnName("generated_at").HasDefaultValueSql("NOW()");
|
||||
entity.Property(e => e.ExpiresAt).HasColumnName("expires_at");
|
||||
entity.Property(e => e.RequiresReview).HasColumnName("requires_review").HasDefaultValue(true);
|
||||
entity.Property(e => e.ReviewAction).HasColumnName("review_action");
|
||||
entity.Property(e => e.ReviewedBy).HasColumnName("reviewed_by");
|
||||
entity.Property(e => e.ReviewedAt).HasColumnName("reviewed_at");
|
||||
entity.Property(e => e.ReviewComment).HasColumnName("review_comment");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||
|
||||
entity.HasIndex(e => e.CandidateId)
|
||||
.IsUnique()
|
||||
.HasDatabaseName("vex_candidates_candidate_id_key");
|
||||
entity.HasIndex(e => new { e.TenantId, e.ImageDigest })
|
||||
.HasDatabaseName("idx_vex_candidates_tenant_image");
|
||||
entity.HasIndex(e => e.ExpiresAt).HasDatabaseName("idx_vex_candidates_expires");
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// Function-level proof tables (scanner schema, migration 019)
|
||||
// ======================================================================
|
||||
|
||||
modelBuilder.Entity<FuncProofEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("func_proof", schema);
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id").HasDefaultValueSql("gen_random_uuid()");
|
||||
entity.Property(e => e.ScanId).HasColumnName("scan_id");
|
||||
entity.Property(e => e.ProofId).HasColumnName("proof_id");
|
||||
entity.Property(e => e.BuildId).HasColumnName("build_id");
|
||||
entity.Property(e => e.BuildIdType).HasColumnName("build_id_type");
|
||||
entity.Property(e => e.FileSha256).HasColumnName("file_sha256");
|
||||
entity.Property(e => e.BinaryFormat).HasColumnName("binary_format");
|
||||
entity.Property(e => e.Architecture).HasColumnName("architecture");
|
||||
entity.Property(e => e.IsStripped).HasColumnName("is_stripped").HasDefaultValue(false);
|
||||
entity.Property(e => e.FunctionCount).HasColumnName("function_count").HasDefaultValue(0);
|
||||
entity.Property(e => e.TraceCount).HasColumnName("trace_count").HasDefaultValue(0);
|
||||
entity.Property(e => e.ProofContent).HasColumnName("proof_content").HasColumnType("jsonb");
|
||||
entity.Property(e => e.CompressedContent).HasColumnName("compressed_content");
|
||||
entity.Property(e => e.DsseEnvelopeId).HasColumnName("dsse_envelope_id");
|
||||
entity.Property(e => e.OciArtifactDigest).HasColumnName("oci_artifact_digest");
|
||||
entity.Property(e => e.RekorEntryId).HasColumnName("rekor_entry_id");
|
||||
entity.Property(e => e.GeneratorVersion).HasColumnName("generator_version");
|
||||
entity.Property(e => e.GeneratedAtUtc).HasColumnName("generated_at_utc");
|
||||
entity.Property(e => e.CreatedAtUtc).HasColumnName("created_at_utc").HasDefaultValueSql("NOW()");
|
||||
entity.Property(e => e.UpdatedAtUtc).HasColumnName("updated_at_utc");
|
||||
|
||||
entity.HasIndex(e => e.ProofId).IsUnique().HasDatabaseName("idx_func_proof_proof_id");
|
||||
entity.HasIndex(e => e.BuildId).HasDatabaseName("idx_func_proof_build_id");
|
||||
entity.HasIndex(e => e.FileSha256).HasDatabaseName("idx_func_proof_file_sha256");
|
||||
entity.HasIndex(e => e.ScanId).HasDatabaseName("idx_func_proof_scan_id");
|
||||
entity.HasIndex(e => new { e.BuildId, e.Architecture }).HasDatabaseName("idx_func_proof_build_arch");
|
||||
|
||||
entity.HasMany(e => e.Nodes)
|
||||
.WithOne(n => n.FuncProof)
|
||||
.HasForeignKey(n => n.FuncProofId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasMany(e => e.Traces)
|
||||
.WithOne(t => t.FuncProof)
|
||||
.HasForeignKey(t => t.FuncProofId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<FuncNodeEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("func_node", schema);
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id").HasDefaultValueSql("gen_random_uuid()");
|
||||
entity.Property(e => e.FuncProofId).HasColumnName("func_proof_id");
|
||||
entity.Property(e => e.Symbol).HasColumnName("symbol");
|
||||
entity.Property(e => e.SymbolDigest).HasColumnName("symbol_digest");
|
||||
entity.Property(e => e.StartAddress).HasColumnName("start_address");
|
||||
entity.Property(e => e.EndAddress).HasColumnName("end_address");
|
||||
entity.Property(e => e.FunctionHash).HasColumnName("function_hash");
|
||||
entity.Property(e => e.Confidence).HasColumnName("confidence").HasDefaultValue(1.0);
|
||||
entity.Property(e => e.IsEntrypoint).HasColumnName("is_entrypoint").HasDefaultValue(false);
|
||||
entity.Property(e => e.EntrypointType).HasColumnName("entrypoint_type");
|
||||
entity.Property(e => e.IsSink).HasColumnName("is_sink").HasDefaultValue(false);
|
||||
entity.Property(e => e.SinkVulnId).HasColumnName("sink_vuln_id");
|
||||
entity.Property(e => e.SourceFile).HasColumnName("source_file");
|
||||
entity.Property(e => e.SourceLine).HasColumnName("source_line");
|
||||
entity.Property(e => e.CreatedAtUtc).HasColumnName("created_at_utc").HasDefaultValueSql("NOW()");
|
||||
|
||||
entity.HasIndex(e => e.SymbolDigest).HasDatabaseName("idx_func_node_symbol_digest");
|
||||
entity.HasIndex(e => e.FuncProofId).HasDatabaseName("idx_func_node_proof_id");
|
||||
entity.HasIndex(e => e.Symbol).HasDatabaseName("idx_func_node_symbol");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<FuncTraceEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("func_trace", schema);
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id").HasDefaultValueSql("gen_random_uuid()");
|
||||
entity.Property(e => e.FuncProofId).HasColumnName("func_proof_id");
|
||||
entity.Property(e => e.TraceId).HasColumnName("trace_id");
|
||||
entity.Property(e => e.EdgeListHash).HasColumnName("edge_list_hash");
|
||||
entity.Property(e => e.HopCount).HasColumnName("hop_count");
|
||||
entity.Property(e => e.EntrySymbolDigest).HasColumnName("entry_symbol_digest");
|
||||
entity.Property(e => e.SinkSymbolDigest).HasColumnName("sink_symbol_digest");
|
||||
entity.Property(e => e.Path).HasColumnName("path");
|
||||
entity.Property(e => e.Truncated).HasColumnName("truncated").HasDefaultValue(false);
|
||||
entity.Property(e => e.CreatedAtUtc).HasColumnName("created_at_utc").HasDefaultValueSql("NOW()");
|
||||
|
||||
entity.HasIndex(e => e.FuncProofId).HasDatabaseName("idx_func_trace_proof_id");
|
||||
entity.HasIndex(e => e.EntrySymbolDigest).HasDatabaseName("idx_func_trace_entry_digest");
|
||||
entity.HasIndex(e => e.SinkSymbolDigest).HasDatabaseName("idx_func_trace_sink_digest");
|
||||
entity.HasIndex(e => e.EdgeListHash).HasDatabaseName("idx_func_trace_edge_hash");
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// Facet seals (scanner schema)
|
||||
// ======================================================================
|
||||
|
||||
modelBuilder.Entity<FacetSealEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("facet_seals", schema);
|
||||
entity.HasKey(e => e.CombinedMerkleRoot);
|
||||
|
||||
entity.Property(e => e.CombinedMerkleRoot).HasColumnName("combined_merkle_root");
|
||||
entity.Property(e => e.ImageDigest).HasColumnName("image_digest");
|
||||
entity.Property(e => e.SchemaVersion).HasColumnName("schema_version");
|
||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at");
|
||||
entity.Property(e => e.BuildAttestationRef).HasColumnName("build_attestation_ref");
|
||||
entity.Property(e => e.Signature).HasColumnName("signature");
|
||||
entity.Property(e => e.SigningKeyId).HasColumnName("signing_key_id");
|
||||
entity.Property(e => e.SealContent).HasColumnName("seal_content").HasColumnType("jsonb");
|
||||
|
||||
entity.HasIndex(e => e.ImageDigest).HasDatabaseName("idx_facet_seals_image_digest");
|
||||
entity.HasIndex(e => new { e.ImageDigest, e.CreatedAt })
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("idx_facet_seals_image_created");
|
||||
});
|
||||
|
||||
OnModelCreatingPartial(modelBuilder);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace StellaOps.Scanner.Storage.EfCore.Models;
|
||||
|
||||
public class CodeChangeEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid TenantId { get; set; }
|
||||
public string ScanId { get; set; } = null!;
|
||||
public string BaseScanId { get; set; } = null!;
|
||||
public string Language { get; set; } = null!;
|
||||
public string? NodeId { get; set; }
|
||||
public string File { get; set; } = null!;
|
||||
public string Symbol { get; set; } = null!;
|
||||
public string ChangeKind { get; set; } = null!;
|
||||
public string? Details { get; set; }
|
||||
public DateTimeOffset DetectedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace StellaOps.Scanner.Storage.EfCore.Models;
|
||||
|
||||
public class DriftedSinkEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid DriftResultId { get; set; }
|
||||
public string SinkNodeId { get; set; } = null!;
|
||||
public string Symbol { get; set; } = null!;
|
||||
public string SinkCategory { get; set; } = null!;
|
||||
public string Direction { get; set; } = null!;
|
||||
public string CauseKind { get; set; } = null!;
|
||||
public string CauseDescription { get; set; } = null!;
|
||||
public string? CauseSymbol { get; set; }
|
||||
public string? CauseFile { get; set; }
|
||||
public int? CauseLine { get; set; }
|
||||
public Guid? CodeChangeId { get; set; }
|
||||
public string CompressedPath { get; set; } = null!;
|
||||
public string? AssociatedVulns { get; set; }
|
||||
|
||||
public ReachabilityDriftResultEntity DriftResult { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace StellaOps.Scanner.Storage.EfCore.Models;
|
||||
|
||||
public class EpssRawEntity
|
||||
{
|
||||
public long RawId { get; set; }
|
||||
public string SourceUri { get; set; } = null!;
|
||||
public DateOnly AsOfDate { get; set; }
|
||||
public DateTimeOffset IngestionTs { get; set; }
|
||||
public string Payload { get; set; } = null!;
|
||||
public byte[] PayloadSha256 { get; set; } = null!;
|
||||
public string? HeaderComment { get; set; }
|
||||
public string? ModelVersion { get; set; }
|
||||
public DateOnly? PublishedDate { get; set; }
|
||||
public int RowCount { get; set; }
|
||||
public long? CompressedSize { get; set; }
|
||||
public long? DecompressedSize { get; set; }
|
||||
public Guid? ImportRunId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace StellaOps.Scanner.Storage.EfCore.Models;
|
||||
|
||||
public class EpssSignalConfigEntity
|
||||
{
|
||||
public Guid ConfigId { get; set; }
|
||||
public Guid TenantId { get; set; }
|
||||
public double CriticalPercentile { get; set; }
|
||||
public double HighPercentile { get; set; }
|
||||
public double MediumPercentile { get; set; }
|
||||
public double BigJumpDelta { get; set; }
|
||||
public bool SuppressOnModelChange { get; set; }
|
||||
public string[] EnabledEventTypes { get; set; } = [];
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace StellaOps.Scanner.Storage.EfCore.Models;
|
||||
|
||||
public class EpssSignalEntity
|
||||
{
|
||||
public long SignalId { get; set; }
|
||||
public Guid TenantId { get; set; }
|
||||
public DateOnly ModelDate { get; set; }
|
||||
public string CveId { get; set; } = null!;
|
||||
public string EventType { get; set; } = null!;
|
||||
public string? RiskBand { get; set; }
|
||||
public double? EpssScore { get; set; }
|
||||
public double? EpssDelta { get; set; }
|
||||
public double? Percentile { get; set; }
|
||||
public double? PercentileDelta { get; set; }
|
||||
public bool IsModelChange { get; set; }
|
||||
public string? ModelVersion { get; set; }
|
||||
public string DedupeKey { get; set; } = null!;
|
||||
public byte[] ExplainHash { get; set; } = null!;
|
||||
public string Payload { get; set; } = null!;
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace StellaOps.Scanner.Storage.EfCore.Models;
|
||||
|
||||
public class FacetSealEntity
|
||||
{
|
||||
public string CombinedMerkleRoot { get; set; } = null!;
|
||||
public string ImageDigest { get; set; } = null!;
|
||||
public int SchemaVersion { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public string? BuildAttestationRef { get; set; }
|
||||
public string? Signature { get; set; }
|
||||
public string? SigningKeyId { get; set; }
|
||||
public string SealContent { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace StellaOps.Scanner.Storage.EfCore.Models;
|
||||
|
||||
public class FuncNodeEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid FuncProofId { get; set; }
|
||||
public string Symbol { get; set; } = null!;
|
||||
public string SymbolDigest { get; set; } = null!;
|
||||
public long StartAddress { get; set; }
|
||||
public long EndAddress { get; set; }
|
||||
public string FunctionHash { get; set; } = null!;
|
||||
public double Confidence { get; set; }
|
||||
public bool IsEntrypoint { get; set; }
|
||||
public string? EntrypointType { get; set; }
|
||||
public bool IsSink { get; set; }
|
||||
public string? SinkVulnId { get; set; }
|
||||
public string? SourceFile { get; set; }
|
||||
public int? SourceLine { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; }
|
||||
|
||||
public FuncProofEntity FuncProof { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace StellaOps.Scanner.Storage.EfCore.Models;
|
||||
|
||||
public class FuncProofEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ScanId { get; set; }
|
||||
public string ProofId { get; set; } = null!;
|
||||
public string BuildId { get; set; } = null!;
|
||||
public string BuildIdType { get; set; } = null!;
|
||||
public string FileSha256 { get; set; } = null!;
|
||||
public string BinaryFormat { get; set; } = null!;
|
||||
public string Architecture { get; set; } = null!;
|
||||
public bool IsStripped { get; set; }
|
||||
public int FunctionCount { get; set; }
|
||||
public int TraceCount { get; set; }
|
||||
public string ProofContent { get; set; } = null!;
|
||||
public byte[]? CompressedContent { get; set; }
|
||||
public string? DsseEnvelopeId { get; set; }
|
||||
public string? OciArtifactDigest { get; set; }
|
||||
public string? RekorEntryId { get; set; }
|
||||
public string GeneratorVersion { get; set; } = null!;
|
||||
public DateTimeOffset GeneratedAtUtc { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; }
|
||||
public DateTimeOffset? UpdatedAtUtc { get; set; }
|
||||
|
||||
public ICollection<FuncNodeEntity> Nodes { get; set; } = new List<FuncNodeEntity>();
|
||||
public ICollection<FuncTraceEntity> Traces { get; set; } = new List<FuncTraceEntity>();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace StellaOps.Scanner.Storage.EfCore.Models;
|
||||
|
||||
public class FuncTraceEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid FuncProofId { get; set; }
|
||||
public string TraceId { get; set; } = null!;
|
||||
public string EdgeListHash { get; set; } = null!;
|
||||
public int HopCount { get; set; }
|
||||
public string EntrySymbolDigest { get; set; } = null!;
|
||||
public string SinkSymbolDigest { get; set; } = null!;
|
||||
public string[] Path { get; set; } = [];
|
||||
public bool Truncated { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; }
|
||||
|
||||
public FuncProofEntity FuncProof { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace StellaOps.Scanner.Storage.EfCore.Models;
|
||||
|
||||
public class ReachabilityDriftResultEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid TenantId { get; set; }
|
||||
public string BaseScanId { get; set; } = null!;
|
||||
public string HeadScanId { get; set; } = null!;
|
||||
public string Language { get; set; } = null!;
|
||||
public int NewlyReachableCount { get; set; }
|
||||
public int NewlyUnreachableCount { get; set; }
|
||||
public DateTimeOffset DetectedAt { get; set; }
|
||||
public string ResultDigest { get; set; } = null!;
|
||||
|
||||
public ICollection<DriftedSinkEntity> DriftedSinks { get; set; } = new List<DriftedSinkEntity>();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace StellaOps.Scanner.Storage.EfCore.Models;
|
||||
|
||||
public class VexCandidateEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string CandidateId { get; set; } = null!;
|
||||
public Guid TenantId { get; set; }
|
||||
public string VulnId { get; set; } = null!;
|
||||
public string Purl { get; set; } = null!;
|
||||
public string ImageDigest { get; set; } = null!;
|
||||
public string SuggestedStatus { get; set; } = null!;
|
||||
public string Justification { get; set; } = null!;
|
||||
public string Rationale { get; set; } = null!;
|
||||
public string EvidenceLinks { get; set; } = null!;
|
||||
public decimal Confidence { get; set; }
|
||||
public DateTimeOffset GeneratedAt { get; set; }
|
||||
public DateTimeOffset ExpiresAt { get; set; }
|
||||
public bool RequiresReview { get; set; }
|
||||
public string? ReviewAction { get; set; }
|
||||
public string? ReviewedBy { get; set; }
|
||||
public DateTimeOffset? ReviewedAt { get; set; }
|
||||
public string? ReviewComment { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
@@ -11,6 +11,7 @@ ALTER TABLE scanner.runtime_observations
|
||||
ADD COLUMN IF NOT EXISTS observation_id TEXT,
|
||||
ADD COLUMN IF NOT EXISTS node_hash TEXT,
|
||||
ADD COLUMN IF NOT EXISTS function_name TEXT,
|
||||
ADD COLUMN IF NOT EXISTS container_id TEXT,
|
||||
ADD COLUMN IF NOT EXISTS pod_name TEXT,
|
||||
ADD COLUMN IF NOT EXISTS namespace TEXT,
|
||||
ADD COLUMN IF NOT EXISTS probe_type TEXT,
|
||||
|
||||
@@ -212,8 +212,16 @@ public sealed class PostgresArtifactBomRepository : IArtifactBomRepository
|
||||
canonical_bom_sha256 AS "CanonicalBomSha256",
|
||||
payload_digest AS "PayloadDigest",
|
||||
inserted_at AS "InsertedAt",
|
||||
NULL::text AS "RawBomRef",
|
||||
NULL::text AS "CanonicalBomRef",
|
||||
NULL::text AS "DsseEnvelopeRef",
|
||||
NULL::text AS "MergedVexRef",
|
||||
NULL::text AS "CanonicalBomJson",
|
||||
NULL::text AS "MergedVexJson",
|
||||
NULL::text AS "AttestationsJson",
|
||||
evidence_score AS "EvidenceScore",
|
||||
rekor_tile_id AS "RekorTileId"
|
||||
rekor_tile_id AS "RekorTileId",
|
||||
NULL::text AS "PendingMergedVexJson"
|
||||
FROM {TableName}
|
||||
WHERE payload_digest = @p0
|
||||
ORDER BY inserted_at DESC, build_id ASC
|
||||
@@ -246,7 +254,16 @@ public sealed class PostgresArtifactBomRepository : IArtifactBomRepository
|
||||
canonical_bom_sha256 AS "CanonicalBomSha256",
|
||||
payload_digest AS "PayloadDigest",
|
||||
inserted_at AS "InsertedAt",
|
||||
evidence_score AS "EvidenceScore"
|
||||
NULL::text AS "RawBomRef",
|
||||
NULL::text AS "CanonicalBomRef",
|
||||
NULL::text AS "DsseEnvelopeRef",
|
||||
NULL::text AS "MergedVexRef",
|
||||
NULL::text AS "CanonicalBomJson",
|
||||
NULL::text AS "MergedVexJson",
|
||||
NULL::text AS "AttestationsJson",
|
||||
evidence_score AS "EvidenceScore",
|
||||
NULL::text AS "RekorTileId",
|
||||
NULL::text AS "PendingMergedVexJson"
|
||||
FROM {TableName}
|
||||
WHERE jsonb_path_exists(
|
||||
canonical_bom,
|
||||
@@ -289,7 +306,16 @@ public sealed class PostgresArtifactBomRepository : IArtifactBomRepository
|
||||
canonical_bom_sha256 AS "CanonicalBomSha256",
|
||||
payload_digest AS "PayloadDigest",
|
||||
inserted_at AS "InsertedAt",
|
||||
evidence_score AS "EvidenceScore"
|
||||
NULL::text AS "RawBomRef",
|
||||
NULL::text AS "CanonicalBomRef",
|
||||
NULL::text AS "DsseEnvelopeRef",
|
||||
NULL::text AS "MergedVexRef",
|
||||
NULL::text AS "CanonicalBomJson",
|
||||
NULL::text AS "MergedVexJson",
|
||||
NULL::text AS "AttestationsJson",
|
||||
evidence_score AS "EvidenceScore",
|
||||
NULL::text AS "RekorTileId",
|
||||
NULL::text AS "PendingMergedVexJson"
|
||||
FROM {TableName}
|
||||
WHERE jsonb_path_exists(
|
||||
canonical_bom,
|
||||
@@ -333,7 +359,15 @@ public sealed class PostgresArtifactBomRepository : IArtifactBomRepository
|
||||
canonical_bom_sha256 AS "CanonicalBomSha256",
|
||||
payload_digest AS "PayloadDigest",
|
||||
inserted_at AS "InsertedAt",
|
||||
NULL::text AS "RawBomRef",
|
||||
NULL::text AS "CanonicalBomRef",
|
||||
NULL::text AS "DsseEnvelopeRef",
|
||||
NULL::text AS "MergedVexRef",
|
||||
NULL::text AS "CanonicalBomJson",
|
||||
NULL::text AS "MergedVexJson",
|
||||
NULL::text AS "AttestationsJson",
|
||||
evidence_score AS "EvidenceScore",
|
||||
NULL::text AS "RekorTileId",
|
||||
jsonb_path_query_array(merged_vex, @p0::jsonpath)::text AS "PendingMergedVexJson"
|
||||
FROM {TableName}
|
||||
WHERE jsonb_path_exists(merged_vex, @p0::jsonpath)
|
||||
|
||||
@@ -84,7 +84,7 @@ public sealed class PostgresCallGraphSnapshotRepository : ICallGraphSnapshotRepo
|
||||
var tenantScope = ScannerTenantScope.Resolve(tenantId);
|
||||
|
||||
var sql = $"""
|
||||
SELECT snapshot_json
|
||||
SELECT snapshot_json AS "Value"
|
||||
FROM {CallGraphSnapshotsTable}
|
||||
WHERE tenant_id = @p0 AND scan_id = @p1 AND language = @p2
|
||||
ORDER BY extracted_at DESC
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using StellaOps.Scanner.Storage.EfCore.Models;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Postgres;
|
||||
@@ -79,26 +80,16 @@ public sealed class PostgresEpssRawRepository : IEpssRawRepository
|
||||
|
||||
public async Task<EpssRaw?> GetByDateAsync(DateOnly asOfDate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT
|
||||
raw_id, source_uri, asof_date, ingestion_ts,
|
||||
payload, payload_sha256, header_comment, model_version, published_date,
|
||||
row_count, compressed_size, decompressed_size, import_run_id
|
||||
FROM {RawTable}
|
||||
WHERE asof_date = @p0
|
||||
ORDER BY ingestion_ts DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var row = await dbContext.Database.SqlQueryRaw<RawRow>(
|
||||
sql, asOfDate.ToDateTime(TimeOnly.MinValue))
|
||||
var entity = await dbContext.EpssRaw
|
||||
.Where(e => e.AsOfDate == asOfDate)
|
||||
.OrderByDescending(e => e.IngestionTs)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return row is not null && row.raw_id != 0 ? MapToRaw(row) : null;
|
||||
return entity is not null ? MapEntityToRaw(entity) : null;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<EpssRaw>> GetByDateRangeAsync(
|
||||
@@ -106,64 +97,40 @@ public sealed class PostgresEpssRawRepository : IEpssRawRepository
|
||||
DateOnly endDate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT
|
||||
raw_id, source_uri, asof_date, ingestion_ts,
|
||||
payload, payload_sha256, header_comment, model_version, published_date,
|
||||
row_count, compressed_size, decompressed_size, import_run_id
|
||||
FROM {RawTable}
|
||||
WHERE asof_date >= @p0 AND asof_date <= @p1
|
||||
ORDER BY asof_date DESC
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var rows = await dbContext.Database.SqlQueryRaw<RawRow>(
|
||||
sql, startDate.ToDateTime(TimeOnly.MinValue), endDate.ToDateTime(TimeOnly.MinValue))
|
||||
var entities = await dbContext.EpssRaw
|
||||
.Where(e => e.AsOfDate >= startDate && e.AsOfDate <= endDate)
|
||||
.OrderByDescending(e => e.AsOfDate)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return rows.Select(MapToRaw).ToList();
|
||||
return entities.Select(MapEntityToRaw).ToList();
|
||||
}
|
||||
|
||||
public async Task<EpssRaw?> GetLatestAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT
|
||||
raw_id, source_uri, asof_date, ingestion_ts,
|
||||
payload, payload_sha256, header_comment, model_version, published_date,
|
||||
row_count, compressed_size, decompressed_size, import_run_id
|
||||
FROM {RawTable}
|
||||
ORDER BY asof_date DESC, ingestion_ts DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var row = await dbContext.Database.SqlQueryRaw<RawRow>(sql)
|
||||
var entity = await dbContext.EpssRaw
|
||||
.OrderByDescending(e => e.AsOfDate)
|
||||
.ThenByDescending(e => e.IngestionTs)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return row is not null && row.raw_id != 0 ? MapToRaw(row) : null;
|
||||
return entity is not null ? MapEntityToRaw(entity) : null;
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(DateOnly asOfDate, byte[] payloadSha256, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT CAST(CASE WHEN EXISTS (
|
||||
SELECT 1 FROM {RawTable}
|
||||
WHERE asof_date = $1 AND payload_sha256 = $2
|
||||
) THEN 1 ELSE 0 END AS integer)
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
await using var cmd = new NpgsqlCommand(sql, connection);
|
||||
cmd.Parameters.AddWithValue(asOfDate.ToDateTime(TimeOnly.MinValue));
|
||||
cmd.Parameters.AddWithValue(payloadSha256);
|
||||
var result = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Convert.ToInt32(result) == 1;
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
return await dbContext.EpssRaw
|
||||
.AnyAsync(e => e.AsOfDate == asOfDate && e.PayloadSha256 == payloadSha256, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<EpssRaw>> GetByModelVersionAsync(
|
||||
@@ -171,26 +138,17 @@ public sealed class PostgresEpssRawRepository : IEpssRawRepository
|
||||
int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT
|
||||
raw_id, source_uri, asof_date, ingestion_ts,
|
||||
payload, payload_sha256, header_comment, model_version, published_date,
|
||||
row_count, compressed_size, decompressed_size, import_run_id
|
||||
FROM {RawTable}
|
||||
WHERE model_version = @p0
|
||||
ORDER BY asof_date DESC
|
||||
LIMIT @p1
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var rows = await dbContext.Database.SqlQueryRaw<RawRow>(
|
||||
sql, modelVersion, limit)
|
||||
var entities = await dbContext.EpssRaw
|
||||
.Where(e => e.ModelVersion == modelVersion)
|
||||
.OrderByDescending(e => e.AsOfDate)
|
||||
.Take(limit)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return rows.Select(MapToRaw).ToList();
|
||||
return entities.Select(MapEntityToRaw).ToList();
|
||||
}
|
||||
|
||||
public async Task<int> PruneAsync(int retentionDays = 365, CancellationToken cancellationToken = default)
|
||||
@@ -204,40 +162,23 @@ public sealed class PostgresEpssRawRepository : IEpssRawRepository
|
||||
return Convert.ToInt32(result);
|
||||
}
|
||||
|
||||
private static EpssRaw MapToRaw(RawRow row)
|
||||
private static EpssRaw MapEntityToRaw(EpssRawEntity entity)
|
||||
{
|
||||
return new EpssRaw
|
||||
{
|
||||
RawId = row.raw_id,
|
||||
SourceUri = row.source_uri,
|
||||
AsOfDate = DateOnly.FromDateTime(row.asof_date),
|
||||
IngestionTs = row.ingestion_ts,
|
||||
Payload = row.payload,
|
||||
PayloadSha256 = row.payload_sha256,
|
||||
HeaderComment = row.header_comment,
|
||||
ModelVersion = row.model_version,
|
||||
PublishedDate = row.published_date.HasValue ? DateOnly.FromDateTime(row.published_date.Value) : null,
|
||||
RowCount = row.row_count,
|
||||
CompressedSize = row.compressed_size,
|
||||
DecompressedSize = row.decompressed_size,
|
||||
ImportRunId = row.import_run_id
|
||||
RawId = entity.RawId,
|
||||
SourceUri = entity.SourceUri,
|
||||
AsOfDate = entity.AsOfDate,
|
||||
IngestionTs = entity.IngestionTs,
|
||||
Payload = entity.Payload,
|
||||
PayloadSha256 = entity.PayloadSha256,
|
||||
HeaderComment = entity.HeaderComment,
|
||||
ModelVersion = entity.ModelVersion,
|
||||
PublishedDate = entity.PublishedDate,
|
||||
RowCount = entity.RowCount,
|
||||
CompressedSize = entity.CompressedSize,
|
||||
DecompressedSize = entity.DecompressedSize,
|
||||
ImportRunId = entity.ImportRunId
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class RawRow
|
||||
{
|
||||
public long raw_id { get; set; }
|
||||
public string source_uri { get; set; } = "";
|
||||
public DateTime asof_date { get; set; }
|
||||
public DateTimeOffset ingestion_ts { get; set; }
|
||||
public string payload { get; set; } = "";
|
||||
public byte[] payload_sha256 { get; set; } = [];
|
||||
public string? header_comment { get; set; }
|
||||
public string? model_version { get; set; }
|
||||
public DateTime? published_date { get; set; }
|
||||
public int row_count { get; set; }
|
||||
public long? compressed_size { get; set; }
|
||||
public long? decompressed_size { get; set; }
|
||||
public Guid? import_run_id { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using StellaOps.Scanner.Storage.EfCore.Models;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -150,44 +151,27 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
var eventTypeList = eventTypes?.ToList();
|
||||
var hasEventTypeFilter = eventTypeList?.Count > 0;
|
||||
|
||||
var paramList = new List<object>
|
||||
{
|
||||
tenantId,
|
||||
startDate.ToDateTime(TimeOnly.MinValue),
|
||||
endDate.ToDateTime(TimeOnly.MinValue)
|
||||
};
|
||||
var paramIndex = 3;
|
||||
|
||||
var eventTypeClause = "";
|
||||
if (hasEventTypeFilter)
|
||||
{
|
||||
eventTypeClause = $"AND event_type = ANY(@p{paramIndex})";
|
||||
paramList.Add(eventTypeList!.ToArray());
|
||||
}
|
||||
|
||||
var sql = $"""
|
||||
SELECT
|
||||
signal_id, tenant_id, model_date, cve_id, event_type, risk_band,
|
||||
epss_score, epss_delta, percentile, percentile_delta,
|
||||
is_model_change, model_version, dedupe_key, explain_hash, payload, created_at
|
||||
FROM {SignalTable}
|
||||
WHERE tenant_id = @p0
|
||||
AND model_date >= @p1
|
||||
AND model_date <= @p2
|
||||
{eventTypeClause}
|
||||
ORDER BY model_date DESC, created_at DESC
|
||||
LIMIT 10000
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var rows = await dbContext.Database.SqlQueryRaw<SignalRow>(
|
||||
sql, paramList.ToArray())
|
||||
IQueryable<EpssSignalEntity> query = dbContext.EpssSignals
|
||||
.Where(e => e.TenantId == tenantId
|
||||
&& e.ModelDate >= startDate
|
||||
&& e.ModelDate <= endDate);
|
||||
|
||||
if (hasEventTypeFilter)
|
||||
{
|
||||
query = query.Where(e => eventTypeList!.Contains(e.EventType));
|
||||
}
|
||||
|
||||
var entities = await query
|
||||
.OrderByDescending(e => e.ModelDate)
|
||||
.ThenByDescending(e => e.CreatedAt)
|
||||
.Take(10000)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return rows.Select(MapToSignal).ToList();
|
||||
return entities.Select(MapEntityToSignal).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<EpssSignal>> GetByCveAsync(
|
||||
@@ -196,27 +180,18 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT
|
||||
signal_id, tenant_id, model_date, cve_id, event_type, risk_band,
|
||||
epss_score, epss_delta, percentile, percentile_delta,
|
||||
is_model_change, model_version, dedupe_key, explain_hash, payload, created_at
|
||||
FROM {SignalTable}
|
||||
WHERE tenant_id = @p0
|
||||
AND cve_id = @p1
|
||||
ORDER BY model_date DESC, created_at DESC
|
||||
LIMIT @p2
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var rows = await dbContext.Database.SqlQueryRaw<SignalRow>(
|
||||
sql, tenantId, cveId, limit)
|
||||
var entities = await dbContext.EpssSignals
|
||||
.Where(e => e.TenantId == tenantId && e.CveId == cveId)
|
||||
.OrderByDescending(e => e.ModelDate)
|
||||
.ThenByDescending(e => e.CreatedAt)
|
||||
.Take(limit)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return rows.Select(MapToSignal).ToList();
|
||||
return entities.Select(MapEntityToSignal).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<EpssSignal>> GetHighPriorityAsync(
|
||||
@@ -225,52 +200,35 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
DateOnly endDate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT
|
||||
signal_id, tenant_id, model_date, cve_id, event_type, risk_band,
|
||||
epss_score, epss_delta, percentile, percentile_delta,
|
||||
is_model_change, model_version, dedupe_key, explain_hash, payload, created_at
|
||||
FROM {SignalTable}
|
||||
WHERE tenant_id = @p0
|
||||
AND model_date >= @p1
|
||||
AND model_date <= @p2
|
||||
AND risk_band IN ('CRITICAL', 'HIGH')
|
||||
ORDER BY model_date DESC, created_at DESC
|
||||
LIMIT 10000
|
||||
""";
|
||||
string[] highPriorityBands = ["CRITICAL", "HIGH"];
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var rows = await dbContext.Database.SqlQueryRaw<SignalRow>(
|
||||
sql, tenantId, startDate.ToDateTime(TimeOnly.MinValue), endDate.ToDateTime(TimeOnly.MinValue))
|
||||
var entities = await dbContext.EpssSignals
|
||||
.Where(e => e.TenantId == tenantId
|
||||
&& e.ModelDate >= startDate
|
||||
&& e.ModelDate <= endDate
|
||||
&& e.RiskBand != null && highPriorityBands.Contains(e.RiskBand))
|
||||
.OrderByDescending(e => e.ModelDate)
|
||||
.ThenByDescending(e => e.CreatedAt)
|
||||
.Take(10000)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return rows.Select(MapToSignal).ToList();
|
||||
return entities.Select(MapEntityToSignal).ToList();
|
||||
}
|
||||
|
||||
public async Task<EpssSignalConfig?> GetConfigAsync(Guid tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT
|
||||
config_id, tenant_id,
|
||||
critical_percentile, high_percentile, medium_percentile,
|
||||
big_jump_delta, suppress_on_model_change, enabled_event_types,
|
||||
created_at, updated_at
|
||||
FROM {ConfigTable}
|
||||
WHERE tenant_id = @p0
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var row = await dbContext.Database.SqlQueryRaw<ConfigRow>(
|
||||
sql, tenantId)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
var entity = await dbContext.EpssSignalConfigs
|
||||
.FirstOrDefaultAsync(e => e.TenantId == tenantId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return row is not null && row.config_id != Guid.Empty ? MapToConfig(row) : null;
|
||||
return entity is not null ? MapEntityToConfig(entity) : null;
|
||||
}
|
||||
|
||||
public async Task<EpssSignalConfig> UpsertConfigAsync(EpssSignalConfig config, CancellationToken cancellationToken = default)
|
||||
@@ -331,97 +289,53 @@ public sealed class PostgresEpssSignalRepository : IEpssSignalRepository
|
||||
|
||||
private async Task<EpssSignal?> GetByDedupeKeyAsync(Guid tenantId, string dedupeKey, CancellationToken cancellationToken)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT
|
||||
signal_id, tenant_id, model_date, cve_id, event_type, risk_band,
|
||||
epss_score, epss_delta, percentile, percentile_delta,
|
||||
is_model_change, model_version, dedupe_key, explain_hash, payload, created_at
|
||||
FROM {SignalTable}
|
||||
WHERE tenant_id = @p0 AND dedupe_key = @p1
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId.ToString("D"), cancellationToken);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var row = await dbContext.Database.SqlQueryRaw<SignalRow>(
|
||||
sql, tenantId, dedupeKey)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
var entity = await dbContext.EpssSignals
|
||||
.FirstOrDefaultAsync(e => e.TenantId == tenantId && e.DedupeKey == dedupeKey, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return row is not null && row.signal_id != 0 ? MapToSignal(row) : null;
|
||||
return entity is not null ? MapEntityToSignal(entity) : null;
|
||||
}
|
||||
|
||||
private static EpssSignal MapToSignal(SignalRow row)
|
||||
private static EpssSignal MapEntityToSignal(EpssSignalEntity entity)
|
||||
{
|
||||
return new EpssSignal
|
||||
{
|
||||
SignalId = row.signal_id,
|
||||
TenantId = row.tenant_id,
|
||||
ModelDate = row.model_date,
|
||||
CveId = row.cve_id,
|
||||
EventType = row.event_type,
|
||||
RiskBand = row.risk_band,
|
||||
EpssScore = row.epss_score,
|
||||
EpssDelta = row.epss_delta,
|
||||
Percentile = row.percentile,
|
||||
PercentileDelta = row.percentile_delta,
|
||||
IsModelChange = row.is_model_change,
|
||||
ModelVersion = row.model_version,
|
||||
DedupeKey = row.dedupe_key,
|
||||
ExplainHash = row.explain_hash,
|
||||
Payload = row.payload,
|
||||
CreatedAt = row.created_at
|
||||
SignalId = entity.SignalId,
|
||||
TenantId = entity.TenantId,
|
||||
ModelDate = entity.ModelDate,
|
||||
CveId = entity.CveId,
|
||||
EventType = entity.EventType,
|
||||
RiskBand = entity.RiskBand,
|
||||
EpssScore = entity.EpssScore,
|
||||
EpssDelta = entity.EpssDelta,
|
||||
Percentile = entity.Percentile,
|
||||
PercentileDelta = entity.PercentileDelta,
|
||||
IsModelChange = entity.IsModelChange,
|
||||
ModelVersion = entity.ModelVersion,
|
||||
DedupeKey = entity.DedupeKey,
|
||||
ExplainHash = entity.ExplainHash,
|
||||
Payload = entity.Payload,
|
||||
CreatedAt = entity.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
private static EpssSignalConfig MapToConfig(ConfigRow row)
|
||||
private static EpssSignalConfig MapEntityToConfig(EpssSignalConfigEntity entity)
|
||||
{
|
||||
return new EpssSignalConfig
|
||||
{
|
||||
ConfigId = row.config_id,
|
||||
TenantId = row.tenant_id,
|
||||
CriticalPercentile = row.critical_percentile,
|
||||
HighPercentile = row.high_percentile,
|
||||
MediumPercentile = row.medium_percentile,
|
||||
BigJumpDelta = row.big_jump_delta,
|
||||
SuppressOnModelChange = row.suppress_on_model_change,
|
||||
EnabledEventTypes = row.enabled_event_types ?? Array.Empty<string>(),
|
||||
CreatedAt = row.created_at,
|
||||
UpdatedAt = row.updated_at
|
||||
ConfigId = entity.ConfigId,
|
||||
TenantId = entity.TenantId,
|
||||
CriticalPercentile = entity.CriticalPercentile,
|
||||
HighPercentile = entity.HighPercentile,
|
||||
MediumPercentile = entity.MediumPercentile,
|
||||
BigJumpDelta = entity.BigJumpDelta,
|
||||
SuppressOnModelChange = entity.SuppressOnModelChange,
|
||||
EnabledEventTypes = entity.EnabledEventTypes ?? [],
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class SignalRow
|
||||
{
|
||||
public long signal_id { get; set; }
|
||||
public Guid tenant_id { get; set; }
|
||||
public DateOnly model_date { get; set; }
|
||||
public string cve_id { get; set; } = "";
|
||||
public string event_type { get; set; } = "";
|
||||
public string? risk_band { get; set; }
|
||||
public double? epss_score { get; set; }
|
||||
public double? epss_delta { get; set; }
|
||||
public double? percentile { get; set; }
|
||||
public double? percentile_delta { get; set; }
|
||||
public bool is_model_change { get; set; }
|
||||
public string? model_version { get; set; }
|
||||
public string dedupe_key { get; set; } = "";
|
||||
public byte[] explain_hash { get; set; } = [];
|
||||
public string payload { get; set; } = "";
|
||||
public DateTimeOffset created_at { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ConfigRow
|
||||
{
|
||||
public Guid config_id { get; set; }
|
||||
public Guid tenant_id { get; set; }
|
||||
public double critical_percentile { get; set; }
|
||||
public double high_percentile { get; set; }
|
||||
public double medium_percentile { get; set; }
|
||||
public double big_jump_delta { get; set; }
|
||||
public bool suppress_on_model_change { get; set; }
|
||||
public string[]? enabled_event_types { get; set; }
|
||||
public DateTimeOffset created_at { get; set; }
|
||||
public DateTimeOffset updated_at { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
// Sprint: SPRINT_20260105_002_003_FACET (QTA-013)
|
||||
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using NpgsqlTypes;
|
||||
using StellaOps.Facet;
|
||||
using StellaOps.Facet.Serialization;
|
||||
using StellaOps.Scanner.Storage.EfCore.Models;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -29,10 +31,7 @@ public sealed class PostgresFacetSealStore : IFacetSealStore
|
||||
private readonly ILogger<PostgresFacetSealStore> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private const string SelectColumns = """
|
||||
combined_merkle_root, image_digest, schema_version, created_at,
|
||||
build_attestation_ref, signature, signing_key_id, seal_content
|
||||
""";
|
||||
private const int DefaultCommandTimeoutSeconds = 30;
|
||||
|
||||
private const string InsertSql = """
|
||||
INSERT INTO scanner.facet_seals (
|
||||
@@ -44,40 +43,6 @@ public sealed class PostgresFacetSealStore : IFacetSealStore
|
||||
)
|
||||
""";
|
||||
|
||||
private const string SelectLatestSql = $"""
|
||||
SELECT {SelectColumns}
|
||||
FROM scanner.facet_seals
|
||||
WHERE image_digest = @image_digest
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
private const string SelectByCombinedRootSql = $"""
|
||||
SELECT {SelectColumns}
|
||||
FROM scanner.facet_seals
|
||||
WHERE combined_merkle_root = @combined_merkle_root
|
||||
""";
|
||||
|
||||
private const string SelectHistorySql = $"""
|
||||
SELECT {SelectColumns}
|
||||
FROM scanner.facet_seals
|
||||
WHERE image_digest = @image_digest
|
||||
ORDER BY created_at DESC
|
||||
LIMIT @limit
|
||||
""";
|
||||
|
||||
private const string ExistsSql = """
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM scanner.facet_seals
|
||||
WHERE image_digest = @image_digest
|
||||
)
|
||||
""";
|
||||
|
||||
private const string DeleteByImageSql = """
|
||||
DELETE FROM scanner.facet_seals
|
||||
WHERE image_digest = @image_digest
|
||||
""";
|
||||
|
||||
private const string PurgeSql = """
|
||||
WITH ranked AS (
|
||||
SELECT combined_merkle_root, image_digest, created_at,
|
||||
@@ -116,16 +81,15 @@ public sealed class PostgresFacetSealStore : IFacetSealStore
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(imageDigest);
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(SelectLatestSql, conn);
|
||||
cmd.Parameters.AddWithValue("image_digest", imageDigest);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, ScannerStorageDefaults.DefaultSchemaName);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var entity = await dbContext.FacetSeals
|
||||
.Where(e => e.ImageDigest == imageDigest)
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.FirstOrDefaultAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return MapSeal(reader);
|
||||
return entity is not null ? MapEntityToSeal(entity) : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -135,16 +99,13 @@ public sealed class PostgresFacetSealStore : IFacetSealStore
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(combinedMerkleRoot);
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(SelectByCombinedRootSql, conn);
|
||||
cmd.Parameters.AddWithValue("combined_merkle_root", combinedMerkleRoot);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, ScannerStorageDefaults.DefaultSchemaName);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var entity = await dbContext.FacetSeals
|
||||
.FirstOrDefaultAsync(e => e.CombinedMerkleRoot == combinedMerkleRoot, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return MapSeal(reader);
|
||||
return entity is not null ? MapEntityToSeal(entity) : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -158,18 +119,16 @@ public sealed class PostgresFacetSealStore : IFacetSealStore
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(limit);
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(SelectHistorySql, conn);
|
||||
cmd.Parameters.AddWithValue("image_digest", imageDigest);
|
||||
cmd.Parameters.AddWithValue("limit", limit);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, ScannerStorageDefaults.DefaultSchemaName);
|
||||
|
||||
var seals = new List<FacetSeal>();
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
seals.Add(MapSeal(reader));
|
||||
}
|
||||
var entities = await dbContext.FacetSeals
|
||||
.Where(e => e.ImageDigest == imageDigest)
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.Take(limit)
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return [.. seals];
|
||||
return [.. entities.Select(MapEntityToSeal)];
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -214,11 +173,11 @@ public sealed class PostgresFacetSealStore : IFacetSealStore
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(imageDigest);
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(ExistsSql, conn);
|
||||
cmd.Parameters.AddWithValue("image_digest", imageDigest);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, ScannerStorageDefaults.DefaultSchemaName);
|
||||
|
||||
var result = await cmd.ExecuteScalarAsync(ct).ConfigureAwait(false);
|
||||
return result is true;
|
||||
return await dbContext.FacetSeals
|
||||
.AnyAsync(e => e.ImageDigest == imageDigest, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -228,10 +187,13 @@ public sealed class PostgresFacetSealStore : IFacetSealStore
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(imageDigest);
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(DeleteByImageSql, conn);
|
||||
cmd.Parameters.AddWithValue("image_digest", imageDigest);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, ScannerStorageDefaults.DefaultSchemaName);
|
||||
|
||||
var deleted = await dbContext.FacetSeals
|
||||
.Where(e => e.ImageDigest == imageDigest)
|
||||
.ExecuteDeleteAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var deleted = await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
|
||||
_logger.LogInformation("Deleted {Count} facet seal(s) for image {ImageDigest}",
|
||||
deleted, imageDigest);
|
||||
return deleted;
|
||||
@@ -259,16 +221,14 @@ public sealed class PostgresFacetSealStore : IFacetSealStore
|
||||
return purged;
|
||||
}
|
||||
|
||||
private static FacetSeal MapSeal(NpgsqlDataReader reader)
|
||||
private static FacetSeal MapEntityToSeal(FacetSealEntity entity)
|
||||
{
|
||||
// Read seal from JSONB column (index 7 is seal_content)
|
||||
var sealJson = reader.GetString(7);
|
||||
var seal = JsonSerializer.Deserialize<FacetSeal>(sealJson, FacetJsonOptions.Default);
|
||||
var seal = JsonSerializer.Deserialize<FacetSeal>(entity.SealContent, FacetJsonOptions.Default);
|
||||
|
||||
if (seal is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to deserialize facet seal from database: {reader.GetString(0)}");
|
||||
$"Failed to deserialize facet seal from database: {entity.CombinedMerkleRoot}");
|
||||
}
|
||||
|
||||
return seal;
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using NpgsqlTypes;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scanner.Storage.EfCore.Models;
|
||||
using StellaOps.Scanner.Storage.Entities;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -69,6 +71,8 @@ public sealed class PostgresFuncProofRepository : IFuncProofRepository
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
private const int DefaultCommandTimeoutSeconds = 30;
|
||||
|
||||
public PostgresFuncProofRepository(
|
||||
NpgsqlDataSource dataSource,
|
||||
TimeProvider? timeProvider = null,
|
||||
@@ -135,106 +139,64 @@ public sealed class PostgresFuncProofRepository : IFuncProofRepository
|
||||
|
||||
public async Task<FuncProofDocumentRow?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, scan_id, proof_id, build_id, build_id_type,
|
||||
file_sha256, binary_format, architecture, is_stripped,
|
||||
function_count, trace_count, proof_content, compressed_content,
|
||||
dsse_envelope_id, oci_artifact_digest, rekor_entry_id,
|
||||
generator_version, generated_at_utc, created_at_utc, updated_at_utc
|
||||
FROM scanner.func_proof
|
||||
WHERE id = @id
|
||||
""";
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct);
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("id", id);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, ScannerStorageDefaults.DefaultSchemaName);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
return await reader.ReadAsync(ct) ? MapRow(reader) : null;
|
||||
var entity = await dbContext.FuncProofs
|
||||
.FirstOrDefaultAsync(e => e.Id == id, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return entity is not null ? MapEntityToRow(entity) : null;
|
||||
}
|
||||
|
||||
public async Task<FuncProofDocumentRow?> GetByProofIdAsync(string proofId, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, scan_id, proof_id, build_id, build_id_type,
|
||||
file_sha256, binary_format, architecture, is_stripped,
|
||||
function_count, trace_count, proof_content, compressed_content,
|
||||
dsse_envelope_id, oci_artifact_digest, rekor_entry_id,
|
||||
generator_version, generated_at_utc, created_at_utc, updated_at_utc
|
||||
FROM scanner.func_proof
|
||||
WHERE proof_id = @proof_id
|
||||
""";
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct);
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("proof_id", proofId);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, ScannerStorageDefaults.DefaultSchemaName);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
return await reader.ReadAsync(ct) ? MapRow(reader) : null;
|
||||
var entity = await dbContext.FuncProofs
|
||||
.FirstOrDefaultAsync(e => e.ProofId == proofId, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return entity is not null ? MapEntityToRow(entity) : null;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<FuncProofDocumentRow>> GetByBuildIdAsync(string buildId, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, scan_id, proof_id, build_id, build_id_type,
|
||||
file_sha256, binary_format, architecture, is_stripped,
|
||||
function_count, trace_count, proof_content, compressed_content,
|
||||
dsse_envelope_id, oci_artifact_digest, rekor_entry_id,
|
||||
generator_version, generated_at_utc, created_at_utc, updated_at_utc
|
||||
FROM scanner.func_proof
|
||||
WHERE build_id = @build_id
|
||||
ORDER BY generated_at_utc DESC
|
||||
""";
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct);
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("build_id", buildId);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, ScannerStorageDefaults.DefaultSchemaName);
|
||||
|
||||
var results = new List<FuncProofDocumentRow>();
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
while (await reader.ReadAsync(ct))
|
||||
{
|
||||
results.Add(MapRow(reader));
|
||||
}
|
||||
return results;
|
||||
var entities = await dbContext.FuncProofs
|
||||
.Where(e => e.BuildId == buildId)
|
||||
.OrderByDescending(e => e.GeneratedAtUtc)
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return entities.Select(MapEntityToRow).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<FuncProofDocumentRow>> GetByScanIdAsync(Guid scanId, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, scan_id, proof_id, build_id, build_id_type,
|
||||
file_sha256, binary_format, architecture, is_stripped,
|
||||
function_count, trace_count, proof_content, compressed_content,
|
||||
dsse_envelope_id, oci_artifact_digest, rekor_entry_id,
|
||||
generator_version, generated_at_utc, created_at_utc, updated_at_utc
|
||||
FROM scanner.func_proof
|
||||
WHERE scan_id = @scan_id
|
||||
ORDER BY build_id
|
||||
""";
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct);
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("scan_id", scanId);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, ScannerStorageDefaults.DefaultSchemaName);
|
||||
|
||||
var results = new List<FuncProofDocumentRow>();
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
while (await reader.ReadAsync(ct))
|
||||
{
|
||||
results.Add(MapRow(reader));
|
||||
}
|
||||
return results;
|
||||
var entities = await dbContext.FuncProofs
|
||||
.Where(e => e.ScanId == scanId)
|
||||
.OrderBy(e => e.BuildId)
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return entities.Select(MapEntityToRow).ToList();
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string proofId, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = "SELECT EXISTS(SELECT 1 FROM scanner.func_proof WHERE proof_id = @proof_id)";
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct);
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("proof_id", proofId);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, ScannerStorageDefaults.DefaultSchemaName);
|
||||
|
||||
var result = await cmd.ExecuteScalarAsync(ct);
|
||||
return result is true;
|
||||
return await dbContext.FuncProofs
|
||||
.AnyAsync(e => e.ProofId == proofId, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UpdateSignatureInfoAsync(
|
||||
@@ -266,30 +228,30 @@ public sealed class PostgresFuncProofRepository : IFuncProofRepository
|
||||
await cmd.ExecuteNonQueryAsync(ct);
|
||||
}
|
||||
|
||||
private static FuncProofDocumentRow MapRow(NpgsqlDataReader reader)
|
||||
private static FuncProofDocumentRow MapEntityToRow(FuncProofEntity entity)
|
||||
{
|
||||
return new FuncProofDocumentRow
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
ScanId = reader.GetGuid(1),
|
||||
ProofId = reader.GetString(2),
|
||||
BuildId = reader.GetString(3),
|
||||
BuildIdType = reader.GetString(4),
|
||||
FileSha256 = reader.GetString(5),
|
||||
BinaryFormat = reader.GetString(6),
|
||||
Architecture = reader.GetString(7),
|
||||
IsStripped = reader.GetBoolean(8),
|
||||
FunctionCount = reader.GetInt32(9),
|
||||
TraceCount = reader.GetInt32(10),
|
||||
ProofContent = reader.GetString(11),
|
||||
CompressedContent = reader.IsDBNull(12) ? null : (byte[])reader.GetValue(12),
|
||||
DsseEnvelopeId = reader.IsDBNull(13) ? null : reader.GetString(13),
|
||||
OciArtifactDigest = reader.IsDBNull(14) ? null : reader.GetString(14),
|
||||
RekorEntryId = reader.IsDBNull(15) ? null : reader.GetString(15),
|
||||
GeneratorVersion = reader.GetString(16),
|
||||
GeneratedAtUtc = reader.GetDateTime(17),
|
||||
CreatedAtUtc = reader.GetDateTime(18),
|
||||
UpdatedAtUtc = reader.IsDBNull(19) ? null : reader.GetDateTime(19)
|
||||
Id = entity.Id,
|
||||
ScanId = entity.ScanId,
|
||||
ProofId = entity.ProofId,
|
||||
BuildId = entity.BuildId,
|
||||
BuildIdType = entity.BuildIdType,
|
||||
FileSha256 = entity.FileSha256,
|
||||
BinaryFormat = entity.BinaryFormat,
|
||||
Architecture = entity.Architecture,
|
||||
IsStripped = entity.IsStripped,
|
||||
FunctionCount = entity.FunctionCount,
|
||||
TraceCount = entity.TraceCount,
|
||||
ProofContent = entity.ProofContent,
|
||||
CompressedContent = entity.CompressedContent,
|
||||
DsseEnvelopeId = entity.DsseEnvelopeId,
|
||||
OciArtifactDigest = entity.OciArtifactDigest,
|
||||
RekorEntryId = entity.RekorEntryId,
|
||||
GeneratorVersion = entity.GeneratorVersion,
|
||||
GeneratedAtUtc = entity.GeneratedAtUtc,
|
||||
CreatedAtUtc = entity.CreatedAtUtc,
|
||||
UpdatedAtUtc = entity.UpdatedAtUtc
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
using StellaOps.Scanner.ReachabilityDrift;
|
||||
using StellaOps.Scanner.Storage.EfCore.Models;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
@@ -154,76 +155,47 @@ public sealed class PostgresReachabilityDriftResultRepository : IReachabilityDri
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(headScanId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(language);
|
||||
var tenantScope = ScannerTenantScope.Resolve(tenantId);
|
||||
|
||||
var sql = $"""
|
||||
SELECT id, base_scan_id, head_scan_id, language, detected_at, result_digest
|
||||
FROM {DriftResultsTable}
|
||||
WHERE tenant_id = @p0 AND head_scan_id = @p1 AND language = @p2
|
||||
ORDER BY detected_at DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
var trimmedHead = headScanId.Trim();
|
||||
var trimmedLang = language.Trim();
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantScope.TenantContext, ct).ConfigureAwait(false);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var header = await dbContext.Database.SqlQueryRaw<DriftHeaderRow>(
|
||||
sql, tenantScope.TenantId, headScanId.Trim(), language.Trim())
|
||||
var entity = await dbContext.ReachabilityDriftResults
|
||||
.Include(e => e.DriftedSinks)
|
||||
.Where(e => e.TenantId == tenantScope.TenantId && e.HeadScanId == trimmedHead && e.Language == trimmedLang)
|
||||
.OrderByDescending(e => e.DetectedAt)
|
||||
.FirstOrDefaultAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (header is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await LoadResultAsync(connection, header, tenantScope.TenantId, ct).ConfigureAwait(false);
|
||||
return entity is not null ? MapEntityToResult(entity) : null;
|
||||
}
|
||||
|
||||
public async Task<ReachabilityDriftResult?> TryGetByIdAsync(Guid driftId, CancellationToken ct = default, string? tenantId = null)
|
||||
{
|
||||
var tenantScope = ScannerTenantScope.Resolve(tenantId);
|
||||
var sql = $"""
|
||||
SELECT id, base_scan_id, head_scan_id, language, detected_at, result_digest
|
||||
FROM {DriftResultsTable}
|
||||
WHERE tenant_id = @p0 AND id = @p1
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantScope.TenantContext, ct).ConfigureAwait(false);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var header = await dbContext.Database.SqlQueryRaw<DriftHeaderRow>(
|
||||
sql, tenantScope.TenantId, driftId)
|
||||
.FirstOrDefaultAsync(ct)
|
||||
var entity = await dbContext.ReachabilityDriftResults
|
||||
.Include(e => e.DriftedSinks)
|
||||
.FirstOrDefaultAsync(e => e.TenantId == tenantScope.TenantId && e.Id == driftId, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (header is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await LoadResultAsync(connection, header, tenantScope.TenantId, ct).ConfigureAwait(false);
|
||||
return entity is not null ? MapEntityToResult(entity) : null;
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(Guid driftId, CancellationToken ct = default, string? tenantId = null)
|
||||
{
|
||||
var tenantScope = ScannerTenantScope.Resolve(tenantId);
|
||||
var sql = $"""
|
||||
SELECT CAST(1 AS integer) AS "Value"
|
||||
FROM {DriftResultsTable}
|
||||
WHERE tenant_id = @p0 AND id = @p1
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantScope.TenantContext, ct).ConfigureAwait(false);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var result = await dbContext.Database.SqlQueryRaw<int>(
|
||||
sql, tenantScope.TenantId, driftId)
|
||||
.FirstOrDefaultAsync(ct)
|
||||
return await dbContext.ReachabilityDriftResults
|
||||
.AnyAsync(e => e.TenantId == tenantScope.TenantId && e.Id == driftId, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return result != 0;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DriftedSink>> ListSinksAsync(
|
||||
@@ -244,37 +216,20 @@ public sealed class PostgresReachabilityDriftResultRepository : IReachabilityDri
|
||||
throw new ArgumentOutOfRangeException(nameof(limit));
|
||||
}
|
||||
var tenantScope = ScannerTenantScope.Resolve(tenantId);
|
||||
|
||||
var sql = $"""
|
||||
SELECT
|
||||
id,
|
||||
sink_node_id,
|
||||
symbol,
|
||||
sink_category,
|
||||
direction,
|
||||
cause_kind,
|
||||
cause_description,
|
||||
cause_symbol,
|
||||
cause_file,
|
||||
cause_line,
|
||||
code_change_id,
|
||||
compressed_path,
|
||||
associated_vulns
|
||||
FROM {DriftedSinksTable}
|
||||
WHERE tenant_id = @p0 AND drift_result_id = @p1 AND direction = @p2
|
||||
ORDER BY sink_node_id ASC
|
||||
OFFSET @p3 LIMIT @p4
|
||||
""";
|
||||
var directionValue = ToDbValue(direction);
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantScope.TenantContext, ct).ConfigureAwait(false);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var rows = await dbContext.Database.SqlQueryRaw<DriftSinkRow>(
|
||||
sql, tenantScope.TenantId, driftId, ToDbValue(direction), offset, limit)
|
||||
var entities = await dbContext.DriftedSinks
|
||||
.Where(e => e.TenantId == tenantScope.TenantId && e.DriftResultId == driftId && e.Direction == directionValue)
|
||||
.OrderBy(e => e.SinkNodeId)
|
||||
.Skip(offset)
|
||||
.Take(limit)
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return rows.Select(r => r.ToModel(direction)).ToList();
|
||||
return entities.Select(e => MapSinkEntityToModel(e, direction)).ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<SinkInsertParams> EnumerateSinkParams(
|
||||
@@ -315,61 +270,71 @@ public sealed class PostgresReachabilityDriftResultRepository : IReachabilityDri
|
||||
string CauseKind, string CauseDescription, string? CauseSymbol, string? CauseFile,
|
||||
int? CauseLine, Guid? CodeChangeId, string CompressedPath, string? AssociatedVulns);
|
||||
|
||||
private async Task<ReachabilityDriftResult> LoadResultAsync(
|
||||
NpgsqlConnection connection,
|
||||
DriftHeaderRow header,
|
||||
Guid tenantId,
|
||||
CancellationToken ct)
|
||||
private static ReachabilityDriftResult MapEntityToResult(ReachabilityDriftResultEntity entity)
|
||||
{
|
||||
var sinksSql = $"""
|
||||
SELECT
|
||||
id,
|
||||
sink_node_id,
|
||||
symbol,
|
||||
sink_category,
|
||||
direction,
|
||||
cause_kind,
|
||||
cause_description,
|
||||
cause_symbol,
|
||||
cause_file,
|
||||
cause_line,
|
||||
code_change_id,
|
||||
compressed_path,
|
||||
associated_vulns
|
||||
FROM {DriftedSinksTable}
|
||||
WHERE tenant_id = @p0 AND drift_result_id = @p1
|
||||
ORDER BY direction ASC, sink_node_id ASC
|
||||
""";
|
||||
var sinks = entity.DriftedSinks ?? [];
|
||||
var reachableDirection = ToDbValue(DriftDirection.BecameReachable);
|
||||
var unreachableDirection = ToDbValue(DriftDirection.BecameUnreachable);
|
||||
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var rows = await dbContext.Database.SqlQueryRaw<DriftSinkRow>(
|
||||
sinksSql, tenantId, header.id)
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var reachable = rows
|
||||
.Where(r => string.Equals(r.direction, ToDbValue(DriftDirection.BecameReachable), StringComparison.Ordinal))
|
||||
.Select(r => r.ToModel(DriftDirection.BecameReachable))
|
||||
var reachable = sinks
|
||||
.Where(s => string.Equals(s.Direction, reachableDirection, StringComparison.Ordinal))
|
||||
.Select(s => MapSinkEntityToModel(s, DriftDirection.BecameReachable))
|
||||
.OrderBy(s => s.SinkNodeId, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
var unreachable = rows
|
||||
.Where(r => string.Equals(r.direction, ToDbValue(DriftDirection.BecameUnreachable), StringComparison.Ordinal))
|
||||
.Select(r => r.ToModel(DriftDirection.BecameUnreachable))
|
||||
var unreachable = sinks
|
||||
.Where(s => string.Equals(s.Direction, unreachableDirection, StringComparison.Ordinal))
|
||||
.Select(s => MapSinkEntityToModel(s, DriftDirection.BecameUnreachable))
|
||||
.OrderBy(s => s.SinkNodeId, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return new ReachabilityDriftResult
|
||||
{
|
||||
Id = header.id,
|
||||
BaseScanId = header.base_scan_id,
|
||||
HeadScanId = header.head_scan_id,
|
||||
Language = header.language,
|
||||
DetectedAt = header.detected_at,
|
||||
Id = entity.Id,
|
||||
BaseScanId = entity.BaseScanId,
|
||||
HeadScanId = entity.HeadScanId,
|
||||
Language = entity.Language,
|
||||
DetectedAt = entity.DetectedAt,
|
||||
NewlyReachable = reachable,
|
||||
NewlyUnreachable = unreachable,
|
||||
ResultDigest = header.result_digest
|
||||
ResultDigest = entity.ResultDigest
|
||||
};
|
||||
}
|
||||
|
||||
private static DriftedSink MapSinkEntityToModel(DriftedSinkEntity entity, DriftDirection direction)
|
||||
{
|
||||
var path = JsonSerializer.Deserialize<CompressedPath>(entity.CompressedPath, JsonOptions)
|
||||
?? new CompressedPath
|
||||
{
|
||||
Entrypoint = new PathNode { NodeId = string.Empty, Symbol = string.Empty },
|
||||
Sink = new PathNode { NodeId = string.Empty, Symbol = string.Empty },
|
||||
IntermediateCount = 0,
|
||||
KeyNodes = ImmutableArray<PathNode>.Empty
|
||||
};
|
||||
|
||||
var vulns = string.IsNullOrWhiteSpace(entity.AssociatedVulns)
|
||||
? ImmutableArray<AssociatedVuln>.Empty
|
||||
: (JsonSerializer.Deserialize<AssociatedVuln[]>(entity.AssociatedVulns!, JsonOptions) ?? [])
|
||||
.ToImmutableArray();
|
||||
|
||||
return new DriftedSink
|
||||
{
|
||||
Id = entity.Id,
|
||||
SinkNodeId = entity.SinkNodeId,
|
||||
Symbol = entity.Symbol,
|
||||
SinkCategory = ParseSinkCategory(entity.SinkCategory),
|
||||
Direction = direction,
|
||||
Cause = new DriftCause
|
||||
{
|
||||
Kind = ParseCauseKind(entity.CauseKind),
|
||||
Description = entity.CauseDescription,
|
||||
ChangedSymbol = entity.CauseSymbol,
|
||||
ChangedFile = entity.CauseFile,
|
||||
ChangedLine = entity.CauseLine,
|
||||
CodeChangeId = entity.CodeChangeId
|
||||
},
|
||||
Path = path,
|
||||
AssociatedVulns = vulns
|
||||
};
|
||||
}
|
||||
|
||||
@@ -438,67 +403,4 @@ public sealed class PostgresReachabilityDriftResultRepository : IReachabilityDri
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class DriftHeaderRow
|
||||
{
|
||||
public Guid id { get; init; }
|
||||
public string base_scan_id { get; init; } = string.Empty;
|
||||
public string head_scan_id { get; init; } = string.Empty;
|
||||
public string language { get; init; } = string.Empty;
|
||||
public DateTimeOffset detected_at { get; init; }
|
||||
public string result_digest { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class DriftSinkRow
|
||||
{
|
||||
public Guid id { get; init; }
|
||||
public string sink_node_id { get; init; } = string.Empty;
|
||||
public string symbol { get; init; } = string.Empty;
|
||||
public string sink_category { get; init; } = string.Empty;
|
||||
public string direction { get; init; } = string.Empty;
|
||||
public string cause_kind { get; init; } = string.Empty;
|
||||
public string cause_description { get; init; } = string.Empty;
|
||||
public string? cause_symbol { get; init; }
|
||||
public string? cause_file { get; init; }
|
||||
public int? cause_line { get; init; }
|
||||
public Guid? code_change_id { get; init; }
|
||||
public string compressed_path { get; init; } = "{}";
|
||||
public string? associated_vulns { get; init; }
|
||||
|
||||
public DriftedSink ToModel(DriftDirection direction)
|
||||
{
|
||||
var path = JsonSerializer.Deserialize<CompressedPath>(compressed_path, JsonOptions)
|
||||
?? new CompressedPath
|
||||
{
|
||||
Entrypoint = new PathNode { NodeId = string.Empty, Symbol = string.Empty },
|
||||
Sink = new PathNode { NodeId = string.Empty, Symbol = string.Empty },
|
||||
IntermediateCount = 0,
|
||||
KeyNodes = ImmutableArray<PathNode>.Empty
|
||||
};
|
||||
|
||||
var vulns = string.IsNullOrWhiteSpace(associated_vulns)
|
||||
? ImmutableArray<AssociatedVuln>.Empty
|
||||
: (JsonSerializer.Deserialize<AssociatedVuln[]>(associated_vulns!, JsonOptions) ?? [])
|
||||
.ToImmutableArray();
|
||||
|
||||
return new DriftedSink
|
||||
{
|
||||
Id = id,
|
||||
SinkNodeId = sink_node_id,
|
||||
Symbol = symbol,
|
||||
SinkCategory = ParseSinkCategory(sink_category),
|
||||
Direction = direction,
|
||||
Cause = new DriftCause
|
||||
{
|
||||
Kind = ParseCauseKind(cause_kind),
|
||||
Description = cause_description,
|
||||
ChangedSymbol = cause_symbol,
|
||||
ChangedFile = cause_file,
|
||||
ChangedLine = cause_line,
|
||||
CodeChangeId = code_change_id
|
||||
},
|
||||
Path = path,
|
||||
AssociatedVulns = vulns
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,15 +67,20 @@ public sealed class PostgresScanManifestRepository : IScanManifestRepository
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var result = await dbContext.Database.SqlQueryRaw<ManifestInsertResult>(
|
||||
// INSERT...RETURNING is non-composable SQL; EF Core's SqlQueryRaw cannot wrap
|
||||
// it in a subquery (which .FirstAsync would require). Materialize the full
|
||||
// result set first, then pick the single returned row in-memory.
|
||||
var results = await dbContext.Database.SqlQueryRaw<ManifestInsertResult>(
|
||||
sql,
|
||||
manifest.ScanId, manifest.ManifestHash, manifest.SbomHash, manifest.RulesHash,
|
||||
manifest.FeedHash, manifest.PolicyHash, manifest.ScanStartedAt,
|
||||
(object?)manifest.ScanCompletedAt ?? DBNull.Value,
|
||||
manifest.ManifestContent, manifest.ScannerVersion)
|
||||
.FirstAsync(cancellationToken)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var result = results.First();
|
||||
|
||||
manifest.ManifestId = result.manifest_id;
|
||||
manifest.CreatedAt = result.created_at;
|
||||
return manifest;
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Scanner.SmartDiff.Detection;
|
||||
using StellaOps.Scanner.Storage.EfCore.Models;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -66,55 +67,34 @@ public sealed class PostgresVexCandidateStore : IVexCandidateStore
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(imageDigest);
|
||||
var tenantScope = ScannerTenantScope.Resolve(tenantId);
|
||||
|
||||
var sql = $"""
|
||||
SELECT
|
||||
candidate_id, vuln_id, purl, image_digest,
|
||||
suggested_status::TEXT AS suggested_status, justification::TEXT AS justification, rationale,
|
||||
evidence_links, confidence, generated_at, expires_at,
|
||||
requires_review, review_action::TEXT AS review_action, reviewed_by, reviewed_at, review_comment
|
||||
FROM {VexCandidatesTable}
|
||||
WHERE tenant_id = @p0
|
||||
AND image_digest = @p1
|
||||
ORDER BY confidence DESC
|
||||
""";
|
||||
var trimmedDigest = imageDigest.Trim();
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantScope.TenantContext, ct).ConfigureAwait(false);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var rows = await dbContext.Database.SqlQueryRaw<VexCandidateRow>(
|
||||
sql, tenantScope.TenantId, imageDigest.Trim())
|
||||
var entities = await dbContext.VexCandidates
|
||||
.Where(e => e.TenantId == tenantScope.TenantId && e.ImageDigest == trimmedDigest)
|
||||
.OrderByDescending(e => e.Confidence)
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return rows.Select(r => r.ToCandidate()).ToList();
|
||||
return entities.Select(MapEntityToCandidate).ToList();
|
||||
}
|
||||
|
||||
public async Task<VexCandidate?> GetCandidateAsync(string candidateId, CancellationToken ct = default, string? tenantId = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(candidateId);
|
||||
var tenantScope = ScannerTenantScope.Resolve(tenantId);
|
||||
|
||||
var sql = $"""
|
||||
SELECT
|
||||
candidate_id, vuln_id, purl, image_digest,
|
||||
suggested_status::TEXT AS suggested_status, justification::TEXT AS justification, rationale,
|
||||
evidence_links, confidence, generated_at, expires_at,
|
||||
requires_review, review_action::TEXT AS review_action, reviewed_by, reviewed_at, review_comment
|
||||
FROM {VexCandidatesTable}
|
||||
WHERE tenant_id = @p0
|
||||
AND candidate_id = @p1
|
||||
""";
|
||||
var trimmedId = candidateId.Trim();
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantScope.TenantContext, ct).ConfigureAwait(false);
|
||||
await using var dbContext = ScannerDbContextFactory.Create(connection, _dataSource.CommandTimeoutSeconds, SchemaName);
|
||||
|
||||
var row = await dbContext.Database.SqlQueryRaw<VexCandidateRow>(
|
||||
sql, tenantScope.TenantId, candidateId.Trim())
|
||||
.FirstOrDefaultAsync(ct)
|
||||
var entity = await dbContext.VexCandidates
|
||||
.FirstOrDefaultAsync(e => e.TenantId == tenantScope.TenantId && e.CandidateId == trimmedId, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return row?.ToCandidate();
|
||||
return entity is not null ? MapEntityToCandidate(entity) : null;
|
||||
}
|
||||
|
||||
public async Task<bool> ReviewCandidateAsync(string candidateId, VexCandidateReview review, CancellationToken ct = default, string? tenantId = null)
|
||||
@@ -232,70 +212,47 @@ public sealed class PostgresVexCandidateStore : IVexCandidateStore
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Row mapping class for EF Core SqlQueryRaw.
|
||||
/// </summary>
|
||||
private sealed class VexCandidateRow
|
||||
private static VexCandidate MapEntityToCandidate(VexCandidateEntity entity)
|
||||
{
|
||||
public string candidate_id { get; set; } = "";
|
||||
public string vuln_id { get; set; } = "";
|
||||
public string purl { get; set; } = "";
|
||||
public string image_digest { get; set; } = "";
|
||||
public string suggested_status { get; set; } = "not_affected";
|
||||
public string justification { get; set; } = "vulnerable_code_not_present";
|
||||
public string rationale { get; set; } = "";
|
||||
public string evidence_links { get; set; } = "[]";
|
||||
public decimal confidence { get; set; }
|
||||
public DateTimeOffset generated_at { get; set; }
|
||||
public DateTimeOffset expires_at { get; set; }
|
||||
public bool requires_review { get; set; }
|
||||
public string? review_action { get; set; }
|
||||
public string? reviewed_by { get; set; }
|
||||
public DateTimeOffset? reviewed_at { get; set; }
|
||||
public string? review_comment { get; set; }
|
||||
var links = JsonSerializer.Deserialize<List<EvidenceLink>>(entity.EvidenceLinks, JsonOptions)
|
||||
?? [];
|
||||
|
||||
public VexCandidate ToCandidate()
|
||||
return new VexCandidate(
|
||||
CandidateId: entity.CandidateId,
|
||||
FindingKey: new FindingKey(entity.VulnId, entity.Purl),
|
||||
SuggestedStatus: ParseVexStatus(entity.SuggestedStatus),
|
||||
Justification: ParseJustification(entity.Justification),
|
||||
Rationale: entity.Rationale,
|
||||
EvidenceLinks: [.. links],
|
||||
Confidence: (double)entity.Confidence,
|
||||
ImageDigest: entity.ImageDigest,
|
||||
GeneratedAt: entity.GeneratedAt,
|
||||
ExpiresAt: entity.ExpiresAt,
|
||||
RequiresReview: entity.RequiresReview);
|
||||
}
|
||||
|
||||
private static VexStatusType ParseVexStatus(string value)
|
||||
{
|
||||
return value.ToLowerInvariant() switch
|
||||
{
|
||||
var links = JsonSerializer.Deserialize<List<EvidenceLink>>(evidence_links, JsonOptions)
|
||||
?? [];
|
||||
"affected" => VexStatusType.Affected,
|
||||
"not_affected" => VexStatusType.NotAffected,
|
||||
"fixed" => VexStatusType.Fixed,
|
||||
"under_investigation" => VexStatusType.UnderInvestigation,
|
||||
_ => VexStatusType.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
return new VexCandidate(
|
||||
CandidateId: candidate_id,
|
||||
FindingKey: new FindingKey(vuln_id, purl),
|
||||
SuggestedStatus: ParseVexStatus(suggested_status),
|
||||
Justification: ParseJustification(justification),
|
||||
Rationale: rationale,
|
||||
EvidenceLinks: [.. links],
|
||||
Confidence: (double)confidence,
|
||||
ImageDigest: image_digest,
|
||||
GeneratedAt: generated_at,
|
||||
ExpiresAt: expires_at,
|
||||
RequiresReview: requires_review);
|
||||
}
|
||||
|
||||
private static VexStatusType ParseVexStatus(string value)
|
||||
private static VexJustification ParseJustification(string value)
|
||||
{
|
||||
return value.ToLowerInvariant() switch
|
||||
{
|
||||
return value.ToLowerInvariant() switch
|
||||
{
|
||||
"affected" => VexStatusType.Affected,
|
||||
"not_affected" => VexStatusType.NotAffected,
|
||||
"fixed" => VexStatusType.Fixed,
|
||||
"under_investigation" => VexStatusType.UnderInvestigation,
|
||||
_ => VexStatusType.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private static VexJustification ParseJustification(string value)
|
||||
{
|
||||
return value.ToLowerInvariant() switch
|
||||
{
|
||||
"component_not_present" => VexJustification.ComponentNotPresent,
|
||||
"vulnerable_code_not_present" => VexJustification.VulnerableCodeNotPresent,
|
||||
"vulnerable_code_not_in_execute_path" => VexJustification.VulnerableCodeNotInExecutePath,
|
||||
"vulnerable_code_cannot_be_controlled_by_adversary" => VexJustification.VulnerableCodeCannotBeControlledByAdversary,
|
||||
"inline_mitigations_already_exist" => VexJustification.InlineMitigationsAlreadyExist,
|
||||
_ => VexJustification.VulnerableCodeNotPresent
|
||||
};
|
||||
}
|
||||
"component_not_present" => VexJustification.ComponentNotPresent,
|
||||
"vulnerable_code_not_present" => VexJustification.VulnerableCodeNotPresent,
|
||||
"vulnerable_code_not_in_execute_path" => VexJustification.VulnerableCodeNotInExecutePath,
|
||||
"vulnerable_code_cannot_be_controlled_by_adversary" => VexJustification.VulnerableCodeCannotBeControlledByAdversary,
|
||||
"inline_mitigations_already_exist" => VexJustification.InlineMitigationsAlreadyExist,
|
||||
_ => VexJustification.VulnerableCodeNotPresent
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user