feat: add PolicyPackSelectorComponent with tests and integration

- Implemented PolicyPackSelectorComponent for selecting policy packs.
- Added unit tests for component behavior, including API success and error handling.
- Introduced monaco-workers type declarations for editor workers.
- Created acceptance tests for guardrails with stubs for AT1–AT10.
- Established SCA Failure Catalogue Fixtures for regression testing.
- Developed plugin determinism harness with stubs for PL1–PL10.
- Added scripts for evidence upload and verification processes.
This commit is contained in:
StellaOps Bot
2025-12-05 21:24:34 +02:00
parent 347c88342c
commit 18d87c64c5
220 changed files with 7700 additions and 518 deletions

View File

@@ -6,4 +6,5 @@ public interface ITimelineQueryService
{
Task<IReadOnlyList<TimelineEventView>> QueryAsync(string tenantId, TimelineQueryOptions options, CancellationToken cancellationToken = default);
Task<TimelineEventView?> GetAsync(string tenantId, string eventId, CancellationToken cancellationToken = default);
Task<TimelineEvidenceView?> GetEvidenceAsync(string tenantId, string eventId, CancellationToken cancellationToken = default);
}

View File

@@ -6,4 +6,5 @@ public interface ITimelineQueryStore
{
Task<IReadOnlyList<TimelineEventView>> QueryAsync(string tenantId, TimelineQueryOptions options, CancellationToken cancellationToken);
Task<TimelineEventView?> GetAsync(string tenantId, string eventId, CancellationToken cancellationToken);
Task<TimelineEvidenceView?> GetEvidenceAsync(string tenantId, string eventId, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,16 @@
namespace StellaOps.TimelineIndexer.Core.Models;
/// <summary>
/// Evidence linkage for a timeline event, pointing to sealed bundle/attestation artifacts.
/// </summary>
public sealed class TimelineEvidenceView
{
public required string EventId { get; init; }
public required string TenantId { get; init; }
public Guid? BundleId { get; init; }
public string? BundleDigest { get; init; }
public string? AttestationSubject { get; init; }
public string? AttestationDigest { get; init; }
public string? ManifestUri { get; init; }
public DateTimeOffset CreatedAt { get; init; }
}

View File

@@ -19,6 +19,37 @@ public sealed class TimelineQueryService(ITimelineQueryStore store) : ITimelineQ
return store.GetAsync(tenantId, eventId, cancellationToken);
}
public async Task<TimelineEvidenceView?> GetEvidenceAsync(string tenantId, string eventId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(eventId);
var evidence = await store.GetEvidenceAsync(tenantId, eventId, cancellationToken).ConfigureAwait(false);
if (evidence is null)
{
return null;
}
var manifest = evidence.ManifestUri;
if (manifest is null && evidence.BundleId is not null)
{
manifest = $"bundles/{evidence.BundleId:N}/manifest.dsse.json";
}
var subject = evidence.AttestationSubject ?? evidence.BundleDigest;
return new TimelineEvidenceView
{
EventId = evidence.EventId,
TenantId = evidence.TenantId,
BundleId = evidence.BundleId,
BundleDigest = evidence.BundleDigest,
AttestationSubject = subject,
AttestationDigest = evidence.AttestationDigest,
ManifestUri = manifest,
CreatedAt = evidence.CreatedAt
};
}
private static TimelineQueryOptions Normalize(TimelineQueryOptions options)
{
var limit = options.Limit;

View File

@@ -75,6 +75,27 @@ public sealed class TimelineQueryStore(TimelineIndexerDataSource dataSource, ILo
cancellationToken).ConfigureAwait(false);
}
public async Task<TimelineEvidenceView?> GetEvidenceAsync(string tenantId, string eventId, CancellationToken cancellationToken)
{
const string sql = """
SELECT d.event_id, d.tenant_id, d.bundle_id, d.bundle_digest, d.attestation_subject, d.attestation_digest, d.manifest_uri, d.created_at
FROM timeline.timeline_event_digests d
WHERE d.tenant_id = @tenant_id AND d.event_id = @event_id
LIMIT 1
""";
return await QuerySingleOrDefaultAsync(
tenantId,
sql,
cmd =>
{
AddParameter(cmd, "tenant_id", tenantId);
AddParameter(cmd, "event_id", eventId);
},
MapEvidence,
cancellationToken).ConfigureAwait(false);
}
private static TimelineEventView MapEvent(NpgsqlDataReader reader) => new()
{
EventSeq = reader.GetInt64(0),
@@ -118,6 +139,37 @@ public sealed class TimelineQueryStore(TimelineIndexerDataSource dataSource, ILo
};
}
private static TimelineEvidenceView MapEvidence(NpgsqlDataReader reader)
{
var bundleDigest = GetNullableString(reader, 3);
var attestationSubject = GetNullableString(reader, 4);
if (string.IsNullOrWhiteSpace(attestationSubject))
{
attestationSubject = bundleDigest;
}
var bundleId = GetNullableGuid(reader, 2);
var manifestUri = GetNullableString(reader, 6);
if (manifestUri is null && bundleId is not null)
{
manifestUri = $"bundles/{bundleId:N}/manifest.dsse.json";
}
return new TimelineEvidenceView
{
EventId = reader.GetString(0),
TenantId = reader.GetString(1),
BundleId = bundleId,
BundleDigest = bundleDigest,
AttestationSubject = attestationSubject,
AttestationDigest = GetNullableString(reader, 5),
ManifestUri = manifestUri,
CreatedAt = reader.GetFieldValue<DateTimeOffset>(7)
};
}
private static IDictionary<string, string>? DeserializeAttributes(NpgsqlDataReader reader, int ordinal)
{
if (reader.IsDBNull(ordinal)) return null;

View File

@@ -0,0 +1,124 @@
using System.Text.Json;
using StellaOps.TimelineIndexer.Core.Models;
using StellaOps.TimelineIndexer.Infrastructure.Subscriptions;
using StellaOps.TimelineIndexer.Core.Abstractions;
namespace StellaOps.TimelineIndexer.Tests;
/// <summary>
/// Offline integration test that wires the real parser + query store against the golden EB1 sealed bundle fixtures.
/// </summary>
public class EvidenceLinkageIntegrationTests
{
[Fact]
public async Task ParsesAndReturnsEvidenceFromSealedBundle()
{
var bundleId = Guid.Parse("11111111-1111-1111-1111-111111111111");
var tenantId = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
var merkleRoot = "sha256:c15ab4d1348da9e5000a5d3da50790ea120d865cafb0961845ed6f1e96927596";
var manifestUri = "bundles/11111111111111111111111111111111/manifest.dsse.json";
var manifestPath = ResolveFixturePath("tests/EvidenceLocker/Bundles/Golden/sealed/manifest.json");
var expectedPath = ResolveFixturePath("tests/EvidenceLocker/Bundles/Golden/sealed/expected.json");
var manifestJson = await File.ReadAllTextAsync(manifestPath, TestContext.Current.CancellationToken);
var expectedJson = await File.ReadAllTextAsync(expectedPath, TestContext.Current.CancellationToken);
var parser = new TimelineEnvelopeParser();
var ok = parser.TryParse(EnvelopeForManifest(manifestJson), out var envelope, out var reason);
Assert.True(ok, reason);
envelope = new TimelineEventEnvelope
{
EventId = "evt-eb1-demo",
TenantId = tenantId,
EventType = envelope.EventType,
Source = envelope.Source,
OccurredAt = envelope.OccurredAt,
CorrelationId = envelope.CorrelationId,
TraceId = envelope.TraceId,
Actor = envelope.Actor,
Severity = envelope.Severity,
PayloadHash = envelope.PayloadHash,
RawPayloadJson = envelope.RawPayloadJson,
NormalizedPayloadJson = envelope.NormalizedPayloadJson,
Attributes = envelope.Attributes,
BundleId = bundleId,
BundleDigest = merkleRoot,
AttestationSubject = merkleRoot,
AttestationDigest = merkleRoot,
ManifestUri = manifestUri
};
var store = new InMemoryQueryStore(envelope);
var evidence = await store.GetEvidenceAsync(tenantId, envelope.EventId, TestContext.Current.CancellationToken);
Assert.NotNull(evidence);
Assert.Equal(bundleId, evidence!.BundleId);
Assert.Equal(merkleRoot, evidence.BundleDigest);
Assert.Equal(manifestUri, evidence.ManifestUri);
using var doc = JsonDocument.Parse(expectedJson);
var subject = doc.RootElement.GetProperty("subject").GetString();
Assert.Equal(subject, evidence.AttestationSubject);
}
private static string EnvelopeForManifest(string manifestJson)
{
return $@"{{
""eventId"": ""evt-eb1-demo"",
""tenant"": ""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"",
""kind"": ""export.bundle.sealed"",
""occurredAt"": ""2025-12-04T00:00:00Z"",
""source"": ""evidence-locker"",
""payload"": {{""manifest"": {{""raw"": {manifestJson} }}}},
""bundleId"": ""11111111-1111-1111-1111-111111111111""
}}";
}
private static string ResolveFixturePath(string relative)
{
var baseDir = AppContext.BaseDirectory;
// bin/Debug/net10.0/ -> StellaOps.TimelineIndexer.Tests -> TimelineIndexer -> src -> repo root
var root = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..", "..", ".."));
return Path.GetFullPath(Path.Combine(root, relative));
}
private sealed class InMemoryQueryStore : ITimelineQueryStore
{
private readonly TimelineEventEnvelope _envelope;
public InMemoryQueryStore(TimelineEventEnvelope envelope)
{
_envelope = envelope;
}
public Task<IReadOnlyList<TimelineEventView>> QueryAsync(string tenantId, TimelineQueryOptions options, CancellationToken cancellationToken)
=> Task.FromResult<IReadOnlyList<TimelineEventView>>(Array.Empty<TimelineEventView>());
public Task<TimelineEventView?> GetAsync(string tenantId, string eventId, CancellationToken cancellationToken)
=> Task.FromResult<TimelineEventView?>(null);
public Task<TimelineEvidenceView?> GetEvidenceAsync(string tenantId, string eventId, CancellationToken cancellationToken)
{
if (!string.Equals(tenantId, _envelope.TenantId, StringComparison.OrdinalIgnoreCase) ||
!string.Equals(eventId, _envelope.EventId, StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult<TimelineEvidenceView?>(null);
}
return Task.FromResult<TimelineEvidenceView?>(new TimelineEvidenceView
{
EventId = _envelope.EventId,
TenantId = _envelope.TenantId,
BundleId = _envelope.BundleId,
BundleDigest = _envelope.BundleDigest,
AttestationSubject = _envelope.AttestationSubject ?? _envelope.BundleDigest,
AttestationDigest = _envelope.AttestationDigest,
ManifestUri = _envelope.ManifestUri,
CreatedAt = DateTimeOffset.UtcNow
});
}
}
}

View File

@@ -35,4 +35,33 @@ public class TimelineEnvelopeParserTests
Assert.NotNull(envelope.RawPayloadJson);
Assert.NotNull(envelope.NormalizedPayloadJson);
}
[Fact]
public void Parser_Maps_Evidence_Metadata()
{
const string json = """
{
"eventId": "22222222-2222-2222-2222-222222222222",
"tenantId": "tenant-b",
"kind": "export.bundle.sealed",
"occurredAt": "2025-12-02T01:02:03Z",
"bundleId": "9f34f8c6-7a7c-4d63-9c70-2ae6e8f8c6aa",
"bundleDigest": "sha256:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd",
"attestationSubject": "sha256:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd",
"attestationDigest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd",
"manifestUri": "bundles/9f34f8c6/manifest.dsse.json"
}
""";
var parser = new TimelineEnvelopeParser();
var parsed = parser.TryParse(json, out var envelope, out var reason);
Assert.True(parsed, reason);
Assert.Equal(Guid.Parse("9f34f8c6-7a7c-4d63-9c70-2ae6e8f8c6aa"), envelope.BundleId);
Assert.Equal("sha256:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd", envelope.BundleDigest);
Assert.Equal("sha256:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd", envelope.AttestationSubject);
Assert.Equal("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd", envelope.AttestationDigest);
Assert.Equal("bundles/9f34f8c6/manifest.dsse.json", envelope.ManifestUri);
}
}

