save checkpoint: save features
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -687,6 +687,30 @@ public sealed class PostgresVexObservationStore : RepositoryBase<ExcititorDataSo
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_provider ON vex.observations(tenant, provider_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_created_at ON vex.observations(tenant, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_statements ON vex.observations USING GIN (statements);
|
||||
|
||||
ALTER TABLE IF EXISTS vex.observations
|
||||
ADD COLUMN IF NOT EXISTS rekor_uuid TEXT,
|
||||
ADD COLUMN IF NOT EXISTS rekor_log_index BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS rekor_integrated_time TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS rekor_log_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS rekor_tree_root TEXT,
|
||||
ADD COLUMN IF NOT EXISTS rekor_tree_size BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS rekor_inclusion_proof JSONB,
|
||||
ADD COLUMN IF NOT EXISTS rekor_entry_body_hash TEXT,
|
||||
ADD COLUMN IF NOT EXISTS rekor_entry_kind TEXT,
|
||||
ADD COLUMN IF NOT EXISTS rekor_linked_at TIMESTAMPTZ;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_rekor_uuid
|
||||
ON vex.observations(rekor_uuid)
|
||||
WHERE rekor_uuid IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_rekor_log_index
|
||||
ON vex.observations(rekor_log_index DESC)
|
||||
WHERE rekor_log_index IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_pending_rekor
|
||||
ON vex.observations(created_at)
|
||||
WHERE rekor_uuid IS NULL;
|
||||
""";
|
||||
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
@@ -719,7 +743,7 @@ public sealed class PostgresVexObservationStore : RepositoryBase<ExcititorDataSo
|
||||
await using var connection = await DataSource.OpenConnectionAsync("public", "writer", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = """
|
||||
UPDATE excititor.vex_observations SET
|
||||
UPDATE vex.observations SET
|
||||
rekor_uuid = @rekor_uuid,
|
||||
rekor_log_index = @rekor_log_index,
|
||||
rekor_integrated_time = @rekor_integrated_time,
|
||||
@@ -772,7 +796,7 @@ public sealed class PostgresVexObservationStore : RepositoryBase<ExcititorDataSo
|
||||
const string sql = """
|
||||
SELECT observation_id, tenant, provider_id, stream_id, upstream, statements,
|
||||
content, linkset, created_at, supersedes, attributes
|
||||
FROM excititor.vex_observations
|
||||
FROM vex.observations
|
||||
WHERE tenant = @tenant AND rekor_uuid IS NULL
|
||||
ORDER BY created_at ASC
|
||||
LIMIT @limit
|
||||
@@ -813,7 +837,7 @@ public sealed class PostgresVexObservationStore : RepositoryBase<ExcititorDataSo
|
||||
SELECT observation_id, tenant, provider_id, stream_id, upstream, statements,
|
||||
content, linkset, created_at, supersedes, attributes,
|
||||
rekor_uuid, rekor_log_index, rekor_integrated_time, rekor_log_url, rekor_inclusion_proof
|
||||
FROM excititor.vex_observations
|
||||
FROM vex.observations
|
||||
WHERE tenant = @tenant AND rekor_uuid = @rekor_uuid
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
@@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0323-T | DONE | Revalidated 2026-01-07; test coverage audit for Excititor.Persistence. |
|
||||
| AUDIT-0323-A | TODO | Pending approval (non-test project; revalidated 2026-01-07). |
|
||||
| VEX-LINK-STORE-0001 | DONE | SPRINT_20260113_003_001 - Evidence link migration added. |
|
||||
| QA-DEVOPS-VERIFY-002-F | DONE | 2026-02-11: Fixed Rekor-linkage schema mismatch in `PostgresVexObservationStore` by aligning to `vex.observations` and ensuring Rekor linkage columns/indexes. |
|
||||
|
||||
@@ -187,9 +187,123 @@ public sealed class PostgresVexObservationStoreTests : IAsyncLifetime
|
||||
count.Should().Be(3);
|
||||
}
|
||||
|
||||
private VexObservation CreateObservation(string observationId, string providerId, string vulnId, string productKey)
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateRekorLinkageAsync_RoundTripsLinkageAndLookupByUuid()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
// Arrange
|
||||
var observation = CreateObservation(
|
||||
"obs-rekor-1",
|
||||
"provider-a",
|
||||
"CVE-REKOR-1",
|
||||
"pkg:rekor/one@1.0.0",
|
||||
createdAt: new DateTimeOffset(2026, 1, 17, 8, 0, 0, TimeSpan.Zero));
|
||||
await _store.InsertAsync(observation, CancellationToken.None);
|
||||
|
||||
var linkage = new RekorLinkage
|
||||
{
|
||||
Uuid = "rekor-uuid-obs-rekor-1",
|
||||
LogIndex = 4242,
|
||||
IntegratedTime = new DateTimeOffset(2026, 1, 17, 8, 5, 0, TimeSpan.Zero),
|
||||
LogUrl = "https://rekor.local.test",
|
||||
TreeRoot = "tree-root-001",
|
||||
TreeSize = 9001,
|
||||
EntryBodyHash = "sha256:entry-body-001",
|
||||
EntryKind = "dsse",
|
||||
InclusionProof = new VexInclusionProof
|
||||
{
|
||||
LeafIndex = 4242,
|
||||
TreeSize = 9001,
|
||||
Hashes = ["hash-a", "hash-b"],
|
||||
RootHash = "root-hash-001"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var updated = await _store.UpdateRekorLinkageAsync(
|
||||
_tenantId,
|
||||
"obs-rekor-1",
|
||||
linkage,
|
||||
CancellationToken.None);
|
||||
var fetched = await _store.GetByRekorUuidAsync(
|
||||
_tenantId,
|
||||
"rekor-uuid-obs-rekor-1",
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
updated.Should().BeTrue();
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.ObservationId.Should().Be("obs-rekor-1");
|
||||
fetched.HasRekorLinkage.Should().BeTrue();
|
||||
fetched.RekorUuid.Should().Be("rekor-uuid-obs-rekor-1");
|
||||
fetched.RekorLogIndex.Should().Be(4242);
|
||||
fetched.RekorLogUrl.Should().Be("https://rekor.local.test");
|
||||
fetched.RekorIntegratedTime.Should().Be(new DateTimeOffset(2026, 1, 17, 8, 5, 0, TimeSpan.Zero));
|
||||
fetched.RekorInclusionProof.Should().NotBeNull();
|
||||
fetched.RekorInclusionProof!.LeafIndex.Should().Be(4242);
|
||||
fetched.RekorInclusionProof.TreeSize.Should().Be(9001);
|
||||
fetched.RekorInclusionProof.Hashes.Should().Equal("hash-a", "hash-b");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateRekorLinkageAsync_ReturnsFalseForUnknownObservation()
|
||||
{
|
||||
// Arrange
|
||||
var linkage = new RekorLinkage
|
||||
{
|
||||
Uuid = "missing-observation-rekor-uuid",
|
||||
LogIndex = 1,
|
||||
IntegratedTime = new DateTimeOffset(2026, 1, 17, 9, 0, 0, TimeSpan.Zero)
|
||||
};
|
||||
|
||||
// Act
|
||||
var updated = await _store.UpdateRekorLinkageAsync(
|
||||
_tenantId,
|
||||
"missing-observation-id",
|
||||
linkage,
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
updated.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetPendingRekorAttestationAsync_ReturnsOnlyUnlinkedObservationsOrderedByCreatedAt()
|
||||
{
|
||||
// Arrange
|
||||
var start = new DateTimeOffset(2026, 1, 17, 10, 0, 0, TimeSpan.Zero);
|
||||
await _store.InsertAsync(CreateObservation("obs-pending-1", "provider-a", "CVE-PEND-1", "pkg:pending/one@1.0.0", start), CancellationToken.None);
|
||||
await _store.InsertAsync(CreateObservation("obs-pending-2", "provider-a", "CVE-PEND-2", "pkg:pending/two@1.0.0", start.AddMinutes(1)), CancellationToken.None);
|
||||
await _store.InsertAsync(CreateObservation("obs-pending-3", "provider-a", "CVE-PEND-3", "pkg:pending/three@1.0.0", start.AddMinutes(2)), CancellationToken.None);
|
||||
|
||||
var linkage = new RekorLinkage
|
||||
{
|
||||
Uuid = "rekor-uuid-linked-obs-pending-2",
|
||||
LogIndex = 2002,
|
||||
IntegratedTime = start.AddMinutes(3)
|
||||
};
|
||||
await _store.UpdateRekorLinkageAsync(_tenantId, "obs-pending-2", linkage, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var pending = await _store.GetPendingRekorAttestationAsync(_tenantId, 10, CancellationToken.None);
|
||||
var limited = await _store.GetPendingRekorAttestationAsync(_tenantId, 1, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
pending.Select(o => o.ObservationId).Should().Equal("obs-pending-1", "obs-pending-3");
|
||||
pending.Should().OnlyContain(o => !o.HasRekorLinkage);
|
||||
limited.Select(o => o.ObservationId).Should().Equal("obs-pending-1");
|
||||
}
|
||||
|
||||
private VexObservation CreateObservation(
|
||||
string observationId,
|
||||
string providerId,
|
||||
string vulnId,
|
||||
string productKey,
|
||||
DateTimeOffset? createdAt = null)
|
||||
{
|
||||
var now = createdAt ?? DateTimeOffset.UtcNow;
|
||||
|
||||
var statement = new VexObservationStatement(
|
||||
vulnId,
|
||||
@@ -235,4 +349,3 @@ public sealed class PostgresVexObservationStoreTests : IAsyncLifetime
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0324-M | DONE | Revalidated 2026-01-07; maintainability audit for Excititor.Persistence.Tests. |
|
||||
| AUDIT-0324-T | DONE | Revalidated 2026-01-07; test coverage audit for Excititor.Persistence.Tests. |
|
||||
| AUDIT-0324-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| QA-DEVOPS-VERIFY-002-T | DONE | 2026-02-11: Added Rekor linkage behavioral tests (round-trip, pending ordering, missing-observation negative path) for `vex-rekor-linkage` run-001. |
|
||||
|
||||
Reference in New Issue
Block a user