save checkpoint: save features

This commit is contained in:
master
2026-02-12 10:27:23 +02:00
parent dca86e1248
commit 5bca406787
8837 changed files with 1796879 additions and 5294 deletions

View File

@@ -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
""";

View File

@@ -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. |

View File

@@ -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
}

View File

@@ -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. |