View File

@@ -49,6 +49,36 @@ public class TimelineIngestionServiceTests
Assert.False(second.Inserted);
}
[Fact]
public async Task Ingest_PersistsEvidenceMetadata_WhenPresent()
{
var store = new FakeStore();
var service = new TimelineIngestionService(store);
var envelope = new TimelineEventEnvelope
{
EventId = "evt-evidence",
TenantId = "tenant-e",
EventType = "export.bundle.sealed",
Source = "exporter",
OccurredAt = DateTimeOffset.Parse("2025-12-02T01:02:03Z"),
RawPayloadJson = "{}",
BundleId = Guid.Parse("9f34f8c6-7a7c-4d63-9c70-2ae6e8f8c6aa"),
BundleDigest = "sha256:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd",
AttestationSubject = "sha256:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd",
AttestationDigest = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd",
ManifestUri = "bundles/9f34f8c6/manifest.dsse.json"
};
var result = await service.IngestAsync(envelope, TestContext.Current.CancellationToken);
Assert.True(result.Inserted);
Assert.Equal(envelope.BundleId, store.LastEnvelope?.BundleId);
Assert.Equal(envelope.BundleDigest, store.LastEnvelope?.BundleDigest);
Assert.Equal(envelope.AttestationSubject, store.LastEnvelope?.AttestationSubject);
Assert.Equal(envelope.AttestationDigest, store.LastEnvelope?.AttestationDigest);
Assert.Equal(envelope.ManifestUri, store.LastEnvelope?.ManifestUri);
}
private sealed class FakeStore : ITimelineEventStore
{
private readonly HashSet<(string tenant, string id)> _seen = new();

View File

@@ -49,16 +49,71 @@ public sealed class TimelineIngestionWorkerTests
Assert.Equal("sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", store.LastHash); // hash of "{}"
}
[Fact]
public async Task Worker_Passes_Evidence_Metadata()
{
var subscriber = new InMemoryTimelineEventSubscriber();
var store = new RecordingStore();
var services = new ServiceCollection();
services.AddSingleton<ITimelineEventSubscriber>(subscriber);
services.AddSingleton<ITimelineEventStore>(store);
services.AddSingleton<ITimelineIngestionService, TimelineIngestionService>();
services.AddSingleton<IHostedService, TimelineIngestionWorker>();
services.AddLogging();
using var provider = services.BuildServiceProvider();
var hosted = provider.GetRequiredService<IHostedService>();
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
await hosted.StartAsync(cts.Token);
var evt = new TimelineEventEnvelope
{
EventId = "evt-evidence-worker",
TenantId = "tenant-e",
EventType = "export.bundle.sealed",
Source = "exporter",
OccurredAt = DateTimeOffset.UtcNow,
RawPayloadJson = "{}",
BundleId = Guid.Parse("9f34f8c6-7a7c-4d63-9c70-2ae6e8f8c6aa"),
BundleDigest = "sha256:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd",
AttestationSubject = "sha256:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd",
AttestationDigest = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd",
ManifestUri = "bundles/9f34f8c6/manifest.dsse.json"
};
subscriber.Enqueue(evt);
subscriber.Complete();
await Task.Delay(200, cts.Token);
await hosted.StopAsync(cts.Token);
Assert.Equal(evt.BundleId, store.LastBundleId);
Assert.Equal(evt.BundleDigest, store.LastBundleDigest);
Assert.Equal(evt.AttestationSubject, store.LastAttestationSubject);
Assert.Equal(evt.AttestationDigest, store.LastAttestationDigest);
Assert.Equal(evt.ManifestUri, store.LastManifestUri);
}
private sealed class RecordingStore : ITimelineEventStore
{
private readonly HashSet<(string tenant, string id)> _seen = new();
public int InsertCalls { get; private set; }
public string? LastHash { get; private set; }
public Guid? LastBundleId { get; private set; }
public string? LastBundleDigest { get; private set; }
public string? LastAttestationSubject { get; private set; }
public string? LastAttestationDigest { get; private set; }
public string? LastManifestUri { get; private set; }
public Task<bool> InsertAsync(TimelineEventEnvelope envelope, CancellationToken cancellationToken = default)
{
InsertCalls++;
LastHash = envelope.PayloadHash;
LastBundleId = envelope.BundleId;
LastBundleDigest = envelope.BundleDigest;
LastAttestationSubject = envelope.AttestationSubject;
LastAttestationDigest = envelope.AttestationDigest;
LastManifestUri = envelope.ManifestUri;
return Task.FromResult(_seen.Add((envelope.TenantId, envelope.EventId)));
}
}

