save progress
This commit is contained in:
@@ -50,7 +50,7 @@ public class AuditLogEntity
|
||||
/// Additional details about the operation.
|
||||
/// </summary>
|
||||
[Column("details", TypeName = "jsonb")]
|
||||
public JsonDocument? Details { get; set; }
|
||||
public JsonElement? Details { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this log entry was created.
|
||||
|
||||
@@ -53,7 +53,7 @@ public class RekorEntryEntity
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Column("inclusion_proof", TypeName = "jsonb")]
|
||||
public JsonDocument InclusionProof { get; set; } = null!;
|
||||
public JsonElement InclusionProof { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this record was created.
|
||||
|
||||
@@ -16,7 +16,7 @@ function Resolve-RepoRoot {
|
||||
|
||||
$repoRoot = Resolve-RepoRoot
|
||||
$perfDir = Join-Path $repoRoot "src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Perf"
|
||||
$migrationFile = Join-Path $repoRoot "src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Migrations/20251214000001_AddProofChainSchema.sql"
|
||||
$migrationFile = Join-Path $repoRoot "src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Migrations/001_initial_schema.sql"
|
||||
$seedFile = Join-Path $perfDir "seed.sql"
|
||||
$queriesFile = Join-Path $perfDir "queries.sql"
|
||||
$reportFile = Join-Path $repoRoot "docs/db/reports/proofchain-schema-perf-2025-12-17.md"
|
||||
|
||||
@@ -57,6 +57,9 @@ public class ProofChainDbContext : DbContext
|
||||
entity.HasIndex(e => e.Purl).HasDatabaseName("idx_sbom_entries_purl");
|
||||
entity.HasIndex(e => e.ArtifactDigest).HasDatabaseName("idx_sbom_entries_artifact");
|
||||
entity.HasIndex(e => e.TrustAnchorId).HasDatabaseName("idx_sbom_entries_anchor");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
// Unique constraint
|
||||
entity.HasIndex(e => new { e.BomDigest, e.Purl, e.Version })
|
||||
@@ -87,6 +90,9 @@ public class ProofChainDbContext : DbContext
|
||||
.HasDatabaseName("idx_dsse_entry_predicate");
|
||||
entity.HasIndex(e => e.SignerKeyId).HasDatabaseName("idx_dsse_signer");
|
||||
entity.HasIndex(e => e.BodyHash).HasDatabaseName("idx_dsse_body_hash");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
// Unique constraint
|
||||
entity.HasIndex(e => new { e.EntryId, e.PredicateType, e.BodyHash })
|
||||
@@ -100,6 +106,9 @@ public class ProofChainDbContext : DbContext
|
||||
entity.HasIndex(e => e.BundleId).HasDatabaseName("idx_spines_bundle").IsUnique();
|
||||
entity.HasIndex(e => e.AnchorId).HasDatabaseName("idx_spines_anchor");
|
||||
entity.HasIndex(e => e.PolicyVersion).HasDatabaseName("idx_spines_policy");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
entity.HasOne(e => e.Anchor)
|
||||
.WithMany()
|
||||
@@ -114,6 +123,12 @@ public class ProofChainDbContext : DbContext
|
||||
entity.HasIndex(e => e.IsActive)
|
||||
.HasDatabaseName("idx_trust_anchors_active")
|
||||
.HasFilter("is_active = TRUE");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.ValueGeneratedOnAdd();
|
||||
entity.Property(e => e.UpdatedAt)
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.ValueGeneratedOnAddOrUpdate();
|
||||
});
|
||||
|
||||
// RekorEntryEntity configuration
|
||||
@@ -123,6 +138,9 @@ public class ProofChainDbContext : DbContext
|
||||
entity.HasIndex(e => e.LogId).HasDatabaseName("idx_rekor_log_id");
|
||||
entity.HasIndex(e => e.Uuid).HasDatabaseName("idx_rekor_uuid");
|
||||
entity.HasIndex(e => e.EnvId).HasDatabaseName("idx_rekor_env");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
entity.HasOne(e => e.Envelope)
|
||||
.WithOne(e => e.RekorEntry)
|
||||
@@ -138,6 +156,70 @@ public class ProofChainDbContext : DbContext
|
||||
entity.HasIndex(e => e.CreatedAt)
|
||||
.HasDatabaseName("idx_audit_created")
|
||||
.IsDescending();
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.ValueGeneratedOnAdd();
|
||||
});
|
||||
}
|
||||
|
||||
public override int SaveChanges()
|
||||
{
|
||||
NormalizeTrackedArrays();
|
||||
return base.SaveChanges();
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
NormalizeTrackedArrays();
|
||||
return base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private void NormalizeTrackedArrays()
|
||||
{
|
||||
foreach (var entry in ChangeTracker.Entries<SpineEntity>())
|
||||
{
|
||||
if (entry.State is EntityState.Added or EntityState.Modified)
|
||||
{
|
||||
entry.Entity.EvidenceIds = NormalizeEvidenceIds(entry.Entity.EvidenceIds);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entry in ChangeTracker.Entries<TrustAnchorEntity>())
|
||||
{
|
||||
if (entry.State is EntityState.Added or EntityState.Modified)
|
||||
{
|
||||
entry.Entity.AllowedKeyIds = NormalizeKeyIds(entry.Entity.AllowedKeyIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] NormalizeEvidenceIds(string[] evidenceIds)
|
||||
{
|
||||
if (evidenceIds.Length == 0)
|
||||
{
|
||||
return evidenceIds;
|
||||
}
|
||||
|
||||
return evidenceIds
|
||||
.Select(id => id.Trim())
|
||||
.Where(id => !string.IsNullOrEmpty(id))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static string[] NormalizeKeyIds(string[] keyIds)
|
||||
{
|
||||
if (keyIds.Length == 0)
|
||||
{
|
||||
return keyIds;
|
||||
}
|
||||
|
||||
return keyIds
|
||||
.Select(id => id.Trim())
|
||||
.Where(id => !string.IsNullOrEmpty(id))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ public sealed class TrustAnchorMatcher : ITrustAnchorMatcher
|
||||
private readonly ILogger<TrustAnchorMatcher> _logger;
|
||||
|
||||
// Cache compiled regex patterns
|
||||
private readonly Dictionary<string, Regex> _patternCache = new();
|
||||
private const int MaxRegexCacheSize = 1024;
|
||||
private readonly Dictionary<string, Regex> _patternCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Lock _cacheLock = new();
|
||||
|
||||
public TrustAnchorMatcher(
|
||||
@@ -92,7 +93,7 @@ public sealed class TrustAnchorMatcher : ITrustAnchorMatcher
|
||||
{
|
||||
var specificity = CalculateSpecificity(anchor.PurlPattern);
|
||||
|
||||
if (bestMatch == null || specificity > bestMatch.Specificity)
|
||||
if (IsBetterMatch(anchor, specificity, bestMatch))
|
||||
{
|
||||
bestMatch = new TrustAnchorMatchResult
|
||||
{
|
||||
@@ -190,6 +191,11 @@ public sealed class TrustAnchorMatcher : ITrustAnchorMatcher
|
||||
var regexPattern = ConvertGlobToRegex(pattern);
|
||||
var regex = new Regex(regexPattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
if (_patternCache.Count >= MaxRegexCacheSize)
|
||||
{
|
||||
_patternCache.Clear();
|
||||
}
|
||||
|
||||
_patternCache[pattern] = regex;
|
||||
return regex;
|
||||
}
|
||||
@@ -284,4 +290,36 @@ public sealed class TrustAnchorMatcher : ITrustAnchorMatcher
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsBetterMatch(
|
||||
TrustAnchorEntity candidate,
|
||||
int specificity,
|
||||
TrustAnchorMatchResult? bestMatch)
|
||||
{
|
||||
if (bestMatch == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (specificity != bestMatch.Specificity)
|
||||
{
|
||||
return specificity > bestMatch.Specificity;
|
||||
}
|
||||
|
||||
var candidatePattern = candidate.PurlPattern ?? string.Empty;
|
||||
var bestPattern = bestMatch.MatchedPattern ?? string.Empty;
|
||||
|
||||
if (candidatePattern.Length != bestPattern.Length)
|
||||
{
|
||||
return candidatePattern.Length > bestPattern.Length;
|
||||
}
|
||||
|
||||
var patternCompare = string.Compare(candidatePattern, bestPattern, StringComparison.OrdinalIgnoreCase);
|
||||
if (patternCompare != 0)
|
||||
{
|
||||
return patternCompare < 0;
|
||||
}
|
||||
|
||||
return candidate.AnchorId.CompareTo(bestMatch.Anchor.AnchorId) < 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<LangVersion>preview</LangVersion>
|
||||
<RootNamespace>StellaOps.Attestor.Persistence</RootNamespace>
|
||||
<Description>Proof chain persistence layer with Entity Framework Core and PostgreSQL support.</Description>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0060-M | DONE | Maintainability audit for StellaOps.Attestor.Persistence. |
|
||||
| AUDIT-0060-T | DONE | Test coverage audit for StellaOps.Attestor.Persistence. |
|
||||
| AUDIT-0060-A | TODO | Pending approval for changes. |
|
||||
| AUDIT-0060-A | DONE | Applied defaults, normalization, deterministic matching, perf script, tests. |
|
||||
|
||||
Reference in New Issue
Block a user