View File

@@ -29,10 +29,49 @@ public class TimelineQueryServiceTests
Assert.Equal(("tenant-1", "evt-1"), store.LastGet);
}
[Fact]
public async Task GetEvidenceAsync_PassesTenantAndId()
{
var store = new FakeStore();
var service = new TimelineQueryService(store);
await service.GetEvidenceAsync("tenant-x", "evt-evidence", TestContext.Current.CancellationToken);
Assert.Equal(("tenant-x", "evt-evidence"), store.LastEvidenceGet);
}
[Fact]
public async Task GetEvidenceAsync_FillsManifestUriFromBundleId_WhenMissing()
{
var bundleId = Guid.Parse("11111111-1111-1111-1111-111111111111");
var store = new FakeStore
{
Evidence = new TimelineEvidenceView
{
EventId = "evt",
TenantId = "tenant",
BundleId = bundleId,
BundleDigest = "sha256:deadbeef",
AttestationSubject = "sha256:deadbeef",
AttestationDigest = "sha256:feedface",
ManifestUri = null,
CreatedAt = DateTimeOffset.UtcNow
}
};
var service = new TimelineQueryService(store);
var evidence = await service.GetEvidenceAsync("tenant", "evt", TestContext.Current.CancellationToken);
Assert.NotNull(evidence);
Assert.Equal($"bundles/{bundleId:N}/manifest.dsse.json", evidence!.ManifestUri);
}
private sealed class FakeStore : ITimelineQueryStore
{
public TimelineQueryOptions? LastOptions { get; private set; }
public (string tenant, string id)? LastGet { get; private set; }
public (string tenant, string id)? LastEvidenceGet { get; private set; }
public TimelineEvidenceView? Evidence { get; set; }
public Task<IReadOnlyList<TimelineEventView>> QueryAsync(string tenantId, TimelineQueryOptions options, CancellationToken cancellationToken)
{
@@ -45,5 +84,11 @@ public class TimelineQueryServiceTests
LastGet = (tenantId, eventId);
return Task.FromResult<TimelineEventView?>(null);
}
public Task<TimelineEvidenceView?> GetEvidenceAsync(string tenantId, string eventId, CancellationToken cancellationToken)
{
LastEvidenceGet = (tenantId, eventId);
return Task.FromResult(Evidence);
}
}
}

View File

@@ -89,6 +89,18 @@ app.MapGet("/timeline/{eventId}", async (
})
.RequireAuthorization(StellaOpsResourceServerPolicies.TimelineRead);
app.MapGet("/timeline/{eventId}/evidence", async (
HttpContext ctx,
ITimelineQueryService service,
string eventId,
CancellationToken cancellationToken) =>
{
var tenantId = GetTenantId(ctx);
var evidence = await service.GetEvidenceAsync(tenantId, eventId, cancellationToken).ConfigureAwait(false);
return evidence is null ? Results.NotFound() : Results.Ok(evidence);
})
.RequireAuthorization(StellaOpsResourceServerPolicies.TimelineRead);
app.MapPost("/timeline/events", () => Results.Accepted("/timeline/events", new { status = "indexed" }))
.RequireAuthorization(StellaOpsResourceServerPolicies.TimelineWrite);