diff --git a/AGENTS.md b/AGENTS.md
index 3b5352c56..55aebbc6e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -273,6 +273,7 @@ Implementation principles:
* Always follow .NET 10 and Angular v17 best practices.
* Apply SOLID design principles (SRP, OCP, LSP, ISP, DIP) in service and library code.
+* Keep in mind the nuget versions are controlled centrally by src/Directory* files, not via csproj
* Maximise reuse and composability.
* Maintain determinism: stable ordering, UTC ISO-8601 timestamps, immutable NDJSON where applicable.
@@ -388,6 +389,396 @@ If no design decision is required, you proceed autonomously, implementing the ch
5) **Do not defer:** Execute steps 1–4 immediately; reporting is after the fact, not a gating step.
**Lessons baked in:** Past delays came from missing code carry-over and missing sprint tasks. Always move advisory code into benchmarks/tests and open the corresponding sprint rows the same session you read the advisory.
+
+---
+
+### 8) Code Quality & Determinism Rules
+
+These rules were distilled from a comprehensive audit of 324+ projects. They address the most common recurring issues and must be followed by all implementers.
+
+#### 8.1) Compiler & Warning Discipline
+
+| Rule | Guidance |
+|------|----------|
+| **Enable TreatWarningsAsErrors** | All projects must set `true` in the `.csproj` or via `Directory.Build.props`. Relaxed warnings mask regressions and code quality drift. |
+
+```xml
+
+
+ true
+
+```
+
+#### 8.2) Deterministic Time & ID Generation
+
+| Rule | Guidance |
+|------|----------|
+| **Inject TimeProvider / ID generators** | Never use `DateTime.UtcNow`, `DateTimeOffset.UtcNow`, `Guid.NewGuid()`, or `Random.Shared` directly in production code. Inject `TimeProvider` (or `ITimeProvider`) and `IGuidGenerator` abstractions. |
+
+```csharp
+// BAD - nondeterministic, hard to test
+public class BadService
+{
+ public Record CreateRecord() => new Record
+ {
+ Id = Guid.NewGuid(),
+ CreatedAt = DateTimeOffset.UtcNow
+ };
+}
+
+// GOOD - injectable, testable, deterministic
+public class GoodService(TimeProvider timeProvider, IGuidGenerator guidGenerator)
+{
+ public Record CreateRecord() => new Record
+ {
+ Id = guidGenerator.NewGuid(),
+ CreatedAt = timeProvider.GetUtcNow()
+ };
+}
+```
+
+#### 8.3) ASCII-Only Output
+
+| Rule | Guidance |
+|------|----------|
+| **No mojibake or non-ASCII glyphs** | Use ASCII-only characters in comments, output strings, and log messages. No `ƒ?`, `バ`, `→`, `✓`, `✗`, or box-drawing characters. When Unicode is truly required, use explicit escapes (`\uXXXX`) and document the rationale. |
+
+```csharp
+// BAD - non-ASCII glyphs
+Console.WriteLine("✓ Success → proceeding");
+// or mojibake comments like: // ƒ+ validation passed
+
+// GOOD - ASCII only
+Console.WriteLine("[OK] Success - proceeding");
+// Comment: validation passed
+```
+
+#### 8.4) Test Project Requirements
+
+| Rule | Guidance |
+|------|----------|
+| **Every library needs tests** | All production libraries/services must have a corresponding `*.Tests` project covering: (a) happy paths, (b) error/edge cases, (c) determinism, and (d) serialization round-trips. |
+
+```
+src/
+ Scanner/
+ __Libraries/
+ StellaOps.Scanner.Core/
+ __Tests/
+ StellaOps.Scanner.Core.Tests/ <-- Required
+```
+
+#### 8.5) Culture-Invariant Parsing
+
+| Rule | Guidance |
+|------|----------|
+| **Use InvariantCulture** | Always use `CultureInfo.InvariantCulture` for parsing and formatting dates, numbers, percentages, and any string that will be persisted, hashed, or compared. Current culture causes locale-dependent, nondeterministic behavior. |
+
+```csharp
+// BAD - culture-sensitive
+var value = double.Parse(input);
+var formatted = percentage.ToString("P2");
+
+// GOOD - invariant culture
+var value = double.Parse(input, CultureInfo.InvariantCulture);
+var formatted = percentage.ToString("P2", CultureInfo.InvariantCulture);
+```
+
+#### 8.6) DSSE PAE Consistency
+
+| Rule | Guidance |
+|------|----------|
+| **Single DSSE PAE implementation** | Use one spec-compliant DSSE PAE helper (`StellaOps.Attestation.DsseHelper` or equivalent) across the codebase. DSSE v1 requires ASCII decimal lengths and space separators. Never reimplement PAE encoding. |
+
+```csharp
+// BAD - custom PAE implementation
+var pae = $"DSSEv1 {payloadType.Length} {payloadType} {payload.Length} ";
+
+// GOOD - use shared helper
+var pae = DsseHelper.ComputePreAuthenticationEncoding(payloadType, payload);
+```
+
+#### 8.7) RFC 8785 JSON Canonicalization
+
+| Rule | Guidance |
+|------|----------|
+| **Use shared RFC 8785 canonicalizer** | For digest/signature inputs, use a shared RFC 8785-compliant JSON canonicalizer with: sorted keys, minimal escaping per spec, no exponent notation for numbers, no trailing/leading zeros. Do not use `UnsafeRelaxedJsonEscaping` or `CamelCase` naming for canonical outputs. |
+
+```csharp
+// BAD - non-canonical JSON
+var json = JsonSerializer.Serialize(obj, new JsonSerializerOptions
+{
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+});
+
+// GOOD - use shared canonicalizer
+var canonicalJson = CanonicalJsonSerializer.Serialize(obj);
+var digest = ComputeDigest(canonicalJson);
+```
+
+#### 8.8) CancellationToken Propagation
+
+| Rule | Guidance |
+|------|----------|
+| **Propagate CancellationToken** | Always propagate `CancellationToken` through async call chains. Never use `CancellationToken.None` in production code except at entry points where no token is available. |
+
+```csharp
+// BAD - ignores cancellation
+public async Task ProcessAsync(CancellationToken ct)
+{
+ await _repository.SaveAsync(data, CancellationToken.None); // Wrong!
+ await Task.Delay(1000); // Missing ct
+}
+
+// GOOD - propagates cancellation
+public async Task ProcessAsync(CancellationToken ct)
+{
+ await _repository.SaveAsync(data, ct);
+ await Task.Delay(1000, ct);
+}
+```
+
+#### 8.9) HttpClient via Factory
+
+| Rule | Guidance |
+|------|----------|
+| **Use IHttpClientFactory** | Never `new HttpClient()` directly. Use `IHttpClientFactory` with configured timeouts and retry policies via Polly or `Microsoft.Extensions.Http.Resilience`. Direct HttpClient creation risks socket exhaustion. |
+
+```csharp
+// BAD - direct instantiation
+public class BadService
+{
+ public async Task FetchAsync()
+ {
+ using var client = new HttpClient(); // Socket exhaustion risk
+ await client.GetAsync(url);
+ }
+}
+
+// GOOD - factory with resilience
+public class GoodService(IHttpClientFactory httpClientFactory)
+{
+ public async Task FetchAsync()
+ {
+ var client = httpClientFactory.CreateClient("MyApi");
+ await client.GetAsync(url);
+ }
+}
+
+// Registration with timeout/retry
+services.AddHttpClient("MyApi")
+ .ConfigureHttpClient(c => c.Timeout = TimeSpan.FromSeconds(30))
+ .AddStandardResilienceHandler();
+```
+
+#### 8.10) Path/Root Resolution
+
+| Rule | Guidance |
+|------|----------|
+| **Explicit CLI options for paths** | Do not derive repository root from `AppContext.BaseDirectory` with parent directory walks. Use explicit CLI options (`--repo-root`) or environment variables. Provide sensible defaults with clear error messages. |
+
+```csharp
+// BAD - fragile parent walks
+var repoRoot = Path.GetFullPath(Path.Combine(
+ AppContext.BaseDirectory, "..", "..", "..", ".."));
+
+// GOOD - explicit option with fallback
+[Option("--repo-root", Description = "Repository root path")]
+public string? RepoRoot { get; set; }
+
+public string GetRepoRoot() =>
+ RepoRoot ?? Environment.GetEnvironmentVariable("STELLAOPS_REPO_ROOT")
+ ?? throw new InvalidOperationException("Repository root not specified. Use --repo-root or set STELLAOPS_REPO_ROOT.");
+```
+
+#### 8.11) Test Categorization
+
+| Rule | Guidance |
+|------|----------|
+| **Correct test categories** | Tag tests correctly: `[Trait("Category", "Unit")]` for pure unit tests, `[Trait("Category", "Integration")]` for tests requiring databases, containers, or network. Don't mix DB/network tests into unit suites. |
+
+```csharp
+// BAD - integration test marked as unit
+public class UserRepositoryTests // Uses Testcontainers/Postgres
+{
+ [Fact] // Missing category, runs with unit tests
+ public async Task Save_PersistsUser() { ... }
+}
+
+// GOOD - correctly categorized
+[Trait("Category", "Integration")]
+public class UserRepositoryTests
+{
+ [Fact]
+ public async Task Save_PersistsUser() { ... }
+}
+
+[Trait("Category", "Unit")]
+public class UserValidatorTests
+{
+ [Fact]
+ public void Validate_EmptyEmail_ReturnsFalse() { ... }
+}
+```
+
+#### 8.12) No Silent Stubs
+
+| Rule | Guidance |
+|------|----------|
+| **Unimplemented code must throw** | Placeholder code must throw `NotImplementedException` or return an explicit error/unsupported status. Never return success (`null`, empty results, or success codes) from unimplemented paths. |
+
+```csharp
+// BAD - silent stub masks missing implementation
+public async Task ProcessAsync()
+{
+ // TODO: implement later
+ return Result.Success(); // Ships broken feature!
+}
+
+// GOOD - explicit failure
+public async Task ProcessAsync()
+{
+ throw new NotImplementedException("ProcessAsync not yet implemented. See SPRINT_XYZ.");
+}
+```
+
+#### 8.13) Immutable Collection Returns
+
+| Rule | Guidance |
+|------|----------|
+| **Return immutable collections** | Public APIs must return `IReadOnlyList`, `ImmutableArray`, or defensive copies. Never expose mutable backing stores that callers can mutate. |
+
+```csharp
+// BAD - exposes mutable backing store
+public class BadRegistry
+{
+ private readonly List _scopes = new();
+ public List Scopes => _scopes; // Callers can mutate!
+}
+
+// GOOD - immutable return
+public class GoodRegistry
+{
+ private readonly List _scopes = new();
+ public IReadOnlyList Scopes => _scopes.AsReadOnly();
+ // or: public ImmutableArray Scopes => _scopes.ToImmutableArray();
+}
+```
+
+#### 8.14) Options Validation at Startup
+
+| Rule | Guidance |
+|------|----------|
+| **ValidateOnStart for options** | Use `ValidateDataAnnotations()` and `ValidateOnStart()` for options. Implement `IValidateOptions` for complex validation. All required config must be validated at startup, not at first use. |
+
+```csharp
+// BAD - no validation until runtime failure
+services.Configure(config.GetSection("My"));
+
+// GOOD - validated at startup
+services.AddOptions()
+ .Bind(config.GetSection("My"))
+ .ValidateDataAnnotations()
+ .ValidateOnStart();
+
+// With complex validation
+public class MyOptionsValidator : IValidateOptions
+{
+ public ValidateOptionsResult Validate(string? name, MyOptions options)
+ {
+ if (options.Timeout <= TimeSpan.Zero)
+ return ValidateOptionsResult.Fail("Timeout must be positive");
+ return ValidateOptionsResult.Success;
+ }
+}
+```
+
+#### 8.15) No Backup Files in Source
+
+| Rule | Guidance |
+|------|----------|
+| **Exclude backup/temp artifacts** | Add backup patterns (`*.Backup.tmp`, `*.bak`, `*.orig`) to `.gitignore`. Regularly audit for and remove stray artifacts. Consolidate duplicate tools/harnesses. |
+
+```gitignore
+# .gitignore additions
+*.Backup.tmp
+*.bak
+*.orig
+*~
+```
+
+#### 8.16) Test Production Code, Not Reimplementations
+
+| Rule | Guidance |
+|------|----------|
+| **Helpers call production code** | Test helpers must call production code, not reimplement algorithms (Merkle trees, DSSE PAE, parsers, canonicalizers). Only mock I/O and network boundaries. Reimplementations cause test/production drift. |
+
+```csharp
+// BAD - test reimplements production logic
+[Fact]
+public void Merkle_ComputesCorrectRoot()
+{
+ // Custom Merkle implementation in test
+ var root = TestMerkleHelper.ComputeRoot(leaves); // Drift risk!
+ Assert.Equal(expected, root);
+}
+
+// GOOD - test exercises production code
+[Fact]
+public void Merkle_ComputesCorrectRoot()
+{
+ // Uses production MerkleTreeBuilder
+ var root = MerkleTreeBuilder.ComputeRoot(leaves);
+ Assert.Equal(expected, root);
+}
+```
+
+#### 8.17) Bounded Caches with Eviction
+
+| Rule | Guidance |
+|------|----------|
+| **No unbounded Dictionary caches** | Do not use `ConcurrentDictionary` or `Dictionary` for caching without eviction policies. Use bounded caches with TTL/LRU eviction (`MemoryCache` with size limits, or external cache like Valkey). Document expected cardinality and eviction behavior. |
+
+```csharp
+// BAD - unbounded growth
+private readonly ConcurrentDictionary _cache = new();
+
+public void Add(string key, CacheEntry entry)
+{
+ _cache[key] = entry; // Never evicts, memory grows forever
+}
+
+// GOOD - bounded with eviction
+private readonly MemoryCache _cache = new(new MemoryCacheOptions
+{
+ SizeLimit = 10_000
+});
+
+public void Add(string key, CacheEntry entry)
+{
+ _cache.Set(key, entry, new MemoryCacheEntryOptions
+ {
+ Size = 1,
+ SlidingExpiration = TimeSpan.FromMinutes(30)
+ });
+}
+```
+
+#### 8.18) DateTimeOffset for PostgreSQL timestamptz
+
+| Rule | Guidance |
+|------|----------|
+| **Use GetFieldValue<DateTimeOffset>** | PostgreSQL `timestamptz` columns must be read via `reader.GetFieldValue()`, not `reader.GetDateTime()`. `GetDateTime()` loses offset information and causes UTC/local confusion. Store and retrieve all timestamps as UTC `DateTimeOffset`. |
+
+```csharp
+// BAD - loses offset information
+var createdAt = reader.GetDateTime(reader.GetOrdinal("created_at"));
+
+// GOOD - preserves offset
+var createdAt = reader.GetFieldValue(reader.GetOrdinal("created_at"));
+```
+
---
### 6) Role Switching
diff --git a/CLAUDE.md b/CLAUDE.md
index 1ede1ba39..7b0f7369f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -166,6 +166,7 @@ The codebase follows a monorepo pattern with modules under `src/`:
- Follow .NET 10 and Angular v17 best practices
- Apply SOLID principles (SRP, OCP, LSP, ISP, DIP) when designing services, libraries, and tests
+- Keep in mind the nuget versions are controlled centrally by src/Directory* files, not via csproj
- Maximise reuse and composability
- Never regress determinism, ordering, or precedence
- Every change must be accompanied by or covered by tests
@@ -181,6 +182,393 @@ The codebase follows a monorepo pattern with modules under `src/`:
- Tests use xUnit, Testcontainers for PostgreSQL integration tests
- See `src/__Tests/AGENTS.md` for detailed test infrastructure guidance
+## Code Quality & Determinism Rules
+
+These rules were distilled from a comprehensive audit of 324+ projects. They address the most common recurring issues and must be followed by all implementers.
+
+### 8.1) Compiler & Warning Discipline
+
+| Rule | Guidance |
+|------|----------|
+| **Enable TreatWarningsAsErrors** | All projects must set `true` in the `.csproj` or via `Directory.Build.props`. Relaxed warnings mask regressions and code quality drift. |
+
+```xml
+
+
+ true
+
+```
+
+### 8.2) Deterministic Time & ID Generation
+
+| Rule | Guidance |
+|------|----------|
+| **Inject TimeProvider / ID generators** | Never use `DateTime.UtcNow`, `DateTimeOffset.UtcNow`, `Guid.NewGuid()`, or `Random.Shared` directly in production code. Inject `TimeProvider` (or `ITimeProvider`) and `IGuidGenerator` abstractions. |
+
+```csharp
+// BAD - nondeterministic, hard to test
+public class BadService
+{
+ public Record CreateRecord() => new Record
+ {
+ Id = Guid.NewGuid(),
+ CreatedAt = DateTimeOffset.UtcNow
+ };
+}
+
+// GOOD - injectable, testable, deterministic
+public class GoodService(TimeProvider timeProvider, IGuidGenerator guidGenerator)
+{
+ public Record CreateRecord() => new Record
+ {
+ Id = guidGenerator.NewGuid(),
+ CreatedAt = timeProvider.GetUtcNow()
+ };
+}
+```
+
+### 8.3) ASCII-Only Output
+
+| Rule | Guidance |
+|------|----------|
+| **No mojibake or non-ASCII glyphs** | Use ASCII-only characters in comments, output strings, and log messages. No `ƒ?`, `バ`, `→`, `✓`, `✗`, or box-drawing characters. When Unicode is truly required, use explicit escapes (`\uXXXX`) and document the rationale. |
+
+```csharp
+// BAD - non-ASCII glyphs
+Console.WriteLine("✓ Success → proceeding");
+// or mojibake comments like: // ƒ+ validation passed
+
+// GOOD - ASCII only
+Console.WriteLine("[OK] Success - proceeding");
+// Comment: validation passed
+```
+
+### 8.4) Test Project Requirements
+
+| Rule | Guidance |
+|------|----------|
+| **Every library needs tests** | All production libraries/services must have a corresponding `*.Tests` project covering: (a) happy paths, (b) error/edge cases, (c) determinism, and (d) serialization round-trips. |
+
+```
+src/
+ Scanner/
+ __Libraries/
+ StellaOps.Scanner.Core/
+ __Tests/
+ StellaOps.Scanner.Core.Tests/ <-- Required
+```
+
+### 8.5) Culture-Invariant Parsing
+
+| Rule | Guidance |
+|------|----------|
+| **Use InvariantCulture** | Always use `CultureInfo.InvariantCulture` for parsing and formatting dates, numbers, percentages, and any string that will be persisted, hashed, or compared. Current culture causes locale-dependent, nondeterministic behavior. |
+
+```csharp
+// BAD - culture-sensitive
+var value = double.Parse(input);
+var formatted = percentage.ToString("P2");
+
+// GOOD - invariant culture
+var value = double.Parse(input, CultureInfo.InvariantCulture);
+var formatted = percentage.ToString("P2", CultureInfo.InvariantCulture);
+```
+
+### 8.6) DSSE PAE Consistency
+
+| Rule | Guidance |
+|------|----------|
+| **Single DSSE PAE implementation** | Use one spec-compliant DSSE PAE helper (`StellaOps.Attestation.DsseHelper` or equivalent) across the codebase. DSSE v1 requires ASCII decimal lengths and space separators. Never reimplement PAE encoding. |
+
+```csharp
+// BAD - custom PAE implementation
+var pae = $"DSSEv1 {payloadType.Length} {payloadType} {payload.Length} ";
+
+// GOOD - use shared helper
+var pae = DsseHelper.ComputePreAuthenticationEncoding(payloadType, payload);
+```
+
+### 8.7) RFC 8785 JSON Canonicalization
+
+| Rule | Guidance |
+|------|----------|
+| **Use shared RFC 8785 canonicalizer** | For digest/signature inputs, use a shared RFC 8785-compliant JSON canonicalizer with: sorted keys, minimal escaping per spec, no exponent notation for numbers, no trailing/leading zeros. Do not use `UnsafeRelaxedJsonEscaping` or `CamelCase` naming for canonical outputs. |
+
+```csharp
+// BAD - non-canonical JSON
+var json = JsonSerializer.Serialize(obj, new JsonSerializerOptions
+{
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+});
+
+// GOOD - use shared canonicalizer
+var canonicalJson = CanonicalJsonSerializer.Serialize(obj);
+var digest = ComputeDigest(canonicalJson);
+```
+
+### 8.8) CancellationToken Propagation
+
+| Rule | Guidance |
+|------|----------|
+| **Propagate CancellationToken** | Always propagate `CancellationToken` through async call chains. Never use `CancellationToken.None` in production code except at entry points where no token is available. |
+
+```csharp
+// BAD - ignores cancellation
+public async Task ProcessAsync(CancellationToken ct)
+{
+ await _repository.SaveAsync(data, CancellationToken.None); // Wrong!
+ await Task.Delay(1000); // Missing ct
+}
+
+// GOOD - propagates cancellation
+public async Task ProcessAsync(CancellationToken ct)
+{
+ await _repository.SaveAsync(data, ct);
+ await Task.Delay(1000, ct);
+}
+```
+
+### 8.9) HttpClient via Factory
+
+| Rule | Guidance |
+|------|----------|
+| **Use IHttpClientFactory** | Never `new HttpClient()` directly. Use `IHttpClientFactory` with configured timeouts and retry policies via Polly or `Microsoft.Extensions.Http.Resilience`. Direct HttpClient creation risks socket exhaustion. |
+
+```csharp
+// BAD - direct instantiation
+public class BadService
+{
+ public async Task FetchAsync()
+ {
+ using var client = new HttpClient(); // Socket exhaustion risk
+ await client.GetAsync(url);
+ }
+}
+
+// GOOD - factory with resilience
+public class GoodService(IHttpClientFactory httpClientFactory)
+{
+ public async Task FetchAsync()
+ {
+ var client = httpClientFactory.CreateClient("MyApi");
+ await client.GetAsync(url);
+ }
+}
+
+// Registration with timeout/retry
+services.AddHttpClient("MyApi")
+ .ConfigureHttpClient(c => c.Timeout = TimeSpan.FromSeconds(30))
+ .AddStandardResilienceHandler();
+```
+
+### 8.10) Path/Root Resolution
+
+| Rule | Guidance |
+|------|----------|
+| **Explicit CLI options for paths** | Do not derive repository root from `AppContext.BaseDirectory` with parent directory walks. Use explicit CLI options (`--repo-root`) or environment variables. Provide sensible defaults with clear error messages. |
+
+```csharp
+// BAD - fragile parent walks
+var repoRoot = Path.GetFullPath(Path.Combine(
+ AppContext.BaseDirectory, "..", "..", "..", ".."));
+
+// GOOD - explicit option with fallback
+[Option("--repo-root", Description = "Repository root path")]
+public string? RepoRoot { get; set; }
+
+public string GetRepoRoot() =>
+ RepoRoot ?? Environment.GetEnvironmentVariable("STELLAOPS_REPO_ROOT")
+ ?? throw new InvalidOperationException("Repository root not specified. Use --repo-root or set STELLAOPS_REPO_ROOT.");
+```
+
+### 8.11) Test Categorization
+
+| Rule | Guidance |
+|------|----------|
+| **Correct test categories** | Tag tests correctly: `[Trait("Category", "Unit")]` for pure unit tests, `[Trait("Category", "Integration")]` for tests requiring databases, containers, or network. Don't mix DB/network tests into unit suites. |
+
+```csharp
+// BAD - integration test marked as unit
+public class UserRepositoryTests // Uses Testcontainers/Postgres
+{
+ [Fact] // Missing category, runs with unit tests
+ public async Task Save_PersistsUser() { ... }
+}
+
+// GOOD - correctly categorized
+[Trait("Category", "Integration")]
+public class UserRepositoryTests
+{
+ [Fact]
+ public async Task Save_PersistsUser() { ... }
+}
+
+[Trait("Category", "Unit")]
+public class UserValidatorTests
+{
+ [Fact]
+ public void Validate_EmptyEmail_ReturnsFalse() { ... }
+}
+```
+
+### 8.12) No Silent Stubs
+
+| Rule | Guidance |
+|------|----------|
+| **Unimplemented code must throw** | Placeholder code must throw `NotImplementedException` or return an explicit error/unsupported status. Never return success (`null`, empty results, or success codes) from unimplemented paths. |
+
+```csharp
+// BAD - silent stub masks missing implementation
+public async Task ProcessAsync()
+{
+ // TODO: implement later
+ return Result.Success(); // Ships broken feature!
+}
+
+// GOOD - explicit failure
+public async Task ProcessAsync()
+{
+ throw new NotImplementedException("ProcessAsync not yet implemented. See SPRINT_XYZ.");
+}
+```
+
+### 8.13) Immutable Collection Returns
+
+| Rule | Guidance |
+|------|----------|
+| **Return immutable collections** | Public APIs must return `IReadOnlyList`, `ImmutableArray`, or defensive copies. Never expose mutable backing stores that callers can mutate. |
+
+```csharp
+// BAD - exposes mutable backing store
+public class BadRegistry
+{
+ private readonly List _scopes = new();
+ public List Scopes => _scopes; // Callers can mutate!
+}
+
+// GOOD - immutable return
+public class GoodRegistry
+{
+ private readonly List _scopes = new();
+ public IReadOnlyList Scopes => _scopes.AsReadOnly();
+ // or: public ImmutableArray Scopes => _scopes.ToImmutableArray();
+}
+```
+
+### 8.14) Options Validation at Startup
+
+| Rule | Guidance |
+|------|----------|
+| **ValidateOnStart for options** | Use `ValidateDataAnnotations()` and `ValidateOnStart()` for options. Implement `IValidateOptions` for complex validation. All required config must be validated at startup, not at first use. |
+
+```csharp
+// BAD - no validation until runtime failure
+services.Configure(config.GetSection("My"));
+
+// GOOD - validated at startup
+services.AddOptions()
+ .Bind(config.GetSection("My"))
+ .ValidateDataAnnotations()
+ .ValidateOnStart();
+
+// With complex validation
+public class MyOptionsValidator : IValidateOptions
+{
+ public ValidateOptionsResult Validate(string? name, MyOptions options)
+ {
+ if (options.Timeout <= TimeSpan.Zero)
+ return ValidateOptionsResult.Fail("Timeout must be positive");
+ return ValidateOptionsResult.Success;
+ }
+}
+```
+
+### 8.15) No Backup Files in Source
+
+| Rule | Guidance |
+|------|----------|
+| **Exclude backup/temp artifacts** | Add backup patterns (`*.Backup.tmp`, `*.bak`, `*.orig`) to `.gitignore`. Regularly audit for and remove stray artifacts. Consolidate duplicate tools/harnesses. |
+
+```gitignore
+# .gitignore additions
+*.Backup.tmp
+*.bak
+*.orig
+*~
+```
+
+### 8.16) Test Production Code, Not Reimplementations
+
+| Rule | Guidance |
+|------|----------|
+| **Helpers call production code** | Test helpers must call production code, not reimplement algorithms (Merkle trees, DSSE PAE, parsers, canonicalizers). Only mock I/O and network boundaries. Reimplementations cause test/production drift. |
+
+```csharp
+// BAD - test reimplements production logic
+[Fact]
+public void Merkle_ComputesCorrectRoot()
+{
+ // Custom Merkle implementation in test
+ var root = TestMerkleHelper.ComputeRoot(leaves); // Drift risk!
+ Assert.Equal(expected, root);
+}
+
+// GOOD - test exercises production code
+[Fact]
+public void Merkle_ComputesCorrectRoot()
+{
+ // Uses production MerkleTreeBuilder
+ var root = MerkleTreeBuilder.ComputeRoot(leaves);
+ Assert.Equal(expected, root);
+}
+```
+
+### 8.17) Bounded Caches with Eviction
+
+| Rule | Guidance |
+|------|----------|
+| **No unbounded Dictionary caches** | Do not use `ConcurrentDictionary` or `Dictionary` for caching without eviction policies. Use bounded caches with TTL/LRU eviction (`MemoryCache` with size limits, or external cache like Valkey). Document expected cardinality and eviction behavior. |
+
+```csharp
+// BAD - unbounded growth
+private readonly ConcurrentDictionary _cache = new();
+
+public void Add(string key, CacheEntry entry)
+{
+ _cache[key] = entry; // Never evicts, memory grows forever
+}
+
+// GOOD - bounded with eviction
+private readonly MemoryCache _cache = new(new MemoryCacheOptions
+{
+ SizeLimit = 10_000
+});
+
+public void Add(string key, CacheEntry entry)
+{
+ _cache.Set(key, entry, new MemoryCacheEntryOptions
+ {
+ Size = 1,
+ SlidingExpiration = TimeSpan.FromMinutes(30)
+ });
+}
+```
+
+### 8.18) DateTimeOffset for PostgreSQL timestamptz
+
+| Rule | Guidance |
+|------|----------|
+| **Use GetFieldValue<DateTimeOffset>** | PostgreSQL `timestamptz` columns must be read via `reader.GetFieldValue()`, not `reader.GetDateTime()`. `GetDateTime()` loses offset information and causes UTC/local confusion. Store and retrieve all timestamps as UTC `DateTimeOffset`. |
+
+```csharp
+// BAD - loses offset information
+var createdAt = reader.GetDateTime(reader.GetOrdinal("created_at"));
+
+// GOOD - preserves offset
+var createdAt = reader.GetFieldValue(reader.GetOrdinal("created_at"));
+```
+
### Documentation Updates
When scope, contracts, or workflows change, update the relevant docs under:
diff --git a/build_output_latest.txt b/build_output_latest.txt
deleted file mode 100644
index 1b2a72964..000000000
--- a/build_output_latest.txt
+++ /dev/null
@@ -1,55 +0,0 @@
-
- StellaOps.Router.Common -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.Common\bin\Debug\net10.0\StellaOps.Router.Common.dll
- StellaOps.Router.Config -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.Config\bin\Debug\net10.0\StellaOps.Router.Config.dll
- StellaOps.DependencyInjection -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.DependencyInjection\bin\Debug\net10.0\StellaOps.DependencyInjection.dll
- StellaOps.Plugin -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Plugin\bin\Debug\net10.0\StellaOps.Plugin.dll
- StellaOps.AirGap.Policy -> E:\dev\git.stella-ops.org\src\AirGap\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy\bin\Debug\net10.0\StellaOps.AirGap.Policy.dll
- StellaOps.Concelier.SourceIntel -> E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.SourceIntel\bin\Debug\net10.0\StellaOps.Concelier.SourceIntel.dll
- StellaOps.Cryptography -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography\bin\Debug\net10.0\StellaOps.Cryptography.dll
- StellaOps.Auth.Abstractions -> E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\bin\Debug\net10.0\StellaOps.Auth.Abstractions.dll
- StellaOps.Telemetry.Core -> E:\dev\git.stella-ops.org\src\Telemetry\StellaOps.Telemetry.Core\StellaOps.Telemetry.Core\bin\Debug\net10.0\StellaOps.Telemetry.Core.dll
- StellaOps.Canonical.Json -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Canonical.Json\bin\Debug\net10.0\StellaOps.Canonical.Json.dll
- StellaOps.Evidence.Bundle -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Evidence.Bundle\bin\Debug\net10.0\StellaOps.Evidence.Bundle.dll
- StellaOps.Messaging -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Messaging\bin\Debug\net10.0\StellaOps.Messaging.dll
- StellaOps.Router.Transport.Tcp -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.Transport.Tcp\bin\Debug\net10.0\StellaOps.Router.Transport.Tcp.dll
- StellaOps.Infrastructure.Postgres -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Infrastructure.Postgres\bin\Debug\net10.0\StellaOps.Infrastructure.Postgres.dll
- StellaOps.Infrastructure.Postgres.Testing -> E:\dev\git.stella-ops.org\src\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\bin\Debug\net10.0\StellaOps.Infrastructure.Postgres.Testing.dll
- StellaOps.Feedser.BinaryAnalysis -> E:\dev\git.stella-ops.org\src\Feedser\StellaOps.Feedser.BinaryAnalysis\bin\Debug\net10.0\StellaOps.Feedser.BinaryAnalysis.dll
- StellaOps.Scheduler.Models -> E:\dev\git.stella-ops.org\src\Scheduler\__Libraries\StellaOps.Scheduler.Models\bin\Debug\net10.0\StellaOps.Scheduler.Models.dll
- StellaOps.Aoc -> E:\dev\git.stella-ops.org\src\Aoc\__Libraries\StellaOps.Aoc\bin\Debug\net10.0\StellaOps.Aoc.dll
- StellaOps.Router.Transport.RabbitMq -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.Transport.RabbitMq\bin\Debug\net10.0\StellaOps.Router.Transport.RabbitMq.dll
- NotifySmokeCheck -> E:\dev\git.stella-ops.org\src\Tools\NotifySmokeCheck\bin\Debug\net10.0\NotifySmokeCheck.dll
- StellaOps.Infrastructure.EfCore -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Infrastructure.EfCore\bin\Debug\net10.0\StellaOps.Infrastructure.EfCore.dll
- StellaOps.Router.Transport.InMemory -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.Transport.InMemory\bin\Debug\net10.0\StellaOps.Router.Transport.InMemory.dll
- RustFsMigrator -> E:\dev\git.stella-ops.org\src\Tools\RustFsMigrator\bin\Debug\net10.0\RustFsMigrator.dll
- StellaOps.Cryptography.Plugin.WineCsp -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.WineCsp\bin\Debug\net10.0\StellaOps.Cryptography.Plugin.WineCsp.dll
- StellaOps.Microservice -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Microservice\bin\Debug\net10.0\StellaOps.Microservice.dll
- StellaOps.Replay.Core -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Replay.Core\bin\Debug\net10.0\StellaOps.Replay.Core.dll
- StellaOps.Messaging.Transport.InMemory -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Messaging.Transport.InMemory\bin\Debug\net10.0\StellaOps.Messaging.Transport.InMemory.dll
- StellaOps.Feedser.Core -> E:\dev\git.stella-ops.org\src\Feedser\StellaOps.Feedser.Core\bin\Debug\net10.0\StellaOps.Feedser.Core.dll
- StellaOps.Cryptography.Kms -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Kms\bin\Debug\net10.0\StellaOps.Cryptography.Kms.dll
- StellaOps.Cryptography.Plugin.PqSoft -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.PqSoft\bin\Debug\net10.0\StellaOps.Cryptography.Plugin.PqSoft.dll
- StellaOps.Policy.RiskProfile -> E:\dev\git.stella-ops.org\src\Policy\StellaOps.Policy.RiskProfile\bin\Debug\net10.0\StellaOps.Policy.RiskProfile.dll
- StellaOps.Router.Transport.Udp -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.Transport.Udp\bin\Debug\net10.0\StellaOps.Router.Transport.Udp.dll
- StellaOps.Microservice.SourceGen -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Microservice.SourceGen\bin\Debug\netstandard2.0\StellaOps.Microservice.SourceGen.dll
- StellaOps.Findings.Ledger -> E:\dev\git.stella-ops.org\src\Findings\StellaOps.Findings.Ledger\bin\Debug\net10.0\StellaOps.Findings.Ledger.dll
- LedgerReplayHarness -> E:\dev\git.stella-ops.org\src\Findings\StellaOps.Findings.Ledger\tools\LedgerReplayHarness\bin\Debug\net10.0\LedgerReplayHarness.dll
- StellaOps.Attestor.Envelope -> E:\dev\git.stella-ops.org\src\Attestor\StellaOps.Attestor.Envelope\bin\Debug\net10.0\StellaOps.Attestor.Envelope.dll
- StellaOps.Router.Gateway -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.Gateway\bin\Debug\net10.0\StellaOps.Router.Gateway.dll
- StellaOps.Ingestion.Telemetry -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Ingestion.Telemetry\bin\Debug\net10.0\StellaOps.Ingestion.Telemetry.dll
- Examples.Billing.Microservice -> E:\dev\git.stella-ops.org\src\Router\examples\Examples.Billing.Microservice\bin\Debug\net10.0\Examples.Billing.Microservice.dll
- StellaOps.Cryptography.Plugin.Pkcs11Gost -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.Pkcs11Gost\bin\Debug\net10.0\StellaOps.Cryptography.Plugin.Pkcs11Gost.dll
- StellaOps.Microservice.AspNetCore -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Microservice.AspNetCore\bin\Debug\net10.0\StellaOps.Microservice.AspNetCore.dll
- StellaOps.Router.AspNet -> E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.AspNet\bin\Debug\net10.0\StellaOps.Router.AspNet.dll
- StellaOps.Authority.Plugins.Abstractions -> E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Authority.Plugins.Abstractions\bin\Debug\net10.0\StellaOps.Authority.Plugins.Abstractions.dll
- StellaOps.Cryptography.Plugin.OfflineVerification -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.OfflineVerification\bin\Debug\net10.0\StellaOps.Cryptography.Plugin.OfflineVerification.dll
- Examples.Gateway -> E:\dev\git.stella-ops.org\src\Router\examples\Examples.Gateway\bin\Debug\net10.0\Examples.Gateway.dll
- Examples.NotificationService -> E:\dev\git.stella-ops.org\src\Router\examples\Examples.NotificationService\bin\Debug\net10.0\Examples.NotificationService.dll
- StellaOps.Provenance.Attestation -> E:\dev\git.stella-ops.org\src\Provenance\StellaOps.Provenance.Attestation\bin\Debug\net10.0\StellaOps.Provenance.Attestation.dll
- StellaOps.AirGap.Policy.Analyzers -> E:\dev\git.stella-ops.org\src\AirGap\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy.Analyzers\bin\Debug\netstandard2.0\StellaOps.AirGap.Policy.Analyzers.dll
- StellaOps.Cryptography.Plugin.SmRemote -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.SmRemote\bin\Debug\net10.0\StellaOps.Cryptography.Plugin.SmRemote.dll
- StellaOps.VersionComparison -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.VersionComparison\bin\Debug\net10.0\StellaOps.VersionComparison.dll
- StellaOps.TestKit -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.TestKit\bin\Debug\net10.0\StellaOps.TestKit.dll
- StellaOps.Aoc.Analyzers -> E:\dev\git.stella-ops.org\src\Aoc\__Analyzers\StellaOps.Aoc.Analyzers\bin\Debug\netstandard2.0\StellaOps.Aoc.Analyzers.dll
- StellaOps.AirGap.Importer -> E:\dev\git.stella-ops.org\src\AirGap\StellaOps.AirGap.Importer\bin\Debug\net10.0\StellaOps.AirGap.Importer.dll
- StellaOps.Cryptography.PluginLoader -> E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.PluginLoader\bin\Debug\net10.0\StellaOps.Cryptography.PluginLoader.dll
diff --git a/docs/implplan/SPRINT_20251229_006_CICD_full_pipeline_validation.md b/docs/implplan/SPRINT_20251229_006_CICD_full_pipeline_validation.md
index be949019b..0b13a018d 100644
--- a/docs/implplan/SPRINT_20251229_006_CICD_full_pipeline_validation.md
+++ b/docs/implplan/SPRINT_20251229_006_CICD_full_pipeline_validation.md
@@ -754,6 +754,8 @@ docker compose -f devops/compose/docker-compose.ci.yaml logs postgres-ci
| 2025-12-29 | Verified unit-split project count is 302 (`rg --files -g "*Tests.csproj" src`); slices beyond 302 are no-ops and do not execute tests. | DevOps |
| 2025-12-30 | Fixed AirGap bundle copy lock by closing output before hashing; `StellaOps.AirGap.Bundle.Tests` (Category=Unit) passed. | DevOps |
| 2025-12-30 | Added AirGap persistence migrations + schema alignment and updated tests/fixture; `StellaOps.AirGap.Persistence.Tests` (Category=Unit) passed. | DevOps |
+| 2026-01-02 | Fixed smoke build failures (AirGap DSSE PAE ambiguity, Attestor.Oci span mismatch) and resumed unit-split slice 1-100; failures isolated to AirGap Importer + Attestor tests. | DevOps |
+| 2026-01-02 | Adjusted AirGap/Attestor tests and in-memory pagination; verified `StellaOps.AirGap.Importer.Tests`, `StellaOps.Attestor.Envelope.Tests`, `StellaOps.Attestor.Infrastructure.Tests`, and `StellaOps.Attestor.ProofChain.Tests` (Category=Unit) pass. | DevOps |
## Decisions & Risks
- **Risk:** Extended tests (~45 min) may be skipped for time constraints
diff --git a/docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md b/docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md
index f480cc5fc..206b16204 100644
--- a/docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md
+++ b/docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md
@@ -160,58 +160,58 @@ Bulk task definitions (applies to every project row below):
| 138 | AUDIT-0046-A | DONE | Waived (test project) | Guild | src/Attestor/__Tests/StellaOps.Attestor.Bundle.Tests/StellaOps.Attestor.Bundle.Tests.csproj - APPLY |
| 139 | AUDIT-0047-M | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Bundling/StellaOps.Attestor.Bundling.csproj - MAINT |
| 140 | AUDIT-0047-T | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Bundling/StellaOps.Attestor.Bundling.csproj - TEST |
-| 141 | AUDIT-0047-A | TODO | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Bundling/StellaOps.Attestor.Bundling.csproj - APPLY |
+| 141 | AUDIT-0047-A | DONE | - | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Bundling/StellaOps.Attestor.Bundling.csproj - APPLY |
| 142 | AUDIT-0048-M | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.Bundling.Tests/StellaOps.Attestor.Bundling.Tests.csproj - MAINT |
| 143 | AUDIT-0048-T | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.Bundling.Tests/StellaOps.Attestor.Bundling.Tests.csproj - TEST |
| 144 | AUDIT-0048-A | DONE | Waived (test project) | Guild | src/Attestor/__Tests/StellaOps.Attestor.Bundling.Tests/StellaOps.Attestor.Bundling.Tests.csproj - APPLY |
| 145 | AUDIT-0049-M | DONE | Report | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/StellaOps.Attestor.Core.csproj - MAINT |
| 146 | AUDIT-0049-T | DONE | Report | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/StellaOps.Attestor.Core.csproj - TEST |
-| 147 | AUDIT-0049-A | TODO | Approval | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/StellaOps.Attestor.Core.csproj - APPLY |
+| 147 | AUDIT-0049-A | DOING | Approval | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/StellaOps.Attestor.Core.csproj - APPLY |
| 148 | AUDIT-0050-M | DONE | Report | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/StellaOps.Attestor.Core.Tests.csproj - MAINT |
| 149 | AUDIT-0050-T | DONE | Report | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/StellaOps.Attestor.Core.Tests.csproj - TEST |
| 150 | AUDIT-0050-A | DONE | Waived (test project) | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/StellaOps.Attestor.Core.Tests.csproj - APPLY |
| 151 | AUDIT-0051-M | DONE | Report | Guild | src/Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj - MAINT |
| 152 | AUDIT-0051-T | DONE | Report | Guild | src/Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj - TEST |
-| 153 | AUDIT-0051-A | TODO | Approval | Guild | src/Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj - APPLY |
+| 153 | AUDIT-0051-A | DONE | Approval | Guild | src/Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj - APPLY |
| 154 | AUDIT-0052-M | DONE | Report | Guild | src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/StellaOps.Attestor.Envelope.Tests.csproj - MAINT |
| 155 | AUDIT-0052-T | DONE | Report | Guild | src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/StellaOps.Attestor.Envelope.Tests.csproj - TEST |
| 156 | AUDIT-0052-A | DONE | Waived (test project) | Guild | src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/StellaOps.Attestor.Envelope.Tests.csproj - APPLY |
| 157 | AUDIT-0053-M | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/StellaOps.Attestor.GraphRoot.csproj - MAINT |
| 158 | AUDIT-0053-T | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/StellaOps.Attestor.GraphRoot.csproj - TEST |
-| 159 | AUDIT-0053-A | TODO | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/StellaOps.Attestor.GraphRoot.csproj - APPLY |
+| 159 | AUDIT-0053-A | DONE | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/StellaOps.Attestor.GraphRoot.csproj - APPLY |
| 160 | AUDIT-0054-M | DONE | Report | Guild | src/Attestor/__Libraries/__Tests/StellaOps.Attestor.GraphRoot.Tests/StellaOps.Attestor.GraphRoot.Tests.csproj - MAINT |
| 161 | AUDIT-0054-T | DONE | Report | Guild | src/Attestor/__Libraries/__Tests/StellaOps.Attestor.GraphRoot.Tests/StellaOps.Attestor.GraphRoot.Tests.csproj - TEST |
| 162 | AUDIT-0054-A | DONE | Waived (test project) | Guild | src/Attestor/__Libraries/__Tests/StellaOps.Attestor.GraphRoot.Tests/StellaOps.Attestor.GraphRoot.Tests.csproj - APPLY |
| 163 | AUDIT-0055-M | DONE | Report | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj - MAINT |
| 164 | AUDIT-0055-T | DONE | Report | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj - TEST |
-| 165 | AUDIT-0055-A | TODO | Approval | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj - APPLY |
+| 165 | AUDIT-0055-A | DONE | Approval | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj - APPLY |
| 166 | AUDIT-0056-M | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Oci/StellaOps.Attestor.Oci.csproj - MAINT |
| 167 | AUDIT-0056-T | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Oci/StellaOps.Attestor.Oci.csproj - TEST |
-| 168 | AUDIT-0056-A | TODO | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Oci/StellaOps.Attestor.Oci.csproj - APPLY |
+| 168 | AUDIT-0056-A | DONE | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Oci/StellaOps.Attestor.Oci.csproj - APPLY |
| 169 | AUDIT-0057-M | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.Oci.Tests/StellaOps.Attestor.Oci.Tests.csproj - MAINT |
| 170 | AUDIT-0057-T | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.Oci.Tests/StellaOps.Attestor.Oci.Tests.csproj - TEST |
| 171 | AUDIT-0057-A | DONE | Waived (test project) | Guild | src/Attestor/__Tests/StellaOps.Attestor.Oci.Tests/StellaOps.Attestor.Oci.Tests.csproj - APPLY |
| 172 | AUDIT-0058-M | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Offline/StellaOps.Attestor.Offline.csproj - MAINT |
| 173 | AUDIT-0058-T | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Offline/StellaOps.Attestor.Offline.csproj - TEST |
-| 174 | AUDIT-0058-A | TODO | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Offline/StellaOps.Attestor.Offline.csproj - APPLY |
+| 174 | AUDIT-0058-A | DOING | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Offline/StellaOps.Attestor.Offline.csproj - APPLY |
| 175 | AUDIT-0059-M | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.Offline.Tests/StellaOps.Attestor.Offline.Tests.csproj - MAINT |
| 176 | AUDIT-0059-T | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.Offline.Tests/StellaOps.Attestor.Offline.Tests.csproj - TEST |
| 177 | AUDIT-0059-A | DONE | Waived (test project) | Guild | src/Attestor/__Tests/StellaOps.Attestor.Offline.Tests/StellaOps.Attestor.Offline.Tests.csproj - APPLY |
| 178 | AUDIT-0060-M | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Persistence/StellaOps.Attestor.Persistence.csproj - MAINT |
| 179 | AUDIT-0060-T | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Persistence/StellaOps.Attestor.Persistence.csproj - TEST |
-| 180 | AUDIT-0060-A | TODO | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Persistence/StellaOps.Attestor.Persistence.csproj - APPLY |
+| 180 | AUDIT-0060-A | DONE | Applied defaults, normalization, deterministic matching, tests | Guild | src/Attestor/__Libraries/StellaOps.Attestor.Persistence/StellaOps.Attestor.Persistence.csproj - APPLY |
| 181 | AUDIT-0061-M | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.Persistence.Tests/StellaOps.Attestor.Persistence.Tests.csproj - MAINT |
| 182 | AUDIT-0061-T | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.Persistence.Tests/StellaOps.Attestor.Persistence.Tests.csproj - TEST |
| 183 | AUDIT-0061-A | DONE | Waived (test project) | Guild | src/Attestor/__Tests/StellaOps.Attestor.Persistence.Tests/StellaOps.Attestor.Persistence.Tests.csproj - APPLY |
| 184 | AUDIT-0062-M | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/StellaOps.Attestor.ProofChain.csproj - MAINT |
| 185 | AUDIT-0062-T | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/StellaOps.Attestor.ProofChain.csproj - TEST |
-| 186 | AUDIT-0062-A | TODO | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/StellaOps.Attestor.ProofChain.csproj - APPLY |
+| 186 | AUDIT-0062-A | DONE | Applied determinism, time providers, canonicalization, tests | Guild | src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/StellaOps.Attestor.ProofChain.csproj - APPLY |
| 187 | AUDIT-0063-M | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.ProofChain.Tests/StellaOps.Attestor.ProofChain.Tests.csproj - MAINT |
| 188 | AUDIT-0063-T | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.ProofChain.Tests/StellaOps.Attestor.ProofChain.Tests.csproj - TEST |
| 189 | AUDIT-0063-A | DONE | Waived (test project) | Guild | src/Attestor/__Tests/StellaOps.Attestor.ProofChain.Tests/StellaOps.Attestor.ProofChain.Tests.csproj - APPLY |
| 190 | AUDIT-0064-M | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.StandardPredicates/StellaOps.Attestor.StandardPredicates.csproj - MAINT |
| 191 | AUDIT-0064-T | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.StandardPredicates/StellaOps.Attestor.StandardPredicates.csproj - TEST |
-| 192 | AUDIT-0064-A | TODO | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.StandardPredicates/StellaOps.Attestor.StandardPredicates.csproj - APPLY |
+| 192 | AUDIT-0064-A | DONE | Applied canonicalization, registry normalization, parser fixes, tests | Guild | src/Attestor/__Libraries/StellaOps.Attestor.StandardPredicates/StellaOps.Attestor.StandardPredicates.csproj - APPLY |
| 193 | AUDIT-0065-M | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.StandardPredicates.Tests/StellaOps.Attestor.StandardPredicates.Tests.csproj - MAINT |
| 194 | AUDIT-0065-T | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.StandardPredicates.Tests/StellaOps.Attestor.StandardPredicates.Tests.csproj - TEST |
| 195 | AUDIT-0065-A | DONE | Waived (test project) | Guild | src/Attestor/__Tests/StellaOps.Attestor.StandardPredicates.Tests/StellaOps.Attestor.StandardPredicates.Tests.csproj - APPLY |
@@ -220,31 +220,31 @@ Bulk task definitions (applies to every project row below):
| 198 | AUDIT-0066-A | DONE | Waived (test project) | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/StellaOps.Attestor.Tests.csproj - APPLY |
| 199 | AUDIT-0067-M | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/StellaOps.Attestor.TrustVerdict.csproj - MAINT |
| 200 | AUDIT-0067-T | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/StellaOps.Attestor.TrustVerdict.csproj - TEST |
-| 201 | AUDIT-0067-A | TODO | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/StellaOps.Attestor.TrustVerdict.csproj - APPLY |
+| 201 | AUDIT-0067-A | DOING | Approval | Guild | src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/StellaOps.Attestor.TrustVerdict.csproj - APPLY |
| 202 | AUDIT-0068-M | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict.Tests/StellaOps.Attestor.TrustVerdict.Tests.csproj - MAINT |
| 203 | AUDIT-0068-T | DONE | Report | Guild | src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict.Tests/StellaOps.Attestor.TrustVerdict.Tests.csproj - TEST |
| 204 | AUDIT-0068-A | DONE | Waived (test project) | Guild | src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict.Tests/StellaOps.Attestor.TrustVerdict.Tests.csproj - APPLY |
| 205 | AUDIT-0069-M | DONE | Report | Guild | src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/StellaOps.Attestor.Types.Generator.csproj - MAINT |
| 206 | AUDIT-0069-T | DONE | Report | Guild | src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/StellaOps.Attestor.Types.Generator.csproj - TEST |
-| 207 | AUDIT-0069-A | TODO | Approval | Guild | src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/StellaOps.Attestor.Types.Generator.csproj - APPLY |
+| 207 | AUDIT-0069-A | DONE | Applied generator hardening + tests | Guild | src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/StellaOps.Attestor.Types.Generator.csproj - APPLY |
| 208 | AUDIT-0070-M | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.Types.Tests/StellaOps.Attestor.Types.Tests.csproj - MAINT |
| 209 | AUDIT-0070-T | DONE | Report | Guild | src/Attestor/__Tests/StellaOps.Attestor.Types.Tests/StellaOps.Attestor.Types.Tests.csproj - TEST |
| 210 | AUDIT-0070-A | DONE | Waived (test project) | Guild | src/Attestor/__Tests/StellaOps.Attestor.Types.Tests/StellaOps.Attestor.Types.Tests.csproj - APPLY |
| 211 | AUDIT-0071-M | DONE | Report | Guild | src/Attestor/StellaOps.Attestor.Verify/StellaOps.Attestor.Verify.csproj - MAINT |
| 212 | AUDIT-0071-T | DONE | Report | Guild | src/Attestor/StellaOps.Attestor.Verify/StellaOps.Attestor.Verify.csproj - TEST |
-| 213 | AUDIT-0071-A | TODO | Approval | Guild | src/Attestor/StellaOps.Attestor.Verify/StellaOps.Attestor.Verify.csproj - APPLY |
+| 213 | AUDIT-0071-A | DONE | Applied verification fixes + tests | Guild | src/Attestor/StellaOps.Attestor.Verify/StellaOps.Attestor.Verify.csproj - APPLY |
| 214 | AUDIT-0072-M | DONE | Report | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/StellaOps.Attestor.WebService.csproj - MAINT |
| 215 | AUDIT-0072-T | DONE | Report | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/StellaOps.Attestor.WebService.csproj - TEST |
-| 216 | AUDIT-0072-A | TODO | Approval | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/StellaOps.Attestor.WebService.csproj - APPLY |
+| 216 | AUDIT-0072-A | DOING | Approval | Guild | src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/StellaOps.Attestor.WebService.csproj - APPLY |
| 217 | AUDIT-0073-M | DONE | Report | Guild | src/__Libraries/StellaOps.Audit.ReplayToken/StellaOps.Audit.ReplayToken.csproj - MAINT |
| 218 | AUDIT-0073-T | DONE | Report | Guild | src/__Libraries/StellaOps.Audit.ReplayToken/StellaOps.Audit.ReplayToken.csproj - TEST |
-| 219 | AUDIT-0073-A | TODO | Approval | Guild | src/__Libraries/StellaOps.Audit.ReplayToken/StellaOps.Audit.ReplayToken.csproj - APPLY |
+| 219 | AUDIT-0073-A | DONE | Approval | Guild | src/__Libraries/StellaOps.Audit.ReplayToken/StellaOps.Audit.ReplayToken.csproj - APPLY |
| 220 | AUDIT-0074-M | DONE | Report | Guild | src/__Tests/StellaOps.Audit.ReplayToken.Tests/StellaOps.Audit.ReplayToken.Tests.csproj - MAINT |
| 221 | AUDIT-0074-T | DONE | Report | Guild | src/__Tests/StellaOps.Audit.ReplayToken.Tests/StellaOps.Audit.ReplayToken.Tests.csproj - TEST |
| 222 | AUDIT-0074-A | DONE | Waived (test project) | Guild | src/__Tests/StellaOps.Audit.ReplayToken.Tests/StellaOps.Audit.ReplayToken.Tests.csproj - APPLY |
| 223 | AUDIT-0075-M | DONE | Report | Guild | src/__Libraries/StellaOps.AuditPack/StellaOps.AuditPack.csproj - MAINT |
| 224 | AUDIT-0075-T | DONE | Report | Guild | src/__Libraries/StellaOps.AuditPack/StellaOps.AuditPack.csproj - TEST |
-| 225 | AUDIT-0075-A | TODO | Approval | Guild | src/__Libraries/StellaOps.AuditPack/StellaOps.AuditPack.csproj - APPLY |
+| 225 | AUDIT-0075-A | DONE | Approval | Guild | src/__Libraries/StellaOps.AuditPack/StellaOps.AuditPack.csproj - APPLY |
| 226 | AUDIT-0076-M | DONE | Report | Guild | src/__Libraries/__Tests/StellaOps.AuditPack.Tests/StellaOps.AuditPack.Tests.csproj - MAINT |
| 227 | AUDIT-0076-T | DONE | Report | Guild | src/__Libraries/__Tests/StellaOps.AuditPack.Tests/StellaOps.AuditPack.Tests.csproj - TEST |
| 228 | AUDIT-0076-A | DONE | Waived (test project) | Guild | src/__Libraries/__Tests/StellaOps.AuditPack.Tests/StellaOps.AuditPack.Tests.csproj - APPLY |
@@ -253,31 +253,31 @@ Bulk task definitions (applies to every project row below):
| 231 | AUDIT-0077-A | DONE | Waived (test project) | Guild | src/__Tests/unit/StellaOps.AuditPack.Tests/StellaOps.AuditPack.Tests.csproj - APPLY |
| 232 | AUDIT-0078-M | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj - MAINT |
| 233 | AUDIT-0078-T | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj - TEST |
-| 234 | AUDIT-0078-A | TODO | Approval | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj - APPLY |
+| 234 | AUDIT-0078-A | DONE | Done | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj - APPLY |
| 235 | AUDIT-0079-M | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions.Tests/StellaOps.Auth.Abstractions.Tests.csproj - MAINT |
| 236 | AUDIT-0079-T | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions.Tests/StellaOps.Auth.Abstractions.Tests.csproj - TEST |
| 237 | AUDIT-0079-A | DONE | Waived (test project) | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions.Tests/StellaOps.Auth.Abstractions.Tests.csproj - APPLY |
| 238 | AUDIT-0080-M | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj - MAINT |
| 239 | AUDIT-0080-T | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj - TEST |
-| 240 | AUDIT-0080-A | TODO | Approval | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj - APPLY |
+| 240 | AUDIT-0080-A | DONE | Done | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj - APPLY |
| 241 | AUDIT-0081-M | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Client.Tests/StellaOps.Auth.Client.Tests.csproj - MAINT |
| 242 | AUDIT-0081-T | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Client.Tests/StellaOps.Auth.Client.Tests.csproj - TEST |
| 243 | AUDIT-0081-A | DONE | Waived (test project) | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.Client.Tests/StellaOps.Auth.Client.Tests.csproj - APPLY |
| 244 | AUDIT-0082-M | DONE | Report | Guild | src/__Libraries/StellaOps.Auth.Security/StellaOps.Auth.Security.csproj - MAINT |
| 245 | AUDIT-0082-T | DONE | Report | Guild | src/__Libraries/StellaOps.Auth.Security/StellaOps.Auth.Security.csproj - TEST |
-| 246 | AUDIT-0082-A | TODO | Approval | Guild | src/__Libraries/StellaOps.Auth.Security/StellaOps.Auth.Security.csproj - APPLY |
+| 246 | AUDIT-0082-A | DONE | Done | Guild | src/__Libraries/StellaOps.Auth.Security/StellaOps.Auth.Security.csproj - APPLY |
| 247 | AUDIT-0083-M | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj - MAINT |
| 248 | AUDIT-0083-T | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj - TEST |
-| 249 | AUDIT-0083-A | TODO | Approval | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj - APPLY |
+| 249 | AUDIT-0083-A | DONE | Done | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj - APPLY |
| 250 | AUDIT-0084-M | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration.Tests/StellaOps.Auth.ServerIntegration.Tests.csproj - MAINT |
| 251 | AUDIT-0084-T | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration.Tests/StellaOps.Auth.ServerIntegration.Tests.csproj - TEST |
| 252 | AUDIT-0084-A | DONE | Waived (test project) | Guild | src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration.Tests/StellaOps.Auth.ServerIntegration.Tests.csproj - APPLY |
| 253 | AUDIT-0085-M | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Authority/StellaOps.Authority.csproj - MAINT |
| 254 | AUDIT-0085-T | DONE | Report | Guild | src/Authority/StellaOps.Authority/StellaOps.Authority/StellaOps.Authority.csproj - TEST |
-| 255 | AUDIT-0085-A | TODO | Approval | Guild | src/Authority/StellaOps.Authority/StellaOps.Authority/StellaOps.Authority.csproj - APPLY |
+| 255 | AUDIT-0085-A | DONE | Applied store determinism, replay tracking, issuer IDs, and tests | Guild | src/Authority/StellaOps.Authority/StellaOps.Authority/StellaOps.Authority.csproj - APPLY |
| 256 | AUDIT-0086-M | DONE | Report | Guild | src/Authority/__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj - MAINT |
| 257 | AUDIT-0086-T | DONE | Report | Guild | src/Authority/__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj - TEST |
-| 258 | AUDIT-0086-A | TODO | Approval | Guild | src/Authority/__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj - APPLY |
+| 258 | AUDIT-0086-A | DONE | Applied determinism, replay verifier handling, and tests | Guild | src/Authority/__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj - APPLY |
| 259 | AUDIT-0087-M | DONE | Report | Guild | src/Authority/__Tests/StellaOps.Authority.Core.Tests/StellaOps.Authority.Core.Tests.csproj - MAINT |
| 260 | AUDIT-0087-T | DONE | Report | Guild | src/Authority/__Tests/StellaOps.Authority.Core.Tests/StellaOps.Authority.Core.Tests.csproj - TEST |
| 261 | AUDIT-0087-A | DONE | Waived (test project) | Guild | src/Authority/__Tests/StellaOps.Authority.Core.Tests/StellaOps.Authority.Core.Tests.csproj - APPLY |
@@ -986,164 +986,164 @@ Bulk task definitions (applies to every project row below):
| 964 | AUDIT-0322-M | DONE | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Formats.OpenVEX.Tests/StellaOps.Excititor.Formats.OpenVEX.Tests.csproj - MAINT |
| 965 | AUDIT-0322-T | DONE | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Formats.OpenVEX.Tests/StellaOps.Excititor.Formats.OpenVEX.Tests.csproj - TEST |
| 966 | AUDIT-0322-A | DONE | Waived (test project) | Guild | src/Excititor/__Tests/StellaOps.Excititor.Formats.OpenVEX.Tests/StellaOps.Excititor.Formats.OpenVEX.Tests.csproj - APPLY |
-| 967 | AUDIT-0323-M | TODO | Report | Guild | src/Excititor/__Libraries/StellaOps.Excititor.Persistence/StellaOps.Excititor.Persistence.csproj - MAINT |
-| 968 | AUDIT-0323-T | TODO | Report | Guild | src/Excititor/__Libraries/StellaOps.Excititor.Persistence/StellaOps.Excititor.Persistence.csproj - TEST |
+| 967 | AUDIT-0323-M | DONE | Report | Guild | src/Excititor/__Libraries/StellaOps.Excititor.Persistence/StellaOps.Excititor.Persistence.csproj - MAINT |
+| 968 | AUDIT-0323-T | DONE | Report | Guild | src/Excititor/__Libraries/StellaOps.Excititor.Persistence/StellaOps.Excititor.Persistence.csproj - TEST |
| 969 | AUDIT-0323-A | TODO | Approval | Guild | src/Excititor/__Libraries/StellaOps.Excititor.Persistence/StellaOps.Excititor.Persistence.csproj - APPLY |
-| 970 | AUDIT-0324-M | TODO | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Persistence.Tests/StellaOps.Excititor.Persistence.Tests.csproj - MAINT |
-| 971 | AUDIT-0324-T | TODO | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Persistence.Tests/StellaOps.Excititor.Persistence.Tests.csproj - TEST |
+| 970 | AUDIT-0324-M | DONE | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Persistence.Tests/StellaOps.Excititor.Persistence.Tests.csproj - MAINT |
+| 971 | AUDIT-0324-T | DONE | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Persistence.Tests/StellaOps.Excititor.Persistence.Tests.csproj - TEST |
| 972 | AUDIT-0324-A | DONE | Waived (test project) | Guild | src/Excititor/__Tests/StellaOps.Excititor.Persistence.Tests/StellaOps.Excititor.Persistence.Tests.csproj - APPLY |
-| 973 | AUDIT-0325-M | TODO | Report | Guild | src/Excititor/__Libraries/StellaOps.Excititor.Policy/StellaOps.Excititor.Policy.csproj - MAINT |
-| 974 | AUDIT-0325-T | TODO | Report | Guild | src/Excititor/__Libraries/StellaOps.Excititor.Policy/StellaOps.Excititor.Policy.csproj - TEST |
+| 973 | AUDIT-0325-M | DONE | Report | Guild | src/Excititor/__Libraries/StellaOps.Excititor.Policy/StellaOps.Excititor.Policy.csproj - MAINT |
+| 974 | AUDIT-0325-T | DONE | Report | Guild | src/Excititor/__Libraries/StellaOps.Excititor.Policy/StellaOps.Excititor.Policy.csproj - TEST |
| 975 | AUDIT-0325-A | TODO | Approval | Guild | src/Excititor/__Libraries/StellaOps.Excititor.Policy/StellaOps.Excititor.Policy.csproj - APPLY |
-| 976 | AUDIT-0326-M | TODO | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Policy.Tests/StellaOps.Excititor.Policy.Tests.csproj - MAINT |
-| 977 | AUDIT-0326-T | TODO | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Policy.Tests/StellaOps.Excititor.Policy.Tests.csproj - TEST |
+| 976 | AUDIT-0326-M | DONE | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Policy.Tests/StellaOps.Excititor.Policy.Tests.csproj - MAINT |
+| 977 | AUDIT-0326-T | DONE | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Policy.Tests/StellaOps.Excititor.Policy.Tests.csproj - TEST |
| 978 | AUDIT-0326-A | DONE | Waived (test project) | Guild | src/Excititor/__Tests/StellaOps.Excititor.Policy.Tests/StellaOps.Excititor.Policy.Tests.csproj - APPLY |
-| 979 | AUDIT-0327-M | TODO | Report | Guild | src/Excititor/StellaOps.Excititor.WebService/StellaOps.Excititor.WebService.csproj - MAINT |
-| 980 | AUDIT-0327-T | TODO | Report | Guild | src/Excititor/StellaOps.Excititor.WebService/StellaOps.Excititor.WebService.csproj - TEST |
+| 979 | AUDIT-0327-M | DONE | Report | Guild | src/Excititor/StellaOps.Excititor.WebService/StellaOps.Excititor.WebService.csproj - MAINT |
+| 980 | AUDIT-0327-T | DONE | Report | Guild | src/Excititor/StellaOps.Excititor.WebService/StellaOps.Excititor.WebService.csproj - TEST |
| 981 | AUDIT-0327-A | TODO | Approval | Guild | src/Excititor/StellaOps.Excititor.WebService/StellaOps.Excititor.WebService.csproj - APPLY |
-| 982 | AUDIT-0328-M | TODO | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj - MAINT |
-| 983 | AUDIT-0328-T | TODO | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj - TEST |
+| 982 | AUDIT-0328-M | DONE | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj - MAINT |
+| 983 | AUDIT-0328-T | DONE | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj - TEST |
| 984 | AUDIT-0328-A | DONE | Waived (test project) | Guild | src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj - APPLY |
-| 985 | AUDIT-0329-M | TODO | Report | Guild | src/Excititor/StellaOps.Excititor.Worker/StellaOps.Excititor.Worker.csproj - MAINT |
-| 986 | AUDIT-0329-T | TODO | Report | Guild | src/Excititor/StellaOps.Excititor.Worker/StellaOps.Excititor.Worker.csproj - TEST |
+| 985 | AUDIT-0329-M | DONE | Report | Guild | src/Excititor/StellaOps.Excititor.Worker/StellaOps.Excititor.Worker.csproj - MAINT |
+| 986 | AUDIT-0329-T | DONE | Report | Guild | src/Excititor/StellaOps.Excititor.Worker/StellaOps.Excititor.Worker.csproj - TEST |
| 987 | AUDIT-0329-A | TODO | Approval | Guild | src/Excititor/StellaOps.Excititor.Worker/StellaOps.Excititor.Worker.csproj - APPLY |
-| 988 | AUDIT-0330-M | TODO | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Worker.Tests/StellaOps.Excititor.Worker.Tests.csproj - MAINT |
-| 989 | AUDIT-0330-T | TODO | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Worker.Tests/StellaOps.Excititor.Worker.Tests.csproj - TEST |
+| 988 | AUDIT-0330-M | DONE | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Worker.Tests/StellaOps.Excititor.Worker.Tests.csproj - MAINT |
+| 989 | AUDIT-0330-T | DONE | Report | Guild | src/Excititor/__Tests/StellaOps.Excititor.Worker.Tests/StellaOps.Excititor.Worker.Tests.csproj - TEST |
| 990 | AUDIT-0330-A | DONE | Waived (test project) | Guild | src/Excititor/__Tests/StellaOps.Excititor.Worker.Tests/StellaOps.Excititor.Worker.Tests.csproj - APPLY |
-| 991 | AUDIT-0331-M | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/StellaOps.ExportCenter.Client.csproj - MAINT |
-| 992 | AUDIT-0331-T | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/StellaOps.ExportCenter.Client.csproj - TEST |
+| 991 | AUDIT-0331-M | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/StellaOps.ExportCenter.Client.csproj - MAINT |
+| 992 | AUDIT-0331-T | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/StellaOps.ExportCenter.Client.csproj - TEST |
| 993 | AUDIT-0331-A | TODO | Approval | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/StellaOps.ExportCenter.Client.csproj - APPLY |
-| 994 | AUDIT-0332-M | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client.Tests/StellaOps.ExportCenter.Client.Tests.csproj - MAINT |
-| 995 | AUDIT-0332-T | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client.Tests/StellaOps.ExportCenter.Client.Tests.csproj - TEST |
+| 994 | AUDIT-0332-M | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client.Tests/StellaOps.ExportCenter.Client.Tests.csproj - MAINT |
+| 995 | AUDIT-0332-T | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client.Tests/StellaOps.ExportCenter.Client.Tests.csproj - TEST |
| 996 | AUDIT-0332-A | DONE | Waived (test project) | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client.Tests/StellaOps.ExportCenter.Client.Tests.csproj - APPLY |
-| 997 | AUDIT-0333-M | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/StellaOps.ExportCenter.Core.csproj - MAINT |
-| 998 | AUDIT-0333-T | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/StellaOps.ExportCenter.Core.csproj - TEST |
+| 997 | AUDIT-0333-M | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/StellaOps.ExportCenter.Core.csproj - MAINT |
+| 998 | AUDIT-0333-T | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/StellaOps.ExportCenter.Core.csproj - TEST |
| 999 | AUDIT-0333-A | TODO | Approval | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/StellaOps.ExportCenter.Core.csproj - APPLY |
-| 1000 | AUDIT-0334-M | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/StellaOps.ExportCenter.Infrastructure.csproj - MAINT |
-| 1001 | AUDIT-0334-T | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/StellaOps.ExportCenter.Infrastructure.csproj - TEST |
+| 1000 | AUDIT-0334-M | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/StellaOps.ExportCenter.Infrastructure.csproj - MAINT |
+| 1001 | AUDIT-0334-T | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/StellaOps.ExportCenter.Infrastructure.csproj - TEST |
| 1002 | AUDIT-0334-A | TODO | Approval | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/StellaOps.ExportCenter.Infrastructure.csproj - APPLY |
-| 1003 | AUDIT-0335-M | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/StellaOps.ExportCenter.RiskBundles.csproj - MAINT |
-| 1004 | AUDIT-0335-T | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/StellaOps.ExportCenter.RiskBundles.csproj - TEST |
+| 1003 | AUDIT-0335-M | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/StellaOps.ExportCenter.RiskBundles.csproj - MAINT |
+| 1004 | AUDIT-0335-T | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/StellaOps.ExportCenter.RiskBundles.csproj - TEST |
| 1005 | AUDIT-0335-A | TODO | Approval | Guild | src/ExportCenter/StellaOps.ExportCenter.RiskBundles/StellaOps.ExportCenter.RiskBundles.csproj - APPLY |
-| 1006 | AUDIT-0336-M | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj - MAINT |
-| 1007 | AUDIT-0336-T | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj - TEST |
+| 1006 | AUDIT-0336-M | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj - MAINT |
+| 1007 | AUDIT-0336-T | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj - TEST |
| 1008 | AUDIT-0336-A | DONE | Waived (test project) | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj - APPLY |
-| 1009 | AUDIT-0337-M | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - MAINT |
-| 1010 | AUDIT-0337-T | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - TEST |
+| 1009 | AUDIT-0337-M | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - MAINT |
+| 1010 | AUDIT-0337-T | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - TEST |
| 1011 | AUDIT-0337-A | TODO | Approval | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - APPLY |
-| 1012 | AUDIT-0338-M | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj - MAINT |
-| 1013 | AUDIT-0338-T | TODO | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj - TEST |
+| 1012 | AUDIT-0338-M | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj - MAINT |
+| 1013 | AUDIT-0338-T | DONE | Report | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj - TEST |
| 1014 | AUDIT-0338-A | TODO | Approval | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj - APPLY |
-| 1015 | AUDIT-0339-M | TODO | Report | Guild | src/Feedser/StellaOps.Feedser.BinaryAnalysis/StellaOps.Feedser.BinaryAnalysis.csproj - MAINT |
-| 1016 | AUDIT-0339-T | TODO | Report | Guild | src/Feedser/StellaOps.Feedser.BinaryAnalysis/StellaOps.Feedser.BinaryAnalysis.csproj - TEST |
+| 1015 | AUDIT-0339-M | DONE | Report | Guild | src/Feedser/StellaOps.Feedser.BinaryAnalysis/StellaOps.Feedser.BinaryAnalysis.csproj - MAINT |
+| 1016 | AUDIT-0339-T | DONE | Report | Guild | src/Feedser/StellaOps.Feedser.BinaryAnalysis/StellaOps.Feedser.BinaryAnalysis.csproj - TEST |
| 1017 | AUDIT-0339-A | TODO | Approval | Guild | src/Feedser/StellaOps.Feedser.BinaryAnalysis/StellaOps.Feedser.BinaryAnalysis.csproj - APPLY |
-| 1018 | AUDIT-0340-M | TODO | Report | Guild | src/Feedser/StellaOps.Feedser.Core/StellaOps.Feedser.Core.csproj - MAINT |
-| 1019 | AUDIT-0340-T | TODO | Report | Guild | src/Feedser/StellaOps.Feedser.Core/StellaOps.Feedser.Core.csproj - TEST |
+| 1018 | AUDIT-0340-M | DONE | Report | Guild | src/Feedser/StellaOps.Feedser.Core/StellaOps.Feedser.Core.csproj - MAINT |
+| 1019 | AUDIT-0340-T | DONE | Report | Guild | src/Feedser/StellaOps.Feedser.Core/StellaOps.Feedser.Core.csproj - TEST |
| 1020 | AUDIT-0340-A | TODO | Approval | Guild | src/Feedser/StellaOps.Feedser.Core/StellaOps.Feedser.Core.csproj - APPLY |
-| 1021 | AUDIT-0341-M | TODO | Report | Guild | src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/StellaOps.Feedser.Core.Tests.csproj - MAINT |
-| 1022 | AUDIT-0341-T | TODO | Report | Guild | src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/StellaOps.Feedser.Core.Tests.csproj - TEST |
+| 1021 | AUDIT-0341-M | DONE | Report | Guild | src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/StellaOps.Feedser.Core.Tests.csproj - MAINT |
+| 1022 | AUDIT-0341-T | DONE | Report | Guild | src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/StellaOps.Feedser.Core.Tests.csproj - TEST |
| 1023 | AUDIT-0341-A | DONE | Waived (test project) | Guild | src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/StellaOps.Feedser.Core.Tests.csproj - APPLY |
-| 1024 | AUDIT-0342-M | TODO | Report | Guild | src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj - MAINT |
-| 1025 | AUDIT-0342-T | TODO | Report | Guild | src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj - TEST |
+| 1024 | AUDIT-0342-M | DONE | Report | Guild | src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj - MAINT |
+| 1025 | AUDIT-0342-T | DONE | Report | Guild | src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj - TEST |
| 1026 | AUDIT-0342-A | TODO | Approval | Guild | src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj - APPLY |
-| 1027 | AUDIT-0343-M | TODO | Report | Guild | src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj - MAINT |
-| 1028 | AUDIT-0343-T | TODO | Report | Guild | src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj - TEST |
+| 1027 | AUDIT-0343-M | DONE | Report | Guild | src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj - MAINT |
+| 1028 | AUDIT-0343-T | DONE | Report | Guild | src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj - TEST |
| 1029 | AUDIT-0343-A | DONE | Waived (test project) | Guild | src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj - APPLY |
-| 1030 | AUDIT-0344-M | TODO | Report | Guild | src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj - MAINT |
-| 1031 | AUDIT-0344-T | TODO | Report | Guild | src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj - TEST |
+| 1030 | AUDIT-0344-M | DONE | Report | Guild | src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj - MAINT |
+| 1031 | AUDIT-0344-T | DONE | Report | Guild | src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj - TEST |
| 1032 | AUDIT-0344-A | DONE | Waived (test project) | Guild | src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj - APPLY |
-| 1033 | AUDIT-0345-M | TODO | Report | Guild | src/Findings/StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj - MAINT |
-| 1034 | AUDIT-0345-T | TODO | Report | Guild | src/Findings/StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj - TEST |
+| 1033 | AUDIT-0345-M | DONE | Report | Guild | src/Findings/StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj - MAINT |
+| 1034 | AUDIT-0345-T | DONE | Report | Guild | src/Findings/StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj - TEST |
| 1035 | AUDIT-0345-A | TODO | Approval | Guild | src/Findings/StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj - APPLY |
-| 1036 | AUDIT-0346-M | TODO | Report | Guild | src/Gateway/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj - MAINT |
-| 1037 | AUDIT-0346-T | TODO | Report | Guild | src/Gateway/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj - TEST |
+| 1036 | AUDIT-0346-M | DONE | Report | Guild | src/Gateway/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj - MAINT |
+| 1037 | AUDIT-0346-T | DONE | Report | Guild | src/Gateway/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj - TEST |
| 1038 | AUDIT-0346-A | TODO | Approval | Guild | src/Gateway/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj - APPLY |
-| 1039 | AUDIT-0347-M | TODO | Report | Guild | src/Router/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj - MAINT |
-| 1040 | AUDIT-0347-T | TODO | Report | Guild | src/Router/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj - TEST |
+| 1039 | AUDIT-0347-M | DONE | Report | Guild | src/Router/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj - MAINT |
+| 1040 | AUDIT-0347-T | DONE | Report | Guild | src/Router/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj - TEST |
| 1041 | AUDIT-0347-A | TODO | Approval | Guild | src/Router/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj - APPLY |
-| 1042 | AUDIT-0348-M | TODO | Report | Guild | src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj - MAINT |
-| 1043 | AUDIT-0348-T | TODO | Report | Guild | src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj - TEST |
+| 1042 | AUDIT-0348-M | DONE | Report | Guild | src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj - MAINT |
+| 1043 | AUDIT-0348-T | DONE | Report | Guild | src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj - TEST |
| 1044 | AUDIT-0348-A | DONE | Waived (test project) | Guild | src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj - APPLY |
-| 1045 | AUDIT-0349-M | TODO | Report | Guild | src/Router/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj - MAINT |
-| 1046 | AUDIT-0349-T | TODO | Report | Guild | src/Router/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj - TEST |
+| 1045 | AUDIT-0349-M | DONE | Report | Guild | src/Router/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj - MAINT |
+| 1046 | AUDIT-0349-T | DONE | Report | Guild | src/Router/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj - TEST |
| 1047 | AUDIT-0349-A | DONE | Waived (test project) | Guild | src/Router/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj - APPLY |
-| 1048 | AUDIT-0350-M | TODO | Report | Guild | src/Graph/StellaOps.Graph.Api/StellaOps.Graph.Api.csproj - MAINT |
-| 1049 | AUDIT-0350-T | TODO | Report | Guild | src/Graph/StellaOps.Graph.Api/StellaOps.Graph.Api.csproj - TEST |
+| 1048 | AUDIT-0350-M | DONE | Report | Guild | src/Graph/StellaOps.Graph.Api/StellaOps.Graph.Api.csproj - MAINT |
+| 1049 | AUDIT-0350-T | DONE | Report | Guild | src/Graph/StellaOps.Graph.Api/StellaOps.Graph.Api.csproj - TEST |
| 1050 | AUDIT-0350-A | TODO | Approval | Guild | src/Graph/StellaOps.Graph.Api/StellaOps.Graph.Api.csproj - APPLY |
-| 1051 | AUDIT-0351-M | TODO | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Api.Tests/StellaOps.Graph.Api.Tests.csproj - MAINT |
-| 1052 | AUDIT-0351-T | TODO | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Api.Tests/StellaOps.Graph.Api.Tests.csproj - TEST |
+| 1051 | AUDIT-0351-M | DONE | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Api.Tests/StellaOps.Graph.Api.Tests.csproj - MAINT |
+| 1052 | AUDIT-0351-T | DONE | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Api.Tests/StellaOps.Graph.Api.Tests.csproj - TEST |
| 1053 | AUDIT-0351-A | DONE | Waived (test project) | Guild | src/Graph/__Tests/StellaOps.Graph.Api.Tests/StellaOps.Graph.Api.Tests.csproj - APPLY |
-| 1054 | AUDIT-0352-M | TODO | Report | Guild | src/Graph/StellaOps.Graph.Indexer/StellaOps.Graph.Indexer.csproj - MAINT |
-| 1055 | AUDIT-0352-T | TODO | Report | Guild | src/Graph/StellaOps.Graph.Indexer/StellaOps.Graph.Indexer.csproj - TEST |
+| 1054 | AUDIT-0352-M | DONE | Report | Guild | src/Graph/StellaOps.Graph.Indexer/StellaOps.Graph.Indexer.csproj - MAINT |
+| 1055 | AUDIT-0352-T | DONE | Report | Guild | src/Graph/StellaOps.Graph.Indexer/StellaOps.Graph.Indexer.csproj - TEST |
| 1056 | AUDIT-0352-A | TODO | Approval | Guild | src/Graph/StellaOps.Graph.Indexer/StellaOps.Graph.Indexer.csproj - APPLY |
-| 1057 | AUDIT-0353-M | TODO | Report | Guild | src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/StellaOps.Graph.Indexer.Persistence.csproj - MAINT |
-| 1058 | AUDIT-0353-T | TODO | Report | Guild | src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/StellaOps.Graph.Indexer.Persistence.csproj - TEST |
+| 1057 | AUDIT-0353-M | DONE | Report | Guild | src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/StellaOps.Graph.Indexer.Persistence.csproj - MAINT |
+| 1058 | AUDIT-0353-T | DONE | Report | Guild | src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/StellaOps.Graph.Indexer.Persistence.csproj - TEST |
| 1059 | AUDIT-0353-A | TODO | Approval | Guild | src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/StellaOps.Graph.Indexer.Persistence.csproj - APPLY |
-| 1060 | AUDIT-0354-M | TODO | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/StellaOps.Graph.Indexer.Persistence.Tests.csproj - MAINT |
-| 1061 | AUDIT-0354-T | TODO | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/StellaOps.Graph.Indexer.Persistence.Tests.csproj - TEST |
+| 1060 | AUDIT-0354-M | DONE | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/StellaOps.Graph.Indexer.Persistence.Tests.csproj - MAINT |
+| 1061 | AUDIT-0354-T | DONE | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/StellaOps.Graph.Indexer.Persistence.Tests.csproj - TEST |
| 1062 | AUDIT-0354-A | DONE | Waived (test project) | Guild | src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/StellaOps.Graph.Indexer.Persistence.Tests.csproj - APPLY |
-| 1063 | AUDIT-0355-M | TODO | Report | Guild | src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj - MAINT |
-| 1064 | AUDIT-0355-T | TODO | Report | Guild | src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj - TEST |
+| 1063 | AUDIT-0355-M | DONE | Report | Guild | src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj - MAINT |
+| 1064 | AUDIT-0355-T | DONE | Report | Guild | src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj - TEST |
| 1065 | AUDIT-0355-A | DONE | Waived (test project) | Guild | src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj - APPLY |
-| 1066 | AUDIT-0356-M | TODO | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj - MAINT |
-| 1067 | AUDIT-0356-T | TODO | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj - TEST |
+| 1066 | AUDIT-0356-M | DONE | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj - MAINT |
+| 1067 | AUDIT-0356-T | DONE | Report | Guild | src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj - TEST |
| 1068 | AUDIT-0356-A | DONE | Waived (test project) | Guild | src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj - APPLY |
-| 1069 | AUDIT-0357-M | TODO | Report | Guild | src/__Libraries/StellaOps.Infrastructure.EfCore/StellaOps.Infrastructure.EfCore.csproj - MAINT |
-| 1070 | AUDIT-0357-T | TODO | Report | Guild | src/__Libraries/StellaOps.Infrastructure.EfCore/StellaOps.Infrastructure.EfCore.csproj - TEST |
+| 1069 | AUDIT-0357-M | DONE | Report | Guild | src/__Libraries/StellaOps.Infrastructure.EfCore/StellaOps.Infrastructure.EfCore.csproj - MAINT |
+| 1070 | AUDIT-0357-T | DONE | Report | Guild | src/__Libraries/StellaOps.Infrastructure.EfCore/StellaOps.Infrastructure.EfCore.csproj - TEST |
| 1071 | AUDIT-0357-A | TODO | Approval | Guild | src/__Libraries/StellaOps.Infrastructure.EfCore/StellaOps.Infrastructure.EfCore.csproj - APPLY |
-| 1072 | AUDIT-0358-M | TODO | Report | Guild | src/__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj - MAINT |
-| 1073 | AUDIT-0358-T | TODO | Report | Guild | src/__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj - TEST |
+| 1072 | AUDIT-0358-M | DONE | Report | Guild | src/__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj - MAINT |
+| 1073 | AUDIT-0358-T | DONE | Report | Guild | src/__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj - TEST |
| 1074 | AUDIT-0358-A | TODO | Approval | Guild | src/__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj - APPLY |
-| 1075 | AUDIT-0359-M | TODO | Report | Guild | src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing/StellaOps.Infrastructure.Postgres.Testing.csproj - MAINT |
-| 1076 | AUDIT-0359-T | TODO | Report | Guild | src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing/StellaOps.Infrastructure.Postgres.Testing.csproj - TEST |
+| 1075 | AUDIT-0359-M | DONE | Report | Guild | src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing/StellaOps.Infrastructure.Postgres.Testing.csproj - MAINT |
+| 1076 | AUDIT-0359-T | DONE | Report | Guild | src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing/StellaOps.Infrastructure.Postgres.Testing.csproj - TEST |
| 1077 | AUDIT-0359-A | DONE | Waived (test project) | Guild | src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing/StellaOps.Infrastructure.Postgres.Testing.csproj - APPLY |
-| 1078 | AUDIT-0360-M | TODO | Report | Guild | src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj - MAINT |
-| 1079 | AUDIT-0360-T | TODO | Report | Guild | src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj - TEST |
+| 1078 | AUDIT-0360-M | DONE | Report | Guild | src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj - MAINT |
+| 1079 | AUDIT-0360-T | DONE | Report | Guild | src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj - TEST |
| 1080 | AUDIT-0360-A | DONE | Waived (test project) | Guild | src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj - APPLY |
-| 1081 | AUDIT-0361-M | TODO | Report | Guild | src/__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj - MAINT |
-| 1082 | AUDIT-0361-T | TODO | Report | Guild | src/__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj - TEST |
+| 1081 | AUDIT-0361-M | DONE | Report | Guild | src/__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj - MAINT |
+| 1082 | AUDIT-0361-T | DONE | Report | Guild | src/__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj - TEST |
| 1083 | AUDIT-0361-A | TODO | Approval | Guild | src/__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj - APPLY |
-| 1084 | AUDIT-0362-M | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.AirGap/StellaOps.Integration.AirGap.csproj - MAINT |
-| 1085 | AUDIT-0362-T | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.AirGap/StellaOps.Integration.AirGap.csproj - TEST |
+| 1084 | AUDIT-0362-M | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.AirGap/StellaOps.Integration.AirGap.csproj - MAINT |
+| 1085 | AUDIT-0362-T | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.AirGap/StellaOps.Integration.AirGap.csproj - TEST |
| 1086 | AUDIT-0362-A | DONE | Waived (test project) | Guild | src/__Tests/Integration/StellaOps.Integration.AirGap/StellaOps.Integration.AirGap.csproj - APPLY |
-| 1087 | AUDIT-0363-M | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Determinism/StellaOps.Integration.Determinism.csproj - MAINT |
-| 1088 | AUDIT-0363-T | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Determinism/StellaOps.Integration.Determinism.csproj - TEST |
+| 1087 | AUDIT-0363-M | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Determinism/StellaOps.Integration.Determinism.csproj - MAINT |
+| 1088 | AUDIT-0363-T | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Determinism/StellaOps.Integration.Determinism.csproj - TEST |
| 1089 | AUDIT-0363-A | DONE | Waived (test project) | Guild | src/__Tests/Integration/StellaOps.Integration.Determinism/StellaOps.Integration.Determinism.csproj - APPLY |
-| 1090 | AUDIT-0364-M | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.E2E/StellaOps.Integration.E2E.csproj - MAINT |
-| 1091 | AUDIT-0364-T | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.E2E/StellaOps.Integration.E2E.csproj - TEST |
+| 1090 | AUDIT-0364-M | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.E2E/StellaOps.Integration.E2E.csproj - MAINT |
+| 1091 | AUDIT-0364-T | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.E2E/StellaOps.Integration.E2E.csproj - TEST |
| 1092 | AUDIT-0364-A | DONE | Waived (test project) | Guild | src/__Tests/Integration/StellaOps.Integration.E2E/StellaOps.Integration.E2E.csproj - APPLY |
-| 1093 | AUDIT-0365-M | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Performance/StellaOps.Integration.Performance.csproj - MAINT |
-| 1094 | AUDIT-0365-T | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Performance/StellaOps.Integration.Performance.csproj - TEST |
+| 1093 | AUDIT-0365-M | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Performance/StellaOps.Integration.Performance.csproj - MAINT |
+| 1094 | AUDIT-0365-T | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Performance/StellaOps.Integration.Performance.csproj - TEST |
| 1095 | AUDIT-0365-A | DONE | Waived (test project) | Guild | src/__Tests/Integration/StellaOps.Integration.Performance/StellaOps.Integration.Performance.csproj - APPLY |
-| 1096 | AUDIT-0366-M | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Platform/StellaOps.Integration.Platform.csproj - MAINT |
-| 1097 | AUDIT-0366-T | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Platform/StellaOps.Integration.Platform.csproj - TEST |
+| 1096 | AUDIT-0366-M | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Platform/StellaOps.Integration.Platform.csproj - MAINT |
+| 1097 | AUDIT-0366-T | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Platform/StellaOps.Integration.Platform.csproj - TEST |
| 1098 | AUDIT-0366-A | DONE | Waived (test project) | Guild | src/__Tests/Integration/StellaOps.Integration.Platform/StellaOps.Integration.Platform.csproj - APPLY |
-| 1099 | AUDIT-0367-M | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.ProofChain/StellaOps.Integration.ProofChain.csproj - MAINT |
-| 1100 | AUDIT-0367-T | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.ProofChain/StellaOps.Integration.ProofChain.csproj - TEST |
+| 1099 | AUDIT-0367-M | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.ProofChain/StellaOps.Integration.ProofChain.csproj - MAINT |
+| 1100 | AUDIT-0367-T | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.ProofChain/StellaOps.Integration.ProofChain.csproj - TEST |
| 1101 | AUDIT-0367-A | DONE | Waived (test project) | Guild | src/__Tests/Integration/StellaOps.Integration.ProofChain/StellaOps.Integration.ProofChain.csproj - APPLY |
-| 1102 | AUDIT-0368-M | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Reachability/StellaOps.Integration.Reachability.csproj - MAINT |
-| 1103 | AUDIT-0368-T | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Reachability/StellaOps.Integration.Reachability.csproj - TEST |
+| 1102 | AUDIT-0368-M | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Reachability/StellaOps.Integration.Reachability.csproj - MAINT |
+| 1103 | AUDIT-0368-T | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Reachability/StellaOps.Integration.Reachability.csproj - TEST |
| 1104 | AUDIT-0368-A | DONE | Waived (test project) | Guild | src/__Tests/Integration/StellaOps.Integration.Reachability/StellaOps.Integration.Reachability.csproj - APPLY |
-| 1105 | AUDIT-0369-M | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Unknowns/StellaOps.Integration.Unknowns.csproj - MAINT |
-| 1106 | AUDIT-0369-T | TODO | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Unknowns/StellaOps.Integration.Unknowns.csproj - TEST |
+| 1105 | AUDIT-0369-M | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Unknowns/StellaOps.Integration.Unknowns.csproj - MAINT |
+| 1106 | AUDIT-0369-T | DONE | Report | Guild | src/__Tests/Integration/StellaOps.Integration.Unknowns/StellaOps.Integration.Unknowns.csproj - TEST |
| 1107 | AUDIT-0369-A | DONE | Waived (test project) | Guild | src/__Tests/Integration/StellaOps.Integration.Unknowns/StellaOps.Integration.Unknowns.csproj - APPLY |
-| 1108 | AUDIT-0370-M | TODO | Report | Guild | src/__Libraries/StellaOps.Interop/StellaOps.Interop.csproj - MAINT |
-| 1109 | AUDIT-0370-T | TODO | Report | Guild | src/__Libraries/StellaOps.Interop/StellaOps.Interop.csproj - TEST |
+| 1108 | AUDIT-0370-M | DONE | Report | Guild | src/__Libraries/StellaOps.Interop/StellaOps.Interop.csproj - MAINT |
+| 1109 | AUDIT-0370-T | DONE | Report | Guild | src/__Libraries/StellaOps.Interop/StellaOps.Interop.csproj - TEST |
| 1110 | AUDIT-0370-A | TODO | Approval | Guild | src/__Libraries/StellaOps.Interop/StellaOps.Interop.csproj - APPLY |
-| 1111 | AUDIT-0371-M | TODO | Report | Guild | src/__Tests/interop/StellaOps.Interop.Tests/StellaOps.Interop.Tests.csproj - MAINT |
-| 1112 | AUDIT-0371-T | TODO | Report | Guild | src/__Tests/interop/StellaOps.Interop.Tests/StellaOps.Interop.Tests.csproj - TEST |
+| 1111 | AUDIT-0371-M | DONE | Report | Guild | src/__Tests/interop/StellaOps.Interop.Tests/StellaOps.Interop.Tests.csproj - MAINT |
+| 1112 | AUDIT-0371-T | DONE | Report | Guild | src/__Tests/interop/StellaOps.Interop.Tests/StellaOps.Interop.Tests.csproj - TEST |
| 1113 | AUDIT-0371-A | DONE | Waived (test project) | Guild | src/__Tests/interop/StellaOps.Interop.Tests/StellaOps.Interop.Tests.csproj - APPLY |
-| 1114 | AUDIT-0372-M | TODO | Report | Guild | src/__Libraries/StellaOps.IssuerDirectory.Client/StellaOps.IssuerDirectory.Client.csproj - MAINT |
-| 1115 | AUDIT-0372-T | TODO | Report | Guild | src/__Libraries/StellaOps.IssuerDirectory.Client/StellaOps.IssuerDirectory.Client.csproj - TEST |
+| 1114 | AUDIT-0372-M | DONE | Report | Guild | src/__Libraries/StellaOps.IssuerDirectory.Client/StellaOps.IssuerDirectory.Client.csproj - MAINT |
+| 1115 | AUDIT-0372-T | DONE | Report | Guild | src/__Libraries/StellaOps.IssuerDirectory.Client/StellaOps.IssuerDirectory.Client.csproj - TEST |
| 1116 | AUDIT-0372-A | TODO | Approval | Guild | src/__Libraries/StellaOps.IssuerDirectory.Client/StellaOps.IssuerDirectory.Client.csproj - APPLY |
-| 1117 | AUDIT-0373-M | TODO | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core/StellaOps.IssuerDirectory.Core.csproj - MAINT |
-| 1118 | AUDIT-0373-T | TODO | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core/StellaOps.IssuerDirectory.Core.csproj - TEST |
+| 1117 | AUDIT-0373-M | DONE | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core/StellaOps.IssuerDirectory.Core.csproj - MAINT |
+| 1118 | AUDIT-0373-T | DONE | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core/StellaOps.IssuerDirectory.Core.csproj - TEST |
| 1119 | AUDIT-0373-A | TODO | Approval | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core/StellaOps.IssuerDirectory.Core.csproj - APPLY |
-| 1120 | AUDIT-0374-M | TODO | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/StellaOps.IssuerDirectory.Core.Tests.csproj - MAINT |
-| 1121 | AUDIT-0374-T | TODO | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/StellaOps.IssuerDirectory.Core.Tests.csproj - TEST |
+| 1120 | AUDIT-0374-M | DONE | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/StellaOps.IssuerDirectory.Core.Tests.csproj - MAINT |
+| 1121 | AUDIT-0374-T | DONE | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/StellaOps.IssuerDirectory.Core.Tests.csproj - TEST |
| 1122 | AUDIT-0374-A | DONE | Waived (test project) | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/StellaOps.IssuerDirectory.Core.Tests.csproj - APPLY |
-| 1123 | AUDIT-0375-M | TODO | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/StellaOps.IssuerDirectory.Infrastructure.csproj - MAINT |
-| 1124 | AUDIT-0375-T | TODO | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/StellaOps.IssuerDirectory.Infrastructure.csproj - TEST |
+| 1123 | AUDIT-0375-M | DONE | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/StellaOps.IssuerDirectory.Infrastructure.csproj - MAINT |
+| 1124 | AUDIT-0375-T | DONE | Report | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/StellaOps.IssuerDirectory.Infrastructure.csproj - TEST |
| 1125 | AUDIT-0375-A | TODO | Approval | Guild | src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/StellaOps.IssuerDirectory.Infrastructure.csproj - APPLY |
| 1126 | AUDIT-0376-M | TODO | Report | Guild | src/IssuerDirectory/__Libraries/StellaOps.IssuerDirectory.Persistence/StellaOps.IssuerDirectory.Persistence.csproj - MAINT |
| 1127 | AUDIT-0376-T | TODO | Report | Guild | src/IssuerDirectory/__Libraries/StellaOps.IssuerDirectory.Persistence/StellaOps.IssuerDirectory.Persistence.csproj - TEST |
@@ -2160,6 +2160,49 @@ Bulk task definitions (applies to every project row below):
## Execution Log
| Date (UTC) | Update | Owner |
| --- | --- | --- |
+| 2026-01-02 | Completed AUDIT-0085-A for Authority service (store determinism, replay tracking, token issuer IDs, and adapter/issuer tests). | Codex |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0358; created src/__Libraries/StellaOps.Infrastructure.Postgres/TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0359; created src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0360; created src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0361; created src/__Libraries/StellaOps.Ingestion.Telemetry/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0362; created src/__Tests/Integration/StellaOps.Integration.AirGap/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0363; created src/__Tests/Integration/StellaOps.Integration.Determinism/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0364; created src/__Tests/Integration/StellaOps.Integration.E2E/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0365; created src/__Tests/Integration/StellaOps.Integration.Performance/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0366; created src/__Tests/Integration/StellaOps.Integration.Platform/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0367; created src/__Tests/Integration/StellaOps.Integration.ProofChain/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0368; created src/__Tests/Integration/StellaOps.Integration.Reachability/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0369; created src/__Tests/Integration/StellaOps.Integration.Unknowns/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0370; created src/__Libraries/StellaOps.Interop/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0371; created src/__Tests/interop/StellaOps.Interop.Tests/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0372; created src/__Libraries/StellaOps.IssuerDirectory.Client/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0373; created src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0374; created src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0375; created src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0357; created src/__Libraries/StellaOps.Infrastructure.EfCore/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0356; created src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0355; created src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0354; created src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/AGENTS.md and TASKS.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0353; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0352; created src/Graph/StellaOps.Graph.Indexer/TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0350; created src/Graph/StellaOps.Graph.Api/TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0351; created src/Graph/__Tests/StellaOps.Graph.Api.Tests/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed AUDIT-0071-A for Attestor.Verify (DSSE PAE spec, SAN parsing, keyless chain extras, KMS count fix, distributed provider cleanup) and added Attestor.Verify tests; aligned Attestor.Core PAE and Attestor.Tests helper. | Codex |
+| 2026-01-02 | Completed AUDIT-0073-A for Audit ReplayToken (v2 docs, canonical versioning, expiration validation, CLI escaping, duplicate key guard) with new ReplayToken tests. | Codex |
+| 2026-01-02 | Completed AUDIT-0075-A for AuditPack (deterministic archives, canonical digests, safe extraction, signature verification, export signing, time/id injection) with new importer/attestation/export tests. | Codex |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0349; created src/Router/__Tests/StellaOps.Gateway.WebService.Tests/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0348; created src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0347; created src/Router/StellaOps.Gateway.WebService/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0346; created src/Gateway/StellaOps.Gateway.WebService/AGENTS.md and TASKS.md; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed AUDIT-0069-A for Attestor.Types.Generator (repo-root override, schema id alignment, strict validation, canonicalization, pattern checks, prune, tests). | Codex |
+| 2026-01-02 | Completed AUDIT-0064-A for Attestor.StandardPredicates (RFC8785 canonicalizer, registry normalization, parser metadata fixes, tests). | Codex |
+| 2026-01-02 | Completed AUDIT-0062-A for Attestor.ProofChain (time provider, merkle sorting, canonicalization, schema validation, tests); updated Concelier ProofService for JsonElement evidence payloads. | Codex |
+| 2026-01-02 | Completed AUDIT-0060-A for Attestor.Persistence (defaults, normalization, deterministic matching, perf script, tests). | Codex |
+| 2026-01-02 | Completed AUDIT-0051-A (Attestor.Envelope apply fixes) and updated tests. | Codex |
+| 2026-01-02 | Completed AUDIT-0053-A (Attestor.GraphRoot apply fixes) and updated tests. | Codex |
+| 2026-01-02 | Completed AUDIT-0055-A (Attestor.Infrastructure apply fixes) and added infrastructure tests. | Codex |
+| 2026-01-02 | Completed AUDIT-0056-A (Attestor.Oci apply fixes) and updated tests. | Codex |
| 2026-01-02 | Completed AUDIT-0034-A (AirGap.Time apply fixes) and updated tests. | Codex |
| 2026-01-02 | Completed AUDIT-0036-A (AOC guard library apply fixes) and updated tests. | Codex |
| 2026-01-02 | Completed AUDIT-0037-A (AOC analyzer apply fixes) and updated tests. | Codex |
@@ -2187,6 +2230,52 @@ Bulk task definitions (applies to every project row below):
| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0321; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
| 2026-01-02 | Created AGENTS.md and TASKS.md for Excititor Formats OpenVEX tests project. | Planning |
| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0322; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for Excititor Persistence library. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0323; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for Excititor Persistence tests project. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0324; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created TASKS.md for Excititor Policy library. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0325; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for Excititor Policy tests project. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0326; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created TASKS.md for Excititor WebService. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0327; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for Excititor WebService tests project. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0328; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created TASKS.md for Excititor Worker. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0329; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for Excititor Worker tests project. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0330; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for ExportCenter Client. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for ExportCenter Client tests project. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0331; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0332; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for ExportCenter Core. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0333; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for ExportCenter Infrastructure. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0334; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created TASKS.md for ExportCenter RiskBundles. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0335; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for ExportCenter tests project. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0336; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for ExportCenter WebService. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0337; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for ExportCenter Worker. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0338; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for Feedser BinaryAnalysis. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0339; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for Feedser Core. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0340; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for Feedser Core tests. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0341; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created TASKS.md for Findings Ledger. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0342; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for Findings Ledger tests project. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0343; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for Findings Ledger legacy tests project. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0344; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Created AGENTS.md and TASKS.md for Findings Ledger WebService. | Planning |
+| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0345; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
| 2026-01-02 | Created TASKS.md for Excititor Connectors Ubuntu CSAF library. | Planning |
| 2026-01-02 | Created AGENTS.md and TASKS.md for Excititor Connectors Ubuntu CSAF tests project. | Planning |
| 2026-01-02 | Completed MAINT/TEST audits for AUDIT-0310; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
@@ -2544,6 +2633,10 @@ Bulk task definitions (applies to every project row below):
| 2025-12-30 | Completed MAINT/TEST audits for AUDIT-0064 to AUDIT-0065; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
| 2025-12-30 | Created TASKS.md for Attestor ProofChain library and AGENTS.md + TASKS.md for Attestor ProofChain tests. | Planning |
| 2025-12-30 | Completed MAINT/TEST audits for AUDIT-0062 to AUDIT-0063; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed AUDIT-0078-A Auth Abstractions updates (scope ordering, warning discipline, coverage gaps). | Guild |
+| 2026-01-02 | Completed AUDIT-0080-A Auth Client updates (retries, shared cache, file hardening, tests). | Guild |
+| 2026-01-02 | Completed AUDIT-0082-A Auth Security updates (DPoP validation hardening, nonce normalization, tests); added Auth Security tests project + AGENTS/TASKS. | Guild |
+| 2026-01-02 | Completed AUDIT-0083-A Auth Server Integration updates (metadata fallback, option refresh, scope normalization, tests). | Guild |
| 2025-12-30 | Created TASKS.md for Attestor persistence library and AGENTS.md + TASKS.md for Attestor persistence tests. | Planning |
| 2025-12-30 | Completed MAINT/TEST audits for AUDIT-0060 to AUDIT-0061; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
| 2025-12-30 | Created AGENTS.md and TASKS.md for Attestor offline library and tests. | Planning |
@@ -2558,6 +2651,7 @@ Bulk task definitions (applies to every project row below):
| 2025-12-30 | Completed MAINT/TEST audits for AUDIT-0049 to AUDIT-0050; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
| 2025-12-30 | Created AGENTS.md and TASKS.md for Attestor bundling library and tests. | Planning |
| 2025-12-30 | Completed MAINT/TEST audits for AUDIT-0047 to AUDIT-0048; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
+| 2026-01-02 | Completed AUDIT-0047-A (bundling validation, defaults, and tests). | Guild |
| 2025-12-30 | Created AGENTS.md and TASKS.md for Attestor bundle library and tests. | Planning |
| 2025-12-30 | Completed MAINT/TEST audits for AUDIT-0045 to AUDIT-0046; report updated in docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md. | Planning |
| 2026-01-02 | Completed AUDIT-0045-A (bundle validation, verifier hardening, tests). | Guild |
@@ -2606,6 +2700,7 @@ Bulk task definitions (applies to every project row below):
- Status: Dispositions recorded; APPLY tasks waived for test/example/benchmark projects, several Tools/Scheduler APPLY tasks applied, remaining non-test APPLY tasks still pending implementation.
- Approval gate: APPLY tasks require explicit approval based on docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md.
- Decision: APPLY tasks only proceed after audit report review and explicit approval.
+- Note: Authority service Program.cs decomposition deferred for a dedicated refactor task; audit remediation focused on determinism, replay tracking, and test coverage.
- Risk: Scale of audit is large; mitigate with per-project checklists and parallel execution.
- Risk: Coverage measurement can be inconsistent; mitigate with deterministic test runs and documented tooling.
- Note: GHSA parity fixtures moved to the GHSA test fixture directory; OSV parity fixture resolution updated accordingly (cross-module change recorded).
diff --git a/docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md b/docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md
index 442a4c966..f49eb5c49 100644
--- a/docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md
+++ b/docs/implplan/SPRINT_20251229_049_BE_csproj_audit_report.md
@@ -1,7 +1,7 @@
# Sprint 20251229_049_BE - C# Audit Report (Initial Tranche)
## Scope
-- Projects audited in this tranche: 322 (Router examples + Tools (7) + Findings LedgerReplayHarness x2 + Scheduler.Backfill + AdvisoryAI core + AdvisoryAI hosting + AdvisoryAI tests + AdvisoryAI web service + AdvisoryAI worker + AirGap bundle library + AirGap bundle tests + AirGap controller + AirGap controller tests + AirGap importer + AirGap importer tests + AirGap persistence + AirGap persistence tests + AirGap policy + AirGap policy analyzers + AirGap policy analyzer tests + AirGap policy tests + AirGap time + AirGap time tests + AOC guard library + AOC analyzers + AOC analyzer tests + AOC ASP.NET Core + AOC ASP.NET Core tests + AOC tests + Architecture tests + Attestation library + Attestation tests + Attestor bundle library + Attestor bundle tests + Attestor bundling library + Attestor bundling tests + Attestor core + Attestor core tests + Attestor envelope + Attestor envelope tests + Attestor GraphRoot library + Attestor GraphRoot tests + Attestor infrastructure + Attestor OCI library + Attestor OCI tests + Attestor offline library + Attestor offline tests + Attestor persistence library + Attestor persistence tests + Attestor proof chain library + Attestor proof chain tests + Attestor standard predicates library + Attestor standard predicates tests + Attestor tests + Attestor TrustVerdict library + Attestor TrustVerdict tests + Attestor Types generator tool + Attestor Types tests + Attestor Verify + Attestor WebService + Audit ReplayToken library + Audit ReplayToken tests + AuditPack library + AuditPack tests (libraries) + AuditPack unit tests + Auth Abstractions + Auth Abstractions tests + Auth Client + Auth Client tests + Auth Security + Auth Server Integration + Auth Server Integration tests + Authority service + Authority tests + Authority Core + Authority Core tests + Authority Persistence + Authority Persistence tests + Authority LDAP plugin + Authority LDAP plugin tests + Authority OIDC plugin + Authority OIDC plugin tests + Authority SAML plugin + Authority SAML plugin tests + Authority Standard plugin + Authority Standard plugin tests + Authority Plugin Abstractions + Authority Plugin Abstractions tests + Binary Lookup benchmark + LinkNotMerge benchmark + LinkNotMerge benchmark tests + LinkNotMerge VEX benchmark + LinkNotMerge VEX benchmark tests + Notify benchmark + Notify benchmark tests + PolicyEngine benchmark + ProofChain benchmark + Scanner Analyzers benchmark + Scanner Analyzers benchmark tests + BinaryIndex Builders library + BinaryIndex Builders tests + BinaryIndex Cache library + BinaryIndex Contracts library + BinaryIndex Core library + BinaryIndex Core tests + BinaryIndex Corpus library + BinaryIndex Corpus Alpine library + BinaryIndex Corpus Debian library + BinaryIndex Corpus RPM library + BinaryIndex Fingerprints library + BinaryIndex Fingerprints tests + BinaryIndex FixIndex library + BinaryIndex Persistence library + BinaryIndex Persistence tests + BinaryIndex VexBridge library + BinaryIndex VexBridge tests + BinaryIndex WebService + Canonical Json library + Canonical Json tests + Canonicalization library + Canonicalization tests + Cartographer + Cartographer tests + Chaos Router tests + CLI + CLI AOC plugin + CLI NonCore plugin + CLI Symbols plugin + CLI Verdict plugin + CLI VEX plugin + CLI tests + Concelier analyzers + Concelier Valkey cache + Concelier Valkey cache tests + Concelier ACSC connector + Concelier ACSC connector tests + Concelier CCCS connector + Concelier CCCS connector tests + Concelier CERT-Bund connector + Concelier CERT-Bund connector tests + Concelier CERT/CC connector + Concelier CERT/CC connector tests + Concelier CERT-FR connector + Concelier CERT-FR connector tests + Concelier CERT-In connector + Concelier CERT-In connector tests + Concelier Connector Common + Concelier Connector Common tests + Concelier CVE connector + Concelier CVE connector tests + Concelier Distro.Alpine connector + Concelier Distro.Alpine connector tests + Concelier Distro.Debian connector + Concelier Distro.Debian connector tests + Concelier Distro.RedHat connector + Concelier Distro.RedHat connector tests + Concelier Distro.Suse connector + Concelier Distro.Suse connector tests + Concelier Distro.Ubuntu connector + Concelier Distro.Ubuntu connector tests + Concelier EPSS connector + Concelier EPSS connector tests + Concelier GHSA connector + Concelier GHSA connector tests + Concelier ICS CISA connector + Concelier ICS CISA connector tests + Concelier ICS Kaspersky connector + Concelier ICS Kaspersky connector tests + Concelier JVN connector + Concelier JVN connector tests + Concelier KEV connector + Concelier KEV connector tests + Concelier KISA connector + Concelier KISA connector tests + Concelier NVD connector + Concelier NVD connector tests + Concelier OSV connector + Concelier OSV connector tests + Concelier Ru.Bdu connector + Concelier Ru.Bdu connector tests + Concelier Ru.Nkcki connector + Concelier Ru.Nkcki connector tests + Concelier StellaOpsMirror connector + Concelier StellaOpsMirror connector tests + Concelier Vndr.Adobe connector + Concelier Vndr.Adobe connector tests + Concelier Vndr.Apple connector + Concelier Vndr.Apple connector tests + Concelier Vndr.Chromium connector + Concelier Vndr.Chromium connector tests + Concelier Vndr.Cisco connector + Concelier Vndr.Cisco connector tests + Concelier Vndr.Msrc connector + Concelier Vndr.Msrc connector tests + Concelier Vndr.Oracle connector + Concelier Vndr.Oracle connector tests + Concelier Vndr.Vmware connector + Concelier Vndr.Vmware connector tests + Concelier Core library + Concelier Core tests + Concelier JSON exporter + Concelier JSON exporter tests + Concelier TrivyDb exporter + Concelier TrivyDb exporter tests + Concelier Federation library + Concelier Federation tests + Concelier Integration tests + Concelier Interest library + Concelier Interest tests + Concelier Merge library + Concelier Merge analyzers + Concelier Merge analyzers tests + Concelier Merge tests + Concelier Models library + Concelier Models tests + Concelier Normalization library + Concelier Normalization tests + Concelier Persistence library + Concelier Persistence tests + Concelier ProofService library + Concelier ProofService Postgres library + Concelier ProofService Postgres tests + Concelier RawModels library + Concelier RawModels tests + Concelier SbomIntegration library + Concelier SbomIntegration tests + Concelier SourceIntel library + Concelier SourceIntel tests + Concelier Testing library + Concelier WebService + Concelier WebService tests + StellaOps.Configuration + StellaOps.Configuration tests + StellaOps.Cryptography + Crypto Profiles (src/Cryptography/StellaOps.Cryptography) + Crypto DependencyInjection + Crypto Kms + Crypto Kms Tests + Crypto BouncyCastle plugin + CryptoPro plugin + Crypto eIDAS plugin + Crypto eIDAS tests + Crypto OfflineVerification plugin + Crypto OfflineVerification tests + Crypto OpenSslGost plugin + Crypto Pkcs11Gost plugin + Crypto PqSoft plugin + Crypto SimRemote plugin + Crypto SmRemote plugin + Crypto SmRemote tests + Crypto SmSoft plugin + Crypto SmSoft tests + Crypto WineCsp plugin + Crypto PluginLoader + Crypto PluginLoader tests + Crypto Profiles Ecdsa + Crypto Profiles EdDsa + Crypto OfflineVerification provider + Crypto Tests (__Tests) + Crypto Tests (libraries) + DeltaVerdict library + DeltaVerdict tests + DependencyInjection library + Determinism Abstractions library + Determinism Analyzers + Determinism Analyzers tests + Evidence library + Evidence Bundle library + Evidence Bundle tests + Evidence Core library + Evidence Core tests + Evidence Persistence library + Evidence Persistence tests + Evidence tests + Evidence Locker Core library + Evidence Locker Infrastructure library + Evidence Locker Tests + Evidence Locker WebService + Evidence Locker Worker + Excititor ArtifactStores S3 library + Excititor ArtifactStores S3 tests + Excititor Attestation library + Excititor Attestation tests + Excititor Connectors Abstractions library + Excititor Connectors Cisco CSAF library + Excititor Connectors Cisco CSAF tests + Excititor Connectors MSRC CSAF library + Excititor Connectors MSRC CSAF tests + Excititor Connectors OCI OpenVEX Attest library + Excititor Connectors OCI OpenVEX Attest tests + Excititor Connectors Oracle CSAF library + Excititor Connectors Oracle CSAF tests + Excititor Connectors RedHat CSAF library + Excititor Connectors RedHat CSAF tests + Excititor Connectors SUSE Rancher VEX Hub library + Excititor Connectors SUSE Rancher VEX Hub tests + Excititor Connectors Ubuntu CSAF library + Excititor Connectors Ubuntu CSAF tests + Excititor Core library + Excititor Core tests + Excititor Core unit tests + Excititor Export library + Excititor Export tests + Excititor Formats CSAF library + Excititor Formats CSAF tests + Excititor Formats CycloneDX library + Excititor Formats CycloneDX tests + Excititor Formats OpenVEX library + Excititor Formats OpenVEX tests).
-- MAINT + TEST tasks completed for AUDIT-0001 to AUDIT-0322.
+- Projects audited in this tranche: 375 (Router examples + Tools (7) + Findings LedgerReplayHarness x2 + Scheduler.Backfill + AdvisoryAI core + AdvisoryAI hosting + AdvisoryAI tests + AdvisoryAI web service + AdvisoryAI worker + AirGap bundle library + AirGap bundle tests + AirGap controller + AirGap controller tests + AirGap importer + AirGap importer tests + AirGap persistence + AirGap persistence tests + AirGap policy + AirGap policy analyzers + AirGap policy analyzer tests + AirGap policy tests + AirGap time + AirGap time tests + AOC guard library + AOC analyzers + AOC analyzer tests + AOC ASP.NET Core + AOC ASP.NET Core tests + AOC tests + Architecture tests + Attestation library + Attestation tests + Attestor bundle library + Attestor bundle tests + Attestor bundling library + Attestor bundling tests + Attestor core + Attestor core tests + Attestor envelope + Attestor envelope tests + Attestor GraphRoot library + Attestor GraphRoot tests + Attestor infrastructure + Attestor OCI library + Attestor OCI tests + Attestor offline library + Attestor offline tests + Attestor persistence library + Attestor persistence tests + Attestor proof chain library + Attestor proof chain tests + Attestor standard predicates library + Attestor standard predicates tests + Attestor tests + Attestor TrustVerdict library + Attestor TrustVerdict tests + Attestor Types generator tool + Attestor Types tests + Attestor Verify + Attestor WebService + Audit ReplayToken library + Audit ReplayToken tests + AuditPack library + AuditPack tests (libraries) + AuditPack unit tests + Auth Abstractions + Auth Abstractions tests + Auth Client + Auth Client tests + Auth Security + Auth Server Integration + Auth Server Integration tests + Authority service + Authority tests + Authority Core + Authority Core tests + Authority Persistence + Authority Persistence tests + Authority LDAP plugin + Authority LDAP plugin tests + Authority OIDC plugin + Authority OIDC plugin tests + Authority SAML plugin + Authority SAML plugin tests + Authority Standard plugin + Authority Standard plugin tests + Authority Plugin Abstractions + Authority Plugin Abstractions tests + Binary Lookup benchmark + LinkNotMerge benchmark + LinkNotMerge benchmark tests + LinkNotMerge VEX benchmark + LinkNotMerge VEX benchmark tests + Notify benchmark + Notify benchmark tests + PolicyEngine benchmark + ProofChain benchmark + Scanner Analyzers benchmark + Scanner Analyzers benchmark tests + BinaryIndex Builders library + BinaryIndex Builders tests + BinaryIndex Cache library + BinaryIndex Contracts library + BinaryIndex Core library + BinaryIndex Core tests + BinaryIndex Corpus library + BinaryIndex Corpus Alpine library + BinaryIndex Corpus Debian library + BinaryIndex Corpus RPM library + BinaryIndex Fingerprints library + BinaryIndex Fingerprints tests + BinaryIndex FixIndex library + BinaryIndex Persistence library + BinaryIndex Persistence tests + BinaryIndex VexBridge library + BinaryIndex VexBridge tests + BinaryIndex WebService + Canonical Json library + Canonical Json tests + Canonicalization library + Canonicalization tests + Cartographer + Cartographer tests + Chaos Router tests + CLI + CLI AOC plugin + CLI NonCore plugin + CLI Symbols plugin + CLI Verdict plugin + CLI VEX plugin + CLI tests + Concelier analyzers + Concelier Valkey cache + Concelier Valkey cache tests + Concelier ACSC connector + Concelier ACSC connector tests + Concelier CCCS connector + Concelier CCCS connector tests + Concelier CERT-Bund connector + Concelier CERT-Bund connector tests + Concelier CERT/CC connector + Concelier CERT/CC connector tests + Concelier CERT-FR connector + Concelier CERT-FR connector tests + Concelier CERT-In connector + Concelier CERT-In connector tests + Concelier Connector Common + Concelier Connector Common tests + Concelier CVE connector + Concelier CVE connector tests + Concelier Distro.Alpine connector + Concelier Distro.Alpine connector tests + Concelier Distro.Debian connector + Concelier Distro.Debian connector tests + Concelier Distro.RedHat connector + Concelier Distro.RedHat connector tests + Concelier Distro.Suse connector + Concelier Distro.Suse connector tests + Concelier Distro.Ubuntu connector + Concelier Distro.Ubuntu connector tests + Concelier EPSS connector + Concelier EPSS connector tests + Concelier GHSA connector + Concelier GHSA connector tests + Concelier ICS CISA connector + Concelier ICS CISA connector tests + Concelier ICS Kaspersky connector + Concelier ICS Kaspersky connector tests + Concelier JVN connector + Concelier JVN connector tests + Concelier KEV connector + Concelier KEV connector tests + Concelier KISA connector + Concelier KISA connector tests + Concelier NVD connector + Concelier NVD connector tests + Concelier OSV connector + Concelier OSV connector tests + Concelier Ru.Bdu connector + Concelier Ru.Bdu connector tests + Concelier Ru.Nkcki connector + Concelier Ru.Nkcki connector tests + Concelier StellaOpsMirror connector + Concelier StellaOpsMirror connector tests + Concelier Vndr.Adobe connector + Concelier Vndr.Adobe connector tests + Concelier Vndr.Apple connector + Concelier Vndr.Apple connector tests + Concelier Vndr.Chromium connector + Concelier Vndr.Chromium connector tests + Concelier Vndr.Cisco connector + Concelier Vndr.Cisco connector tests + Concelier Vndr.Msrc connector + Concelier Vndr.Msrc connector tests + Concelier Vndr.Oracle connector + Concelier Vndr.Oracle connector tests + Concelier Vndr.Vmware connector + Concelier Vndr.Vmware connector tests + Concelier Core library + Concelier Core tests + Concelier JSON exporter + Concelier JSON exporter tests + Concelier TrivyDb exporter + Concelier TrivyDb exporter tests + Concelier Federation library + Concelier Federation tests + Concelier Integration tests + Concelier Interest library + Concelier Interest tests + Concelier Merge library + Concelier Merge analyzers + Concelier Merge analyzers tests + Concelier Merge tests + Concelier Models library + Concelier Models tests + Concelier Normalization library + Concelier Normalization tests + Concelier Persistence library + Concelier Persistence tests + Concelier ProofService library + Concelier ProofService Postgres library + Concelier ProofService Postgres tests + Concelier RawModels library + Concelier RawModels tests + Concelier SbomIntegration library + Concelier SbomIntegration tests + Concelier SourceIntel library + Concelier SourceIntel tests + Concelier Testing library + Concelier WebService + Concelier WebService tests + StellaOps.Configuration + StellaOps.Configuration tests + StellaOps.Cryptography + Crypto Profiles (src/Cryptography/StellaOps.Cryptography) + Crypto DependencyInjection + Crypto Kms + Crypto Kms Tests + Crypto BouncyCastle plugin + CryptoPro plugin + Crypto eIDAS plugin + Crypto eIDAS tests + Crypto OfflineVerification plugin + Crypto OfflineVerification tests + Crypto OpenSslGost plugin + Crypto Pkcs11Gost plugin + Crypto PqSoft plugin + Crypto SimRemote plugin + Crypto SmRemote plugin + Crypto SmRemote tests + Crypto SmSoft plugin + Crypto SmSoft tests + Crypto WineCsp plugin + Crypto PluginLoader + Crypto PluginLoader tests + Crypto Profiles Ecdsa + Crypto Profiles EdDsa + Crypto OfflineVerification provider + Crypto Tests (__Tests) + Crypto Tests (libraries) + DeltaVerdict library + DeltaVerdict tests + DependencyInjection library + Determinism Abstractions library + Determinism Analyzers + Determinism Analyzers tests + Evidence library + Evidence Bundle library + Evidence Bundle tests + Evidence Core library + Evidence Core tests + Evidence Persistence library + Evidence Persistence tests + Evidence tests + Evidence Locker Core library + Evidence Locker Infrastructure library + Evidence Locker Tests + Evidence Locker WebService + Evidence Locker Worker + Excititor ArtifactStores S3 library + Excititor ArtifactStores S3 tests + Excititor Attestation library + Excititor Attestation tests + Excititor Connectors Abstractions library + Excititor Connectors Cisco CSAF library + Excititor Connectors Cisco CSAF tests + Excititor Connectors MSRC CSAF library + Excititor Connectors MSRC CSAF tests + Excititor Connectors OCI OpenVEX Attest library + Excititor Connectors OCI OpenVEX Attest tests + Excititor Connectors Oracle CSAF library + Excititor Connectors Oracle CSAF tests + Excititor Connectors RedHat CSAF library + Excititor Connectors RedHat CSAF tests + Excititor Connectors SUSE Rancher VEX Hub library + Excititor Connectors SUSE Rancher VEX Hub tests + Excititor Connectors Ubuntu CSAF library + Excititor Connectors Ubuntu CSAF tests + Excititor Core library + Excititor Core tests + Excititor Core unit tests + Excititor Export library + Excititor Export tests + Excititor Formats CSAF library + Excititor Formats CSAF tests + Excititor Formats CycloneDX library + Excititor Formats CycloneDX tests + Excititor Formats OpenVEX library + Excititor Formats OpenVEX tests + Excititor Persistence library + Excititor Persistence tests + Excititor Policy library + Excititor Policy tests + Excititor WebService + Excititor WebService tests + Excititor Worker + Excititor Worker tests + ExportCenter Client + ExportCenter Client tests + ExportCenter Core + ExportCenter Infrastructure + ExportCenter RiskBundles + ExportCenter Tests + ExportCenter WebService + ExportCenter Worker + Feedser BinaryAnalysis + Feedser Core + Feedser Core tests + Findings Ledger + Findings Ledger tests + Findings Ledger legacy tests + Findings Ledger WebService + Gateway WebService + Router Gateway WebService + Gateway WebService tests + Router Gateway WebService tests + Graph Api + Graph Api tests + Graph Indexer + Graph Indexer Persistence + Graph Indexer Persistence tests + Graph Indexer tests (legacy path) + Graph Indexer tests + StellaOps.Infrastructure.EfCore + StellaOps.Infrastructure.Postgres + StellaOps.Infrastructure.Postgres.Testing + StellaOps.Infrastructure.Postgres.Tests + StellaOps.Ingestion.Telemetry + StellaOps.Integration.AirGap + StellaOps.Integration.Determinism + StellaOps.Integration.E2E + StellaOps.Integration.Performance + StellaOps.Integration.Platform + StellaOps.Integration.ProofChain + StellaOps.Integration.Reachability + StellaOps.Integration.Unknowns + StellaOps.Interop + StellaOps.Interop.Tests + StellaOps.IssuerDirectory.Client + StellaOps.IssuerDirectory.Core + StellaOps.IssuerDirectory.Core.Tests + StellaOps.IssuerDirectory.Infrastructure).
+- MAINT + TEST tasks completed for AUDIT-0001 to AUDIT-0375.
- APPLY tasks remain pending approval for non-example projects.
## Findings
### src/Router/examples/Examples.Billing.Microservice/Examples.Billing.Microservice.csproj
@@ -3050,6 +3050,520 @@
- TEST: Coverage exists for OpenVEX exporter output, normalizer mapping, and statement merge conflict handling.
- TEST: Missing tests for missing statement/product handling, deterministic ID generation, justification conflict diagnostics, trust-weight ordering, invalid JSON handling, and merge trace serialization.
- Disposition: waived (test project; no apply changes).
+### src/Excititor/__Libraries/StellaOps.Excititor.Persistence/StellaOps.Excititor.Persistence.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed.
+- MAINT: VexDelta defaults to Guid.NewGuid and DateTimeOffset.UtcNow (`src/Excititor/__Libraries/StellaOps.Excititor.Persistence/Repositories/IVexDeltaRepository.cs`), making IDs/timestamps nondeterministic; require explicit values or inject providers.
+- MAINT: PostgresConnectorStateRepository.SaveAsync falls back to DateTimeOffset.UtcNow when LastUpdated is missing (`src/Excititor/__Libraries/StellaOps.Excititor.Persistence/Postgres/Repositories/PostgresConnectorStateRepository.cs`); prefer explicit timestamps or a TimeProvider for deterministic persistence.
+- MAINT: PostgresVexDeltaRepository.AddBatchAsync assumes all deltas share the first tenant and does not validate tenant consistency (`src/Excititor/__Libraries/StellaOps.Excititor.Persistence/Postgres/Repositories/PostgresVexDeltaRepository.cs`).
+- MAINT: PostgresVexDeltaRepository.MapDelta reads created_at via GetDateTime and relies on implicit conversion, unlike other repos that normalize DateTimeOffset; explicitly normalize to UTC (`src/Excititor/__Libraries/StellaOps.Excititor.Persistence/Postgres/Repositories/PostgresVexDeltaRepository.cs`).
+- MAINT: PostgresVexTimelineEventStore serializes attributes with default JsonSerializer options and swallows parse errors, which can hide malformed payloads and lead to nondeterministic key ordering (`src/Excititor/__Libraries/StellaOps.Excititor.Persistence/Postgres/Repositories/PostgresVexTimelineEventStore.cs`).
+- TEST: Coverage exists for append-only linkset store, observation store, provider store, attestation store, timeline event store, and migration/idempotency/determinism checks.
+- TEST: Missing tests for VEX delta repository CRUD/ordering, VEX statement repository CRUD/precedence, raw document canonicalization/inline vs blob paths, connector state serialization, and append-only checkpoint store behavior.
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; require explicit ID/timestamp inputs (or inject providers); validate tenant consistency in batch inserts; normalize created_at to DateTimeOffset UTC; make timeline event attribute JSON deterministic with logged parse failures; add tests for deltas/raw store/connector state/checkpoint store and statement ordering.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Excititor/__Tests/StellaOps.Excititor.Persistence.Tests/StellaOps.Excititor.Persistence.Tests.csproj
+- MAINT: TreatWarningsAsErrors is not set in the project file; warning discipline is relaxed.
+- MAINT: Multiple tests use Guid.NewGuid/Random.Shared/DateTimeOffset.UtcNow in fixtures (VexQueryDeterminismTests, VexStatementIdempotencyTests, PostgresVexAttestationStoreTests, PostgresVexObservationStoreTests, PostgresVexTimelineEventStoreTests), reducing deterministic replay.
+- TEST: Coverage exists for append-only linkset store, observation store, provider store, attestation store, timeline event store, migrations, and linkset determinism/idempotency.
+- TEST: Missing tests for VEX delta repository, raw store canonicalization and cursor paging, connector state repository serialization, VEX statement repository CRUD/precedence ordering, and append-only checkpoint store behavior.
+- Disposition: waived (test project; no apply changes).
+### src/Excititor/__Libraries/StellaOps.Excititor.Policy/StellaOps.Excititor.Policy.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed.
+- MAINT: VexPolicyProvider logs every issue on every GetSnapshot call when issues persist, which can spam logs if the snapshot is queried frequently (`src/Excititor/__Libraries/StellaOps.Excititor.Policy/IVexPolicyProvider.cs`).
+- MAINT: VexPolicyBinder reads entire policy streams into memory without size guards (`src/Excititor/__Libraries/StellaOps.Excititor.Policy/VexPolicyBinder.cs`).
+- TEST: Coverage exists for policy provider defaults and override clamping.
+- TEST: Missing tests for JSON/YAML binder parsing errors, diagnostics report ordering/recommendations, digest stability, weight ceiling/coefficient clamping, and provider override key normalization.
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; log policy issues only on revision changes or with throttling; add size/length guardrails for streamed policy input; add tests for binder/diagnostics/digest and normalization edge cases.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Excititor/__Tests/StellaOps.Excititor.Policy.Tests/StellaOps.Excititor.Policy.Tests.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed.
+- MAINT: VexPolicyProviderTests uses DateTimeOffset.UtcNow when building claims, introducing nondeterministic timestamps.
+- TEST: Coverage exists for policy provider defaults and overrides/clamps.
+- TEST: Missing tests for JSON/YAML binder parsing errors, diagnostics report ordering/recommendations, digest stability, weight ceiling/coefficient clamping, and provider override key normalization.
+- Disposition: waived (test project; no apply changes).
+### src/Excititor/StellaOps.Excititor.WebService/StellaOps.Excititor.WebService.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed.
+- MAINT: Program.cs registers TimeProvider.System and IMemoryCache twice; redundant registrations can confuse DI resolution or IEnumerable usage (`src/Excititor/StellaOps.Excititor.WebService/Program.cs`).
+- MAINT: Program.cs is a monolithic composition root with endpoints and large inline OpenAPI JSON; risk of drift and harder maintenance (`src/Excititor/StellaOps.Excititor.WebService/Program.cs`).
+- MAINT: Candidate approve/reject endpoints generate CVE IDs using candidateId.GetHashCode and statement IDs using Guid.NewGuid; GetHashCode is nondeterministic across processes and responses vary run-to-run (`src/Excititor/StellaOps.Excititor.WebService/Program.cs`).
+- MAINT: Airgap import timeline events generate a new trace ID when missing, which makes emitted events nondeterministic for audit replay (`src/Excititor/StellaOps.Excititor.WebService/Program.cs`).
+- MAINT: /excititor/statements ingestion uses VexStatementEntry.ToDomainClaim which throws on invalid DocumentUri; no validation guard returns 400 on bad input (`src/Excititor/StellaOps.Excititor.WebService/Program.cs`).
+- MAINT: IngestEndpoints inject TimeProvider but do not use it; remove or wire into responses for determinism (`src/Excititor/StellaOps.Excititor.WebService/Endpoints/IngestEndpoints.cs`).
+- MAINT: VexIngestOrchestrator uses Guid.NewGuid for run IDs that are returned to clients; tests cannot easily make deterministic assertions (`src/Excititor/StellaOps.Excititor.WebService/Services/VexIngestOrchestrator.cs`).
+- TEST: Coverage exists for airgap import endpoint/validator, airgap mode enforcer, evidence telemetry, evidence locker endpoints, graph overlay/status/tooltip factories, attestation verify endpoint, OpenAPI discovery, and policy endpoints.
+- TEST: Missing tests for ingest run/resume/reconcile endpoints, mirror endpoints, VEX raw endpoints, observation projection/list endpoints, linkset list endpoints, evidence chunk service/endpoint, status/resolve/risk feed endpoints, observability endpoints, and OpenAPI contract snapshots.
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; consolidate DI registration; split Program.cs endpoint wiring/OpenAPI spec into dedicated modules or builders; replace GetHashCode/Guid.NewGuid response IDs with deterministic IDs or persisted values; add input validation for DocumentUri; use a GUID provider for ingest run IDs; add tests for missing endpoints and OpenAPI contract snapshot.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed.
+- MAINT: Compile Remove="**/*.cs" with a partial include list means many test files are not compiled (BatchIngestValidationTests.cs, GraphOverlayCacheTests.cs, GraphOverlayStoreTests.cs, IngestEndpointsTests.cs, MirrorEndpointsTests.cs, VexRawEndpointsTests.cs, VexObservationProjectionServiceTests.cs, VexObservationListEndpointTests.cs, VexLinksetListEndpointTests.cs, VexGuardSchemaTests.cs, VexEvidenceChunkServiceTests.cs, VexEvidenceChunksEndpointTests.cs, VexAttestationLinkEndpointTests.cs, VerificationIntegrationTests.cs, StatusEndpointTests.cs, RiskFeedEndpointsTests.cs, ResolveEndpointTests.cs, Contract/OpenApiContractSnapshotTests.cs, ObservabilityEndpointTests.cs, Auth/AuthenticationEnforcementTests.cs, Observability/OTelTraceAssertionTests.cs).
+- MAINT: Included tests use DateTimeOffset.UtcNow and Guid.NewGuid in fixtures, which reduces determinism (AirgapImportValidatorTests.cs, AirgapImportEndpointTests.cs, EvidenceTelemetryTests.cs, EvidenceLockerEndpointTests.cs, GraphOverlayFactoryTests.cs, GraphStatusFactoryTests.cs, GraphTooltipFactoryTests.cs, TestServiceOverrides.cs).
+- TEST: Coverage exists for airgap import endpoint/validator, airgap mode enforcer, evidence telemetry, evidence locker endpoints, graph overlay/status/tooltip factories, attestation verify endpoint, OpenAPI discovery, and policy endpoints.
+- TEST: Missing tests for ingest run/resume/reconcile endpoints, mirror endpoints, VEX raw endpoints, observation projection/list endpoints, linkset list endpoints, evidence chunk service/endpoint, status/resolve/risk feed endpoints, observability endpoints, and OpenAPI contract snapshots.
+- Disposition: waived (test project; no apply changes).
+### src/Excititor/StellaOps.Excititor.Worker/StellaOps.Excititor.Worker.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed.
+- MAINT: Program registers in-memory provider/claim stores after AddExcititorPersistence, which overrides any persistent implementations and can mask configuration errors (`src/Excititor/StellaOps.Excititor.Worker/Program.cs`).
+- MAINT: Program hardcodes plugin catalog fallback paths, but no metrics or health output for missing plugin directories (`src/Excititor/StellaOps.Excititor.Worker/Program.cs`).
+- MAINT: WorkerSignatureVerifier parses timestamp metadata with DateTimeOffset.TryParse without invariant culture; parsing is locale-sensitive and can accept ambiguous inputs (`src/Excititor/StellaOps.Excititor.Worker/Signature/WorkerSignatureVerifier.cs`).
+- MAINT: WorkerSignatureVerifier falls back to _timeProvider.GetUtcNow when signedAt metadata is missing; signature metadata becomes nondeterministic (`src/Excititor/StellaOps.Excititor.Worker/Signature/WorkerSignatureVerifier.cs`).
+- MAINT: VexWorkerOrchestratorClient fallback job context uses Guid.NewGuid; local job IDs vary run-to-run and make deterministic replay harder (`src/Excititor/StellaOps.Excititor.Worker/Orchestration/VexWorkerOrchestratorClient.cs`).
+- MAINT: VexWorkerOrchestratorClient.ParseCheckpoint uses DateTimeOffset.TryParse with default culture; prefer invariant/roundtrip handling for stable parsing (`src/Excititor/StellaOps.Excititor.Worker/Orchestration/VexWorkerOrchestratorClient.cs`).
+- MAINT: DefaultVexProviderRunner uses RandomNumberGenerator jitter for backoff; NextEligibleRun becomes nondeterministic and harder to test (`src/Excititor/StellaOps.Excititor.Worker/Scheduling/DefaultVexProviderRunner.cs`).
+- TEST: Coverage exists for worker options validation, tenant authority validation/client factory, worker signature verification, retry policy, orchestrator client behavior, provider runner behavior, end-to-end ingest jobs, and OTel correlation.
+- TEST: Missing tests for consensus refresh scheduler (VexConsensusRefreshService), hosted service scheduling behavior, plugin catalog fallback path handling, and signature metadata culture parsing edge cases.
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; register in-memory stores via TryAdd or guard with config; emit health/telemetry for missing plugin directories; parse timestamps with invariant culture; require explicit signature timestamps or use document timestamps; inject a deterministic run-id provider for local jobs; inject jitter provider for backoff; add tests for consensus refresh, hosted service scheduling, plugin loading fallback, and timestamp parsing.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Excititor/__Tests/StellaOps.Excititor.Worker.Tests/StellaOps.Excititor.Worker.Tests.csproj
+- MAINT: TreatWarningsAsErrors is not set in the project file; warning discipline is relaxed.
+- MAINT: Multiple tests use Guid.NewGuid/DateTimeOffset.UtcNow for job context, document timestamps, or database names (DefaultVexProviderRunnerIntegrationTests.cs, EndToEndIngestJobTests.cs, VexWorkerOrchestratorClientTests.cs, WorkerSignatureVerifierTests.cs), reducing deterministic replay.
+- TEST: Coverage exists for worker options validation, tenant authority validator/client factory, worker signature verification, retry policy, orchestrator client behavior, provider runner tests, integration ingest jobs, and OTel correlation.
+- TEST: Missing tests for consensus refresh scheduler, hosted service scheduling/backoff cancellation behavior, plugin catalog fallback behavior, and locale-sensitive timestamp parsing in signature metadata.
+- Disposition: waived (test project; no apply changes).
+### src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/StellaOps.ExportCenter.Client.csproj
+- MAINT: TreatWarningsAsErrors is not set in the project file; warning discipline is relaxed.
+- MAINT: ExportCenterClientOptions defines DownloadTimeout, but client constructors and AddExportCenterClient do not apply it; configuration is unused.
+- MAINT: ExportJobLifecycleHelper and ExportDownloadHelper write directly to the final output path; partial files can be left on failure/cancellation. Prefer temp file + atomic move and reuse the download helper from lifecycle methods.
+- MAINT: DownloadAndVerifyAsync deletes corrupted files without error handling; deletion failures can leave partial files.
+- TEST: Coverage exists for discovery metadata, profile listing, evidence/attestation create/status/download, download helper hashing/progress, and lifecycle wait/terminal status.
+- TEST: Missing tests for ListRuns/GetRun/GetProfile success paths, list runs query parameters, DownloadAttestationExportAsync 404/409 handling, GetEvidenceExportStatusAsync and GetAttestationExportStatusAsync not-found paths, CreateAttestationExportAndWaitAsync and download helpers, lifecycle timeout/cancellation, and ServiceCollectionExtensions options wiring (including DownloadTimeout).
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; wire DownloadTimeout to HttpClient or download methods; write downloads to temp files with atomic move and optional checksum verification; add missing client/lifecycle tests and deterministic fixtures.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client.Tests/StellaOps.ExportCenter.Client.Tests.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed.
+- MAINT: Tests use DateTimeOffset.UtcNow, Random.Shared, and Guid.NewGuid for fixtures and temp paths, reducing deterministic replay.
+- TEST: Coverage exists for client happy-path calls, download helpers, and lifecycle wait/terminal status.
+- TEST: Missing tests for ListRuns/GetRun/GetProfile success paths, list runs query parameters, DownloadAttestationExportAsync 404/409 handling, status not-found behavior, lifecycle timeout/cancellation, and ServiceCollectionExtensions options wiring.
+- Disposition: waived (test project; no apply changes).
+### src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/StellaOps.ExportCenter.Core.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed.
+- MAINT: ExportScopeResolver uses Environment.TickCount when Sampling.Seed is null and generates ItemId with Guid.NewGuid, making sampling results and item IDs nondeterministic (`src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Planner/ExportScopeResolver.cs`).
+- MAINT: OfflineBundlePackager uses Guid.NewGuid for bundle IDs and TarFile.CreateFromDirectoryAsync for tar creation; bundle IDs and tar output vary per run and tests assert uniqueness, which conflicts with deterministic bundle requirements (`src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/OfflineBundle/OfflineBundlePackager.cs`, `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/OfflineBundle/OfflineBundlePackagerTests.cs`).
+- MAINT: LineageEvidencePackService and LineageNodeEvidencePack embed DateTimeOffset.UtcNow/Guid.NewGuid defaults and compute ReplayHash with UtcNow, making pack metadata and replay hash nondeterministic (`src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Services/LineageEvidencePackService.cs`, `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Domain/LineageEvidencePack.cs`).
+- MAINT: EvidencePackSigningService uses DateTimeOffset.UtcNow and per-call ECDsa key generation plus a placeholder Rekor timestamp, producing non-replayable signatures (`src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Services/EvidencePackSigningService.cs`).
+- MAINT: ExportPlanner.ParseScope/ParseFormat swallow JSON exceptions without logging, which can hide invalid profile configuration (`src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Planner/ExportPlanner.cs`).
+- MAINT: ExportAdapterServiceExtensions registers StubAgeKeyWrapper and in-memory mirror stores by default; production DI can accidentally use stub crypto/in-memory storage without explicit opt-in (`src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/ExportAdapterRegistry.cs`).
+- MAINT: ExportAdapterModels.Failed and ExportRetentionService use DateTimeOffset.UtcNow directly instead of TimeProvider, weakening determinism (`src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/ExportAdapterModels.cs`, `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Scheduling/ExportRetentionService.cs`).
+- TEST: Coverage exists in `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj` for planner/scope resolver, retention/scheduling, adapters (json, trivy, mirror), offline bundle packaging/verification, mirror bundle builder/signing, manifest writer, evidence cache, snapshots, notifications, pack run integration, dev portal offline, distribution, encryption, and API repository.
+- TEST: Missing tests for LineageEvidencePackService generation/verification/replay-hash determinism, EvidencePackSigningService sign/verify behavior, ExportScopeResolver default seed behavior when Sampling.Seed is null, ExportPlanner ParseScope/ParseFormat error handling, and offline bundle determinism (stable bundle IDs/tar ordering).
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; inject TimeProvider and deterministic ID provider; remove Guid.NewGuid/DateTimeOffset.UtcNow defaults from output models; require or record sampling seed; make offline bundle IDs/tar creation deterministic; log/validate invalid profile JSON; gate stub crypto/in-memory stores behind explicit config; add tests for missing scenarios above.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/StellaOps.ExportCenter.Infrastructure.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed.
+- MAINT: Class1.cs is a placeholder file and adds noise to the project. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Class1.cs`
+- MAINT: ExportCenterDataSource sets app.current_tenant only when tenantId is provided; pooled connections opened without tenantId can retain a previous tenant setting. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Db/ExportCenterDataSource.cs`
+- MAINT: FileSystemDevPortalOfflineObjectStore writes directly to the final path and re-reads the input stream for hashing; partial files can be left on failure and non-seekable streams will fail. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/DevPortalOffline/FileSystemDevPortalOfflineObjectStore.cs`
+- TEST: Coverage exists for MigrationScript/MigrationLoader and HmacDevPortalOfflineManifestSigner in the consolidated ExportCenter test project. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Db/MigrationScriptTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Db/MigrationLoaderTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/HmacDevPortalOfflineManifestSignerTests.cs`
+- TEST: Missing tests for ExportCenterDataSource session configuration (tenant/timezone), ExportCenterMigrationRunner apply/rollback/checksum mismatch paths, FileSystemDevPortalOfflineObjectStore path traversal/atomic writes/non-seekable streams, and migration hosted service gating. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Db/ExportCenterDataSource.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Db/ExportCenterMigrationRunner.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/DevPortalOffline/FileSystemDevPortalOfflineObjectStore.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Db/ExportCenterDbServiceExtensions.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; remove placeholder Class1.cs; clear tenant session state when tenantId is null; write storage files via temp + atomic move and compute hash during write or from file; add tests for session config, migration runner behavior, and file store edge cases.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/ExportCenter/StellaOps.ExportCenter.RiskBundles/StellaOps.ExportCenter.RiskBundles.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed.
+- MAINT: RiskBundleBuildRequest.AllowStaleOptional is defined but not used by the builder; RiskBundleJobRequest duplicates IncludeOsv/AllowStaleOptional without applying them, which can mislead callers. `src/ExportCenter/StellaOps.ExportCenter.RiskBundles/RiskBundleModels.cs` `src/ExportCenter/StellaOps.ExportCenter.RiskBundles/RiskBundleJob.cs`
+- MAINT: RiskBundleBuilder writes additional files in enumeration order; if callers pass unordered collections, tar entry ordering can become nondeterministic. `src/ExportCenter/StellaOps.ExportCenter.RiskBundles/RiskBundleBuilder.cs`
+- MAINT: RiskBundleBuilder uses Path.GetDirectoryName on bundle paths when adding signatures; on Windows this can introduce backslashes into tar entry names. `src/ExportCenter/StellaOps.ExportCenter.RiskBundles/RiskBundleBuilder.cs`
+- MAINT: FileSystemRiskBundleObjectStore does not sanitize storage keys or enforce root containment; path traversal is possible. `src/ExportCenter/StellaOps.ExportCenter.RiskBundles/FileSystemRiskBundleObjectStore.cs`
+- MAINT: FileSystemRiskBundleObjectStore writes directly to the final path without temp/atomic moves; partial artefacts can be left on failure. `src/ExportCenter/StellaOps.ExportCenter.RiskBundles/FileSystemRiskBundleObjectStore.cs`
+- MAINT: Tests use Guid.NewGuid for bundle IDs and temp paths, which reduces deterministic replay. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/RiskBundleBuilderTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/RiskBundleJobTests.cs`
+- TEST: Coverage exists for RiskBundleBuilder, RiskBundleJob, and RiskBundleSigner in the consolidated ExportCenter test project. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/RiskBundleBuilderTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/RiskBundleJobTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/RiskBundleSignerTests.cs`
+- TEST: Missing tests for FileSystemRiskBundleObjectStore path traversal/atomic write behavior/non-seekable streams, additional file ordering determinism, IncludeOsv gating, AllowMissingOptional=false behavior, and signature path absence. `src/ExportCenter/StellaOps.ExportCenter.RiskBundles/FileSystemRiskBundleObjectStore.cs` `src/ExportCenter/StellaOps.ExportCenter.RiskBundles/RiskBundleBuilder.cs` `src/ExportCenter/StellaOps.ExportCenter.RiskBundles/RiskBundleJob.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; remove or implement unused options; sort additional files by BundlePath; derive signature tar paths with forward-slash logic; sanitize storage keys and enforce root containment; write files via temp + atomic move; add tests for file store edge cases and option handling.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed.
+- MAINT: Tests widely use Guid.NewGuid and DateTimeOffset.UtcNow for IDs/timestamps and temp paths, which reduces deterministic replay (examples: ExportVerificationServiceTests, ExportNotificationEmitterTests, ExportManifestWriterTests, ExportProfileTests, ExportScopeResolverTests, ExportPlannerTests, RiskBundle* tests). `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Verification/ExportVerificationServiceTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/ExportNotificationEmitterTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Manifest/ExportManifestWriterTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Domain/ExportProfileTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Planner/ExportScopeResolverTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Planner/ExportPlannerTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/RiskBundleBuilderTests.cs`
+- MAINT: Some tests use DateTimeOffset.UtcNow for deprecation windows and snapshot epochs, which can become time-sensitive. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationInfoTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationHeaderExtensionsTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Snapshots/SnapshotLevelHandlerTests.cs`
+- TEST: Coverage exists for verification, notifications, manifests, tenancy enforcement, scheduling/retention, adapters (json/trivy/mirror), bundle builders (offline, mirror, risk), snapshots, dev portal offline flows, distribution (OCI), migrations, and API repositories. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Verification/ExportVerificationServiceTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/ExportNotificationEmitterTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Manifest/ExportManifestWriterTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Tenancy/TenantScopeEnforcerTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Scheduling/ExportSchedulerServiceTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Adapters/JsonRawAdapterTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Adapters/Trivy/TrivyDbAdapterTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/OfflineBundle/OfflineBundlePackagerTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/MirrorBundleBuilderTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Snapshots/ExportSnapshotServiceTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/DevPortalOfflineBundleBuilderTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/Oci/OciDistributionClientTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Db/MigrationLoaderTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiRepositoryTests.cs`
+- TEST: Missing tests for EvidencePackSigningService, LineageEvidencePackService determinism, ExportCenterDataSource session configuration, ExportCenterMigrationRunner apply/rollback paths, FileSystemDevPortalOfflineObjectStore storage edge cases, FileSystemRiskBundleObjectStore storage edge cases, ExportScopeResolver default seed behavior, and offline bundle deterministic bundle IDs/tar ordering. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Services/EvidencePackSigningService.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Services/LineageEvidencePackService.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Db/ExportCenterDataSource.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Db/ExportCenterMigrationRunner.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/DevPortalOffline/FileSystemDevPortalOfflineObjectStore.cs` `src/ExportCenter/StellaOps.ExportCenter.RiskBundles/FileSystemRiskBundleObjectStore.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Planner/ExportScopeResolver.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/OfflineBundle/OfflineBundlePackager.cs`
+- Disposition: waived (test project; no apply changes).
+### src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj`
+- MAINT: AddExportApiServices registers in-memory repositories by default; production use is not explicitly gated. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiServiceCollectionExtensions.cs`
+- MAINT: ExportApiEndpoints creates IDs/timestamps with Guid.NewGuid/DateTimeOffset.UtcNow and SSE timestamps use UtcNow without a TimeProvider or ID generator. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs`
+- MAINT: ExportApiEndpoints deserializes stored scope/format/signing JSON without error handling; invalid JSON can throw. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs`
+- MAINT: InMemoryExportRunRepository.DequeueNextRunAsync does not mark runs as running; concurrent workers can dequeue the same run. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/InMemoryExportRepositories.cs`
+- MAINT: InMemoryExportProfileRepository and InMemoryExportRunRepository use DateTimeOffset.UtcNow directly for ArchivedAt/CompletedAt; no TimeProvider injection. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/InMemoryExportRepositories.cs`
+- MAINT: Evidence locker configuration uses `new Uri(options.BaseUrl)` without validation; empty/invalid BaseUrl throws at startup. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/EvidenceLocker/EvidenceLockerServiceCollectionExtensions.cs`
+- MAINT: InMemoryExportEvidenceLockerClient uses Guid.NewGuid/DateTimeOffset.UtcNow for bundle IDs/timestamps and hashing depends on input order; outputs are nondeterministic. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/EvidenceLocker/EvidenceLockerServiceCollectionExtensions.cs`
+- MAINT: RiskBundleJobHandler options MaxConcurrentJobs/JobTimeout/JobRetentionPeriod/DefaultStoragePrefix are unused and job retention is not enforced, leading to unbounded in-memory growth. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleJobHandler.cs`
+- MAINT: RiskBundleJobHandler.CancelJobAsync overwrites status before recording "original_status"; audit attributes lose the original value. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleJobHandler.cs`
+- MAINT: RiskBundleJobHandler.CreateProviderInfo uses DateTime.UtcNow instead of TimeProvider; time injection is inconsistent. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleJobHandler.cs`
+- MAINT: RiskBundleJobHandler simulates outcomes with Guid.NewGuid-based bundle IDs/hashes; output is nondeterministic and not flagged as sample-only. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleJobHandler.cs`
+- MAINT: SimulationReportExporter seeds sample simulations with Guid.NewGuid and Random, producing non-reproducible sample data and unbounded in-memory stores. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/SimulationExport/SimulationReportExporter.cs`
+- MAINT: AuditBundleJobHandler uses Guid.NewGuid/DateTimeOffset.UtcNow in bundle IDs and report content, so bundles are not reproducible. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleJobHandler.cs`
+- MAINT: AuditBundleJobHandler starts background work with Task.Run but does not handle cancellation to update job status. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleJobHandler.cs`
+- MAINT: ExceptionReportGenerator uses Guid.NewGuid/DateTimeOffset.UtcNow for job IDs/timestamps and keeps jobs indefinitely; outputs are nondeterministic. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportGenerator.cs`
+- MAINT: ExceptionReportGenerator summary dictionaries are built with GroupBy/ToDictionary, producing nondeterministic JSON key ordering. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportGenerator.cs`
+- MAINT: ExportIncidentManager and the in-memory distribution repository have no retention/cleanup; memory growth in long-running services. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Incident/ExportIncidentManager.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/InMemoryExportDistributionRepository.cs`
+- MAINT: DeprecationInfo uses DateTimeOffset.UtcNow directly; time-sensitive tests. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationInfo.cs`
+- TEST: Coverage exists for OpenApi discovery, audit service, API repository, deprecation helpers, distribution lifecycle, in-memory distribution repository, OCI distribution, and Trivy adapters. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/OpenApiDiscoveryEndpointsTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportAuditServiceTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiRepositoryTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationInfoTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationHeaderExtensionsTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/InMemoryExportDistributionRepositoryTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/ExportDistributionLifecycleTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/Oci/OciDistributionClientTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Adapters/Trivy/TrivyDbAdapterTests.cs`
+- TEST: Missing tests for ExportApiEndpoints CRUD/SSE behavior, in-memory repository concurrency (dequeue), EvidenceLocker in-memory client, RiskBundleJobHandler and AuditBundleJobHandler flows, SimulationReportExporter output determinism, ExceptionReportGenerator outputs, and incident manager operations. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/InMemoryExportRepositories.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/EvidenceLocker/EvidenceLockerServiceCollectionExtensions.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleJobHandler.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleJobHandler.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/SimulationExport/SimulationReportExporter.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportGenerator.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Incident/ExportIncidentManager.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; inject TimeProvider/ID generator into endpoints and in-memory services; gate in-memory repos behind explicit dev/test config; add job retention/cleanup and cancellation handling; validate BaseUrl; normalize deterministic ordering in report summaries; add tests for endpoints and job handlers.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj`
+- MAINT: DevPortal offline worker generates bundle IDs with Guid.NewGuid when not configured, making outputs nondeterministic. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/Worker.cs`
+- MAINT: Risk bundle worker generates bundle IDs with Guid.NewGuid when not configured, making outputs nondeterministic. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/RiskBundleWorker.cs`
+- MAINT: DevPortal offline worker options have no validation; missing PortalDirectory/SpecsDirectory/Storage config fails later at runtime. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/DevPortalOfflineWorkerOptions.cs`
+- MAINT: RiskBundleStorageOptions type is unused; stale config surface increases confusion. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/RiskBundleWorkerOptions.cs`
+- TEST: Coverage exists for DevPortal offline job/bundle builder and risk bundle job/builder in the consolidated ExportCenter tests. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/DevPortalOfflineJobTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/DevPortalOfflineBundleBuilderTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/RiskBundleJobTests.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/RiskBundleBuilderTests.cs`
+- TEST: Missing tests for Worker and RiskBundleWorker hosted service behavior, options validation (enabled/disabled, missing providers), request building defaults, and Program DI wiring. `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/Worker.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/RiskBundleWorker.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/RiskBundleOptionsValidation.cs` `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/Program.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; require explicit bundle IDs or deterministic ID provider; add validation for DevPortalOfflineWorkerOptions when enabled; remove or use RiskBundleStorageOptions; add tests for hosted services/options/request building and DI setup.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Feedser/StellaOps.Feedser.BinaryAnalysis/StellaOps.Feedser.BinaryAnalysis.csproj
+- MAINT: TreatWarningsAsErrors and LangVersion preview are not set in the project file; warning discipline and preview feature alignment are inconsistent with the repo standard. `src/Feedser/StellaOps.Feedser.BinaryAnalysis/StellaOps.Feedser.BinaryAnalysis.csproj`
+- MAINT: Fingerprinters stamp ExtractedAt with DateTimeOffset.UtcNow, making fingerprints nondeterministic without a TimeProvider. `src/Feedser/StellaOps.Feedser.BinaryAnalysis/Fingerprinters/InstructionHashFingerprinter.cs` `src/Feedser/StellaOps.Feedser.BinaryAnalysis/Fingerprinters/SimplifiedTlshFingerprinter.cs`
+- MAINT: BinaryFingerprintFactory uses dictionary enumeration order for ExtractAllAsync and GetAvailableMethods; ordering is not contractually stable. `src/Feedser/StellaOps.Feedser.BinaryAnalysis/BinaryFingerprintFactory.cs`
+- MAINT: MatchBestAsync breaks ties only on confidence/similarity; if equal, selection depends on input enumeration order. `src/Feedser/StellaOps.Feedser.BinaryAnalysis/BinaryFingerprintFactory.cs`
+- MAINT: Format/architecture detection relies on BitConverter endianness; behavior is platform-dependent and should be explicit. `src/Feedser/StellaOps.Feedser.BinaryAnalysis/Fingerprinters/InstructionHashFingerprinter.cs` `src/Feedser/StellaOps.Feedser.BinaryAnalysis/Fingerprinters/SimplifiedTlshFingerprinter.cs`
+- MAINT: MatchDetails uses Dictionary; serialization key order is nondeterministic. `src/Feedser/StellaOps.Feedser.BinaryAnalysis/Models/BinaryFingerprint.cs` `src/Feedser/StellaOps.Feedser.BinaryAnalysis/Fingerprinters/InstructionHashFingerprinter.cs` `src/Feedser/StellaOps.Feedser.BinaryAnalysis/Fingerprinters/SimplifiedTlshFingerprinter.cs`
+- TEST: No tests found for the binary analysis library; the Feedser tests project does not cover these types. `src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/StellaOps.Feedser.Core.Tests.csproj`
+- TEST: Missing tests for TLSH similarity thresholds, instruction hash normalization, factory ordering, metadata detection (ELF/PE/Mach-O), and deterministic timestamps. `src/Feedser/StellaOps.Feedser.BinaryAnalysis/BinaryFingerprintFactory.cs` `src/Feedser/StellaOps.Feedser.BinaryAnalysis/Fingerprinters/InstructionHashFingerprinter.cs` `src/Feedser/StellaOps.Feedser.BinaryAnalysis/Fingerprinters/SimplifiedTlshFingerprinter.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors and LangVersion preview; inject TimeProvider for ExtractedAt; enforce deterministic ordering and tie-breaking; use BinaryPrimitives for explicit endianness; use ordered metadata maps for MatchDetails; add unit tests for fingerprinters and factory behavior.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Feedser/StellaOps.Feedser.Core/StellaOps.Feedser.Core.csproj
+- MAINT: TreatWarningsAsErrors and LangVersion preview are not set in the project file; warning discipline and preview feature alignment are inconsistent with the repo standard. `src/Feedser/StellaOps.Feedser.Core/StellaOps.Feedser.Core.csproj`
+- MAINT: HunkSigExtractor stamps ExtractedAt with DateTimeOffset.UtcNow; patch signatures are nondeterministic without a TimeProvider. `src/Feedser/StellaOps.Feedser.Core/HunkSigExtractor.cs`
+- MAINT: HunkSigExtractor leaves AffectedFunctions null with a TODO; function extraction exists elsewhere but is not wired. `src/Feedser/StellaOps.Feedser.Core/HunkSigExtractor.cs` `src/Feedser/StellaOps.Feedser.Core/FunctionSignatureExtractor.cs`
+- MAINT: ParseUnifiedDiff does not normalize line endings before capturing context lines; context storage can vary across CRLF/LF inputs. `src/Feedser/StellaOps.Feedser.Core/HunkSigExtractor.cs`
+- MAINT: ExtractStartLine returns 0 on non-matching hunk headers without error context; invalid diffs can silently degrade metadata. `src/Feedser/StellaOps.Feedser.Core/HunkSigExtractor.cs`
+- MAINT: FunctionMatchingExtensions.FindBestMatch and FindAllMatches rely on candidate enumeration order for tie-breaking; results can vary without a stable order. `src/Feedser/StellaOps.Feedser.Core/FunctionSignatureExtractor.cs`
+- TEST: Coverage exists for HunkSigExtractor parsing/normalization and function signature extraction/matching across C/Go/Python/Rust/Java/JS. `src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/HunkSigExtractorTests.cs` `src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/FunctionSignatureExtractorTests.cs`
+- TEST: Tests use DateTimeOffset.UtcNow to assert ExtractedAt recency, which is time-sensitive and nondeterministic. `src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/HunkSigExtractorTests.cs`
+- TEST: Missing tests for CRLF line ending normalization, diff headers without counts, deleted/renamed file paths, and deterministic tie-breaking in FindBestMatch. `src/Feedser/StellaOps.Feedser.Core/HunkSigExtractor.cs` `src/Feedser/StellaOps.Feedser.Core/FunctionSignatureExtractor.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors and LangVersion preview; inject TimeProvider and wire ExtractedAt deterministically; integrate function extraction to populate AffectedFunctions; normalize line endings before parsing; add stable tie-breakers (e.g., by signature/name); add tests for diff edge cases and tie-breaking.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/StellaOps.Feedser.Core.Tests.csproj
+- MAINT: TreatWarningsAsErrors and LangVersion preview are not set in the project file; warning discipline and preview feature alignment are inconsistent with the repo standard. `src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/StellaOps.Feedser.Core.Tests.csproj`
+- MAINT: Tests assert ExtractedAt recency using DateTimeOffset.UtcNow; time-sensitive and nondeterministic. `src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/HunkSigExtractorTests.cs`
+- TEST: Coverage exists for HunkSigExtractor parsing/normalization and FunctionSignatureExtractor language detection, extraction, and matching. `src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/HunkSigExtractorTests.cs` `src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/FunctionSignatureExtractorTests.cs`
+- TEST: Missing tests for deterministic ExtractedAt behavior with a fixed time provider and for diff line-ending normalization edge cases. `src/Feedser/__Tests/StellaOps.Feedser.Core.Tests/HunkSigExtractorTests.cs` `src/Feedser/StellaOps.Feedser.Core/HunkSigExtractor.cs`
+- Disposition: waived (test project; no apply changes).
+### src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj
+- MAINT: TreatWarningsAsErrors and LangVersion preview are not set in the project file; warning discipline and preview feature alignment are inconsistent with the repo standard. `src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj`
+- MAINT: DecisionService.RecordAsync sets SequenceNumber to 0 while LedgerEventWriteService enforces sequence >= 1 and expected chain head; decisions will fail with sequence_mismatch/validation_failed. `src/Findings/StellaOps.Findings.Ledger/Services/DecisionService.cs` `src/Findings/StellaOps.Findings.Ledger/Services/LedgerEventWriteService.cs`
+- MAINT: SnapshotService.ComputeMerkleRootAsync replays only the first 10,000 events and does not paginate; Merkle root ignores events beyond the first page. `src/Findings/StellaOps.Findings.Ledger/Services/SnapshotService.cs`
+- MAINT: SnapshotService.ComputeStatisticsAsync uses TotalCount from time-travel queries with PageSize=1, while PostgresTimeTravelRepository returns TotalCount = items.Count; snapshot counts are incorrect. `src/Findings/StellaOps.Findings.Ledger/Services/SnapshotService.cs` `src/Findings/StellaOps.Findings.Ledger/Infrastructure/Postgres/PostgresTimeTravelRepository.cs`
+- MAINT: DecisionEvent.Id defaults to Guid.NewGuid, and event IDs are generated with Guid.NewGuid in AirgapImportService, AirgapTimelineService, DecisionService, EvidenceSnapshotService, and AttestationPointerService; IDs are nondeterministic and not injectable for tests. `src/Findings/StellaOps.Findings.Ledger/Domain/DecisionModels.cs` `src/Findings/StellaOps.Findings.Ledger/Services/AirgapImportService.cs` `src/Findings/StellaOps.Findings.Ledger/Services/AirgapTimelineService.cs` `src/Findings/StellaOps.Findings.Ledger/Services/DecisionService.cs` `src/Findings/StellaOps.Findings.Ledger/Services/EvidenceSnapshotService.cs` `src/Findings/StellaOps.Findings.Ledger/Services/AttestationPointerService.cs`
+- MAINT: PostgresSnapshotRepository uses Guid.NewGuid and DateTimeOffset.UtcNow for snapshot IDs/timestamps; time and ID are not injectable. `src/Findings/StellaOps.Findings.Ledger/Infrastructure/Postgres/PostgresSnapshotRepository.cs`
+- MAINT: LedgerEventWriteService.RecordConflictSnapshot, SnapshotService.ReplayEventsAsync/ExpireOldSnapshotsAsync, and PostgresTimeTravelRepository (null query point and staleness checks) use DateTimeOffset.UtcNow directly; time is not injectable for deterministic tests. `src/Findings/StellaOps.Findings.Ledger/Services/LedgerEventWriteService.cs` `src/Findings/StellaOps.Findings.Ledger/Services/SnapshotService.cs` `src/Findings/StellaOps.Findings.Ledger/Infrastructure/Postgres/PostgresTimeTravelRepository.cs`
+- MAINT: ScoredFindingsExportService.ExportToJson uses DateTimeOffset.UtcNow for generated_at instead of the injected TimeProvider; export envelope timestamp diverges from ExportResult.GeneratedAt. `src/Findings/StellaOps.Findings.Ledger/Services/ScoredFindingsExportService.cs`
+- MAINT: LedgerMerkleAnchorWorker uses Guid.NewGuid for anchor IDs; anchors are nondeterministic and hard to replay in tests. `src/Findings/StellaOps.Findings.Ledger/Infrastructure/Merkle/LedgerMerkleAnchorWorker.cs`
+- TEST: Coverage exists for ledger event writes, projection reduction, scoring query, workflow, and OpenAPI metadata. `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/LedgerEventWriteServiceTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/LedgerProjectionReducerTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/ScoredFindingsQueryServiceTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/FindingWorkflowServiceTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/OpenApiMetadataFactoryTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/OpenApiSchemaTests.cs`
+- TEST: Missing tests for DecisionService sequence handling, snapshot statistics totals, merkle pagination, time-travel TotalCount accuracy, and export generated_at determinism. `src/Findings/StellaOps.Findings.Ledger/Services/DecisionService.cs` `src/Findings/StellaOps.Findings.Ledger/Services/SnapshotService.cs` `src/Findings/StellaOps.Findings.Ledger/Infrastructure/Postgres/PostgresTimeTravelRepository.cs` `src/Findings/StellaOps.Findings.Ledger/Services/ScoredFindingsExportService.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors and LangVersion preview; set SequenceNumber using chain head or let write service assign; paginate merkle replay; return total counts in time-travel queries (COUNT/window) and update snapshot statistics to use them; inject TimeProvider/ID generator where IDs/timestamps are created; use TimeProvider for generated_at; add tests for decision append, snapshot stats, merkle pagination, TotalCount, and export determinism.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj
+- MAINT: TreatWarningsAsErrors is not set in the project file; warning discipline is relaxed. `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj`
+- MAINT: Test SDK/xUnit references are implicit via shared props; the project does not declare them locally, which obscures dependency ownership. `src/Directory.Build.props` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj`
+- MAINT: Tests use Guid.NewGuid/DateTimeOffset.UtcNow and ActivityTraceId.CreateRandom for IDs/timestamps; fixtures are nondeterministic. `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/LedgerProjectionReducerTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/ScoredFindingsQueryServiceTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/InlinePolicyEvaluationServiceTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/PolicyEngineEvaluationServiceTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Schema/OpenApiSchemaTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Services/FindingSummaryBuilderTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Services/EvidenceGraphBuilderTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/ScoringObservabilityTests.cs`
+- MAINT: Integration tests use DateTimeOffset.UtcNow to build query windows; results depend on wall-clock time. `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/ScoringEndpointsIntegrationTests.cs`
+- TEST: Coverage exists for event write service, projection reducer, scoring endpoints/authorization/observability, webhook endpoints, evidence decision API, OpenAPI metadata/schema, inline policy evaluation, workflow, metrics, scored findings query, evidence graph builder, and harness runner. `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/LedgerEventWriteServiceTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/LedgerProjectionReducerTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/ScoringEndpointsIntegrationTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/ScoringAuthorizationTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/ScoringObservabilityTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/WebhookEndpointsIntegrationTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/EvidenceDecisionApiIntegrationTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/OpenApiMetadataFactoryTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Schema/OpenApiSchemaTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/InlinePolicyEvaluationServiceTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/FindingWorkflowServiceTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/LedgerMetricsTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/ScoredFindingsQueryServiceTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Services/EvidenceGraphBuilderTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/HarnessRunnerTests.cs`
+- TEST: Missing tests for DecisionService append flow (sequence mismatch), snapshot statistics totals, merkle root pagination, time-travel TotalCount accuracy, and export generated_at determinism. `src/Findings/StellaOps.Findings.Ledger/Services/DecisionService.cs` `src/Findings/StellaOps.Findings.Ledger/Services/SnapshotService.cs` `src/Findings/StellaOps.Findings.Ledger/Infrastructure/Postgres/PostgresTimeTravelRepository.cs` `src/Findings/StellaOps.Findings.Ledger/Services/ScoredFindingsExportService.cs`
+- Disposition: waived (test project; no apply changes).
+### src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed. `src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj`
+- MAINT: Test SDK/xUnit references are implicit via shared props; the project only declares the VS runner locally, obscuring dependency ownership. `src/Directory.Build.props` `src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj`
+- MAINT: Tests use Guid.NewGuid/DateTimeOffset.UtcNow across determinism and snapshot tests; fixtures are nondeterministic and can mask replay regressions. `src/Findings/StellaOps.Findings.Ledger.Tests/LedgerReplayDeterminismTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/FindingsLedgerIntegrationTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Snapshot/SnapshotServiceTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/ProjectionHashingTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Infrastructure/InMemoryLedgerEventRepositoryTests.cs`
+- MAINT: Web service contract tests use WebApplicationFactory but are tagged as Unit; category labeling is misleading for CI selection. `src/Findings/StellaOps.Findings.Ledger.Tests/FindingsLedgerWebServiceContractTests.cs`
+- MAINT: Non-ASCII glyphs appear in comments and region headers (arrow symbols/encoding artifacts), violating ASCII-only portability guidance. `src/Findings/StellaOps.Findings.Ledger.Tests/FindingsLedgerIntegrationTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/LedgerReplayDeterminismTests.cs`
+- MAINT: ExportFiltersHashTests hard-codes a localhost connection string; if LedgerDataSource opens a connection, the test is not offline-safe. `src/Findings/StellaOps.Findings.Ledger.Tests/Exports/ExportFiltersHashTests.cs`
+- TEST: Coverage exists for snapshot service, replay determinism, projection hashing, incident coordinator, attestation pointer service, export paging/filters, attestation query filters, in-memory ledger repository, observability/telemetry/metrics, and web service contract tests. `src/Findings/StellaOps.Findings.Ledger.Tests/Snapshot/SnapshotServiceTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/LedgerReplayDeterminismTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/ProjectionHashingTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Incident/LedgerIncidentCoordinatorTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Attestation/AttestationPointerServiceTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Exports/ExportPagingTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Exports/ExportFiltersHashTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Exports/AttestationQueryServiceTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Infrastructure/InMemoryLedgerEventRepositoryTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Observability/LedgerTelemetryTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Observability/LedgerMetricsTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Observability/LedgerTimelineTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/FindingsLedgerWebServiceContractTests.cs`
+- TEST: Missing tests for SnapshotService sign/merkle-root path, export page token invalid cases, and error-path validation for contract endpoints. `src/Findings/StellaOps.Findings.Ledger/Services/SnapshotService.cs` `src/Findings/StellaOps.Findings.Ledger/Infrastructure/Exports/ExportPaging.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/FindingsLedgerWebServiceContractTests.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; replace Guid.NewGuid/DateTimeOffset.UtcNow with fixed fixtures or TimeProvider in tests; correct test categories for WebApplicationFactory-based tests; clean non-ASCII comment glyphs; avoid hard-coded localhost connection strings by using a stubbed LedgerDataSource or deferred connection factory; add tests for merkle-root/sign paths and invalid paging tokens.
+- Disposition: waived (test project; no apply changes).
+### src/Findings/StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj
+- MAINT: TreatWarningsAsErrors and LangVersion preview are not set in the project file; warning discipline and preview feature alignment are inconsistent with the repo standard. `src/Findings/StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj`
+- MAINT: In-memory score history and webhook stores are registered unconditionally; no persistence or environment gating, and in-memory data is lost on restart. `src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs` `src/Findings/StellaOps.Findings.Ledger.WebService/Services/ScoreHistoryStore.cs` `src/Findings/StellaOps.Findings.Ledger.WebService/Services/WebhookService.cs`
+- MAINT: InMemoryWebhookStore uses Guid.NewGuid/DateTimeOffset.UtcNow and returns ConcurrentDictionary.Values without ordering; webhook IDs/timestamps and list order are nondeterministic. `src/Findings/StellaOps.Findings.Ledger.WebService/Services/WebhookService.cs`
+- MAINT: WebhookDeliveryService fires-and-forgets delivery tasks without backpressure or queueing; delivery retries can be canceled by request-scoped tokens and failures are only logged. `src/Findings/StellaOps.Findings.Ledger.WebService/Services/WebhookService.cs`
+- MAINT: VexConsensusService statusWeights never updates because keys are normalized to remove underscores; contributions are computed with a running total (not normalized), and statement lists are mutated without thread safety. `src/Findings/StellaOps.Findings.Ledger.WebService/Services/VexConsensusService.cs`
+- MAINT: VexConsensusService stores projections/issuers/statements in memory with no retention; output ordering depends on in-memory ordering and ties. `src/Findings/StellaOps.Findings.Ledger.WebService/Services/VexConsensusService.cs`
+- MAINT: EvidenceGraphBuilder uses DateTimeOffset.UtcNow for GeneratedAt and returns nodes/edges in input order with unordered metadata dictionaries; output can be nondeterministic. `src/Findings/StellaOps.Findings.Ledger.WebService/Services/EvidenceGraphBuilder.cs`
+- MAINT: EvidenceGraphEndpoints exposes includeContent query parameter but never uses it. `src/Findings/StellaOps.Findings.Ledger.WebService/Endpoints/EvidenceGraphEndpoints.cs`
+- MAINT: FindingScoringService stamps CalculatedAt/CachedUntil with DateTimeOffset.UtcNow and does not use a TimeProvider. `src/Findings/StellaOps.Findings.Ledger.WebService/Services/FindingScoringService.cs`
+- MAINT: ScoringEndpoints uses a MaxBatchSize constant (100) independent of FindingScoringOptions.MaxBatchSize, so config and endpoint validation can diverge. `src/Findings/StellaOps.Findings.Ledger.WebService/Endpoints/ScoringEndpoints.cs` `src/Findings/StellaOps.Findings.Ledger.WebService/Services/FindingScoringService.cs`
+- MAINT: LedgerEventMapping uses DateTimeOffset.UtcNow when RecordedAt is not provided; no injected TimeProvider for deterministic tests. `src/Findings/StellaOps.Findings.Ledger.WebService/Mappings/LedgerEventMapping.cs`
+- MAINT: State transition endpoint hard-codes previousStatus = "affected" and uses evidenceRefs.FirstOrDefault without an ordering contract; state history can be incorrect. `src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs`
+- MAINT: ExportQueryService returns empty pages for VEX/advisory/SBOM exports without signaling unimplemented behavior; can mask missing functionality. `src/Findings/StellaOps.Findings.Ledger.WebService/Services/ExportQueryService.cs`
+- TEST: Coverage exists for evidence graph building, finding summary builder, export paging/filters, attestation query filters, web service contract tests, and scoring/webhook/evidence decision endpoints. `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Services/EvidenceGraphBuilderTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Services/FindingSummaryBuilderTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Exports/ExportPagingTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Exports/ExportFiltersHashTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/Exports/AttestationQueryServiceTests.cs` `src/Findings/StellaOps.Findings.Ledger.Tests/FindingsLedgerWebServiceContractTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/ScoringEndpointsIntegrationTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/ScoringAuthorizationTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/ScoringObservabilityTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/WebhookEndpointsIntegrationTests.cs` `src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/EvidenceDecisionApiIntegrationTests.cs`
+- TEST: Missing tests for VexConsensusService (weights, conflicts, ordering), WebhookDeliveryService retries/signatures, InMemoryWebhookStore matching/ordering, ScoreHistoryStore retention/pagination, EvidenceGraphBuilder ordering determinism, and state transition previous status/sequence handling. `src/Findings/StellaOps.Findings.Ledger.WebService/Services/VexConsensusService.cs` `src/Findings/StellaOps.Findings.Ledger.WebService/Services/WebhookService.cs` `src/Findings/StellaOps.Findings.Ledger.WebService/Services/ScoreHistoryStore.cs` `src/Findings/StellaOps.Findings.Ledger.WebService/Services/EvidenceGraphBuilder.cs` `src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors and LangVersion preview; gate in-memory stores to dev/test or add persistence; inject TimeProvider/ID generator; fix VexConsensusService status key normalization and normalize contributions; add ordering for webhooks/graphs; wire includeContent or remove it; align batch size validation with options; capture real previous status in state transitions; implement export queries or return explicit not-implemented responses; add tests for consensus, webhooks, score history, graph determinism, and state transitions.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Gateway/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj
+- MAINT: CorrelationIdMiddleware trusts inbound X-Correlation-Id without length/format validation; unbounded user input can set TraceIdentifier and response headers. `src/Gateway/StellaOps.Gateway.WebService/Middleware/CorrelationIdMiddleware.cs`
+- MAINT: GatewayHostedService and GatewayHealthMonitorService use DateTime.UtcNow for heartbeats/health checks despite TimeProvider registration; time is not injectable for deterministic tests. `src/Gateway/StellaOps.Gateway.WebService/Services/GatewayHostedService.cs` `src/Gateway/StellaOps.Gateway.WebService/Services/GatewayHealthMonitorService.cs`
+- MAINT: GatewayTransportClient buffers streaming responses into an unbounded MemoryStream with no size guard, risking memory pressure and bypassing payload limits. `src/Gateway/StellaOps.Gateway.WebService/Services/GatewayTransportClient.cs`
+- MAINT: GatewayValueParser accepts negative durations/sizes and GatewayOptionsValidator does not enforce positive values; invalid config can yield negative timeouts/limits. `src/Gateway/StellaOps.Gateway.WebService/Configuration/GatewayValueParser.cs` `src/Gateway/StellaOps.Gateway.WebService/Configuration/GatewayOptionsValidator.cs`
+- MAINT: Messaging transport options are not validated when enabled (connection string/queue names/batch size/intervals), so misconfiguration fails later at runtime. `src/Gateway/StellaOps.Gateway.WebService/Configuration/GatewayOptionsValidator.cs`
+- MAINT: Legacy middleware (ClaimsPropagationMiddleware/TenantMiddleware) remains in the project but is no longer used; dead code and tests can drift from active policy. `src/Gateway/StellaOps.Gateway.WebService/Middleware/ClaimsPropagationMiddleware.cs` `src/Gateway/StellaOps.Gateway.WebService/Middleware/TenantMiddleware.cs`
+- TEST: Coverage exists for options validation, value parsing, authorization middleware, identity header policy, correlation ID, legacy middleware, gateway routes, health/openapi/metrics endpoints, and messaging integration wiring. `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayOptionsValidatorTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayValueParserTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Authorization/AuthorizationMiddlewareTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Authorization/EffectiveClaimsStoreTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/IdentityHeaderPolicyMiddlewareTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/CorrelationIdMiddlewareTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/ClaimsPropagationMiddlewareTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/TenantMiddlewareTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/GatewayRoutesTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Integration/GatewayIntegrationTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Integration/MessagingTransportIntegrationTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/GatewayHealthTests.cs`
+- TEST: Missing tests for health monitor stale/degraded transitions with controlled time, hosted service heartbeat updates, streaming response size/backpressure limits, correlation header validation limits, and messaging-enabled validation errors. `src/Gateway/StellaOps.Gateway.WebService/Services/GatewayHealthMonitorService.cs` `src/Gateway/StellaOps.Gateway.WebService/Services/GatewayHostedService.cs` `src/Gateway/StellaOps.Gateway.WebService/Services/GatewayTransportClient.cs` `src/Gateway/StellaOps.Gateway.WebService/Middleware/CorrelationIdMiddleware.cs` `src/Gateway/StellaOps.Gateway.WebService/Configuration/GatewayOptionsValidator.cs`
+- Proposed changes (pending approval): enforce correlation ID length/format with fallback, inject TimeProvider into hosted/health services, add response size limits or streaming passthrough, validate positive durations/sizes and messaging config when enabled, remove/obsolete legacy middleware or mark as legacy-only, and add tests for new validations and time-based behavior.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Router/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj
+- MAINT: CorrelationIdMiddleware trusts inbound X-Correlation-Id without length/format validation; unbounded user input can set TraceIdentifier and response headers. `src/Router/StellaOps.Gateway.WebService/Middleware/CorrelationIdMiddleware.cs`
+- MAINT: GatewayHostedService and GatewayHealthMonitorService use DateTime.UtcNow for heartbeats/health checks despite TimeProvider registration; time is not injectable for deterministic tests. `src/Router/StellaOps.Gateway.WebService/Services/GatewayHostedService.cs` `src/Router/StellaOps.Gateway.WebService/Services/GatewayHealthMonitorService.cs`
+- MAINT: GatewayTransportClient buffers streaming responses into an unbounded MemoryStream with no size guard, risking memory pressure and bypassing payload limits. `src/Router/StellaOps.Gateway.WebService/Services/GatewayTransportClient.cs`
+- MAINT: GatewayValueParser accepts negative durations/sizes and GatewayOptionsValidator does not enforce positive values; invalid config can yield negative timeouts/limits. `src/Router/StellaOps.Gateway.WebService/Configuration/GatewayValueParser.cs` `src/Router/StellaOps.Gateway.WebService/Configuration/GatewayOptionsValidator.cs`
+- MAINT: Messaging transport options are not validated when enabled (connection string/queue names/batch size/intervals), so misconfiguration fails later at runtime. `src/Router/StellaOps.Gateway.WebService/Configuration/GatewayOptionsValidator.cs`
+- MAINT: Legacy middleware (ClaimsPropagationMiddleware/TenantMiddleware) remains in the project but is no longer used; dead code and tests can drift from active policy. `src/Router/StellaOps.Gateway.WebService/Middleware/ClaimsPropagationMiddleware.cs` `src/Router/StellaOps.Gateway.WebService/Middleware/TenantMiddleware.cs`
+- MAINT: Transport plugin loader uses NullLoggerFactory and a hard-coded plugins path; plugin load failures are not observable and the path is not configurable. `src/Router/StellaOps.Gateway.WebService/Program.cs`
+- TEST: Coverage exists for options validation, value parsing, authorization middleware, identity header policy, correlation ID, legacy middleware, gateway routes, health/openapi/metrics endpoints, and messaging integration wiring. `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayOptionsValidatorTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayValueParserTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Authorization/AuthorizationMiddlewareTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Authorization/EffectiveClaimsStoreTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/IdentityHeaderPolicyMiddlewareTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/CorrelationIdMiddlewareTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/ClaimsPropagationMiddlewareTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/TenantMiddlewareTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/GatewayRoutesTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Integration/GatewayIntegrationTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Integration/MessagingTransportIntegrationTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/GatewayHealthTests.cs`
+- TEST: Missing tests for health monitor stale/degraded transitions with controlled time, hosted service heartbeat updates, streaming response size/backpressure limits, correlation header validation limits, messaging-enabled validation errors, and transport plugin loader fallback behavior. `src/Router/StellaOps.Gateway.WebService/Services/GatewayHealthMonitorService.cs` `src/Router/StellaOps.Gateway.WebService/Services/GatewayHostedService.cs` `src/Router/StellaOps.Gateway.WebService/Services/GatewayTransportClient.cs` `src/Router/StellaOps.Gateway.WebService/Middleware/CorrelationIdMiddleware.cs` `src/Router/StellaOps.Gateway.WebService/Configuration/GatewayOptionsValidator.cs` `src/Router/StellaOps.Gateway.WebService/Program.cs`
+- Proposed changes (pending approval): enforce correlation ID length/format with fallback, inject TimeProvider into hosted/health services, add response size limits or streaming passthrough, validate positive durations/sizes and messaging config when enabled, gate/remove legacy middleware or mark as legacy-only, log plugin load failures and make plugin path configurable, and add tests for validations, time-based behavior, and plugin fallback.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed. `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj`
+- MAINT: Test SDK/xUnit references are implicit via shared props; the project does not declare them locally, which obscures dependency ownership. `src/Directory.Build.props` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj`
+- MAINT: Tests use Guid.NewGuid for correlation IDs; fixtures are nondeterministic. `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Integration/GatewayIntegrationTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Integration/MessagingTransportIntegrationTests.cs`
+- MAINT: WebApplicationFactory-based health test is tagged as Unit; category labeling is misleading for CI selection. `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/GatewayHealthTests.cs`
+- TEST: Coverage exists for options validation, value parsing, authorization middleware, identity header policy, correlation ID, legacy middleware, gateway routes, health/openapi/metrics endpoints, and messaging integration wiring. `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayOptionsValidatorTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayValueParserTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Authorization/AuthorizationMiddlewareTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Authorization/EffectiveClaimsStoreTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/IdentityHeaderPolicyMiddlewareTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/CorrelationIdMiddlewareTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/ClaimsPropagationMiddlewareTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/TenantMiddlewareTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/GatewayRoutesTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Integration/GatewayIntegrationTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Integration/MessagingTransportIntegrationTests.cs` `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/GatewayHealthTests.cs`
+- TEST: Missing tests for health monitor stale/degraded transitions with controlled time, hosted service heartbeat updates, streaming response size/backpressure limits, correlation header validation limits, and messaging-enabled validation errors. `src/Gateway/StellaOps.Gateway.WebService/Services/GatewayHealthMonitorService.cs` `src/Gateway/StellaOps.Gateway.WebService/Services/GatewayHostedService.cs` `src/Gateway/StellaOps.Gateway.WebService/Services/GatewayTransportClient.cs` `src/Gateway/StellaOps.Gateway.WebService/Middleware/CorrelationIdMiddleware.cs` `src/Gateway/StellaOps.Gateway.WebService/Configuration/GatewayOptionsValidator.cs`
+- Disposition: waived (test project; no apply changes).
+### src/Router/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj
+- MAINT: TreatWarningsAsErrors is set to false in the project file; warning discipline is relaxed. `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj`
+- MAINT: Test SDK/xUnit references are implicit via shared props; the project does not declare them locally, which obscures dependency ownership. `src/Directory.Build.props` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj`
+- MAINT: Tests use Guid.NewGuid for correlation IDs; fixtures are nondeterministic. `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Integration/GatewayIntegrationTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Integration/MessagingTransportIntegrationTests.cs`
+- MAINT: WebApplicationFactory-based health test is tagged as Unit; category labeling is misleading for CI selection. `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/GatewayHealthTests.cs`
+- TEST: Coverage exists for options validation, value parsing, authorization middleware, identity header policy, correlation ID, legacy middleware, gateway routes, health/openapi/metrics endpoints, and messaging integration wiring. `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayOptionsValidatorTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/GatewayValueParserTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Authorization/AuthorizationMiddlewareTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Authorization/EffectiveClaimsStoreTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/IdentityHeaderPolicyMiddlewareTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/CorrelationIdMiddlewareTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/ClaimsPropagationMiddlewareTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/TenantMiddlewareTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/GatewayRoutesTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Integration/GatewayIntegrationTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Integration/MessagingTransportIntegrationTests.cs` `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/GatewayHealthTests.cs`
+- TEST: Missing tests for health monitor stale/degraded transitions with controlled time, hosted service heartbeat updates, streaming response size/backpressure limits, correlation header validation limits, messaging-enabled validation errors, and transport plugin loader fallback behavior. `src/Router/StellaOps.Gateway.WebService/Services/GatewayHealthMonitorService.cs` `src/Router/StellaOps.Gateway.WebService/Services/GatewayHostedService.cs` `src/Router/StellaOps.Gateway.WebService/Services/GatewayTransportClient.cs` `src/Router/StellaOps.Gateway.WebService/Middleware/CorrelationIdMiddleware.cs` `src/Router/StellaOps.Gateway.WebService/Configuration/GatewayOptionsValidator.cs` `src/Router/StellaOps.Gateway.WebService/Program.cs`
+- Disposition: waived (test project; no apply changes).
+### src/Graph/StellaOps.Graph.Api/StellaOps.Graph.Api.csproj
+- MAINT: TreatWarningsAsErrors and LangVersion preview are not set in the project file; warning discipline and preview feature alignment are inconsistent with the repo standard. `src/Graph/StellaOps.Graph.Api/StellaOps.Graph.Api.csproj`
+- MAINT: Graph API registers in-memory repository/services/rate limiter/audit logger unconditionally; no environment gating or persistence. `src/Graph/StellaOps.Graph.Api/Program.cs`
+- MAINT: Auth logic only checks Authorization header presence and trusts X-Stella-Scopes/X-Stella-Tenant headers; X-StellaOps-* headers are ignored, and scopes can be spoofed without gateway enforcement. `src/Graph/StellaOps.Graph.Api/Program.cs`
+- MAINT: LogAudit writes the raw Authorization header as actor and uses Console.WriteLine; audit logs can leak tokens and bypass structured logging. `src/Graph/StellaOps.Graph.Api/Program.cs` `src/Graph/StellaOps.Graph.Api/Services/IAuditLogger.cs`
+- MAINT: Cursor resume URLs are hard-coded to `https://gateway.local`, so responses are not portable across environments. `src/Graph/StellaOps.Graph.Api/Services/InMemoryGraphSearchService.cs` `src/Graph/StellaOps.Graph.Api/Services/InMemoryGraphQueryService.cs`
+- MAINT: InMemoryGraphExportService uses Guid.NewGuid/DateTimeOffset.UtcNow for job IDs and timestamps, stores jobs in an unbounded Dictionary without synchronization or retention. `src/Graph/StellaOps.Graph.Api/Services/InMemoryGraphExportService.cs`
+- MAINT: InMemoryReachabilityDeltaService uses DateTimeOffset.UtcNow for ComputedAt and HashSet/Except on ReachabilityPathData with list members; changed-path ordering and equality are unstable. `src/Graph/StellaOps.Graph.Api/Services/InMemoryReachabilityDeltaService.cs`
+- MAINT: QueryValidator budget tiles error message says "1 to 5000" while validation allows up to 6000; messaging is inconsistent. `src/Graph/StellaOps.Graph.Api/Contracts/SearchContracts.cs`
+- TEST: Coverage exists for search/query/path/diff/export/lineage services, budget enforcement, audit logger behavior, rate limiter windows, metrics, and deterministic ordering. `src/Graph/__Tests/StellaOps.Graph.Api.Tests/SearchServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/QueryServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/PathServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/DiffServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/ExportServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/LineageServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/GraphApiContractTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/AuditLoggerTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/RateLimiterServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/MetricsTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/LoadTests.cs`
+- TEST: Missing tests for minimal API endpoints (header validation, rate limit errors, export download), reachability delta service behavior, and cursor base URL configuration. `src/Graph/StellaOps.Graph.Api/Program.cs` `src/Graph/StellaOps.Graph.Api/Services/InMemoryReachabilityDeltaService.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors and LangVersion preview; gate in-memory services to dev/test or add persistence; enforce auth via validated claims and accept X-StellaOps-* headers; redact tokens in audit logs and use structured logging; make cursor base URL configurable/relative; inject TimeProvider/ID generator; add export job retention and thread-safe storage; fix budget tiles error message; add tests for endpoint validation, reachability deltas, and cursor URLs.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Graph/__Tests/StellaOps.Graph.Api.Tests/StellaOps.Graph.Api.Tests.csproj
+- MAINT: TreatWarningsAsErrors and LangVersion preview are not set in the project file; warning discipline and preview feature alignment are inconsistent with the repo standard. `src/Graph/__Tests/StellaOps.Graph.Api.Tests/StellaOps.Graph.Api.Tests.csproj`
+- MAINT: Test SDK/xUnit references are implicit via shared props; the project uses Update entries only, which obscures dependency ownership. `src/Directory.Build.props` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/StellaOps.Graph.Api.Tests.csproj`
+- MAINT: Non-ASCII/mojibake characters appear in comments, violating ASCII-only portability guidance. `src/Graph/__Tests/StellaOps.Graph.Api.Tests/GraphApiContractTests.cs`
+- MAINT: Contract/load tests are tagged as Unit; category labeling is misleading for CI selection. `src/Graph/__Tests/StellaOps.Graph.Api.Tests/GraphApiContractTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/LoadTests.cs`
+- TEST: Coverage exists for audit logger, diff/export/lineage/path/query/search services, rate limiter windows, metrics, load ordering, and contract behaviors. `src/Graph/__Tests/StellaOps.Graph.Api.Tests/AuditLoggerTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/DiffServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/ExportServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/LineageServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/PathServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/QueryServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/SearchServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/RateLimiterServiceTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/MetricsTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/LoadTests.cs` `src/Graph/__Tests/StellaOps.Graph.Api.Tests/GraphApiContractTests.cs`
+- TEST: Missing tests for minimal API endpoint header enforcement and error responses, export download endpoint, reachability delta service behavior, and cursor base URL configuration. `src/Graph/StellaOps.Graph.Api/Program.cs` `src/Graph/StellaOps.Graph.Api/Services/InMemoryReachabilityDeltaService.cs`
+- Disposition: waived (test project; no apply changes).
+### src/Graph/StellaOps.Graph.Indexer/StellaOps.Graph.Indexer.csproj
+- MAINT: TreatWarningsAsErrors is not set in the project file; warning discipline is relaxed. `src/Graph/StellaOps.Graph.Indexer/StellaOps.Graph.Indexer.csproj`
+- MAINT: In-memory writer/idempotency/analytics stores are registered by default with no persistence or retention; long-running indexers can grow without bound. `src/Graph/StellaOps.Graph.Indexer/Incremental/GraphChangeStreamServiceCollectionExtensions.cs` `src/Graph/StellaOps.Graph.Indexer/Ingestion/Sbom/SbomIngestServiceCollectionExtensions.cs` `src/Graph/StellaOps.Graph.Indexer/Analytics/GraphAnalyticsServiceCollectionExtensions.cs` `src/Graph/StellaOps.Graph.Indexer/Ingestion/Sbom/InMemoryGraphDocumentWriter.cs` `src/Graph/StellaOps.Graph.Indexer/Incremental/InMemoryIdempotencyStore.cs` `src/Graph/StellaOps.Graph.Indexer/Analytics/InMemoryGraphAnalyticsWriter.cs`
+- MAINT: GraphChangeStreamOptions.MaxBatchSize is unused; change stream processing does not enforce batch limits, so configuration is misleading. `src/Graph/StellaOps.Graph.Indexer/Incremental/GraphChangeStreamOptions.cs` `src/Graph/StellaOps.Graph.Indexer/Incremental/GraphChangeStreamProcessor.cs`
+- MAINT: Change stream lag and snapshot export timestamps use DateTimeOffset.UtcNow directly; time is not injectable for deterministic tests. `src/Graph/StellaOps.Graph.Indexer/Incremental/GraphChangeStreamProcessor.cs` `src/Graph/StellaOps.Graph.Indexer/Ingestion/Sbom/SbomSnapshotExporter.cs`
+- MAINT: Change stream and analytics options are not validated; zero/negative intervals or backoffs can throw at runtime when PeriodicTimer starts. `src/Graph/StellaOps.Graph.Indexer/Incremental/GraphChangeStreamServiceCollectionExtensions.cs` `src/Graph/StellaOps.Graph.Indexer/Analytics/GraphAnalyticsServiceCollectionExtensions.cs` `src/Graph/StellaOps.Graph.Indexer/Incremental/GraphChangeStreamOptions.cs` `src/Graph/StellaOps.Graph.Indexer/Analytics/GraphAnalyticsOptions.cs`
+- MAINT: InMemoryGraphDocumentWriter exposes batches via ConcurrentBag with nondeterministic ordering; tests or consumers relying on order can be flaky. `src/Graph/StellaOps.Graph.Indexer/Ingestion/Sbom/InMemoryGraphDocumentWriter.cs`
+- TEST: Coverage exists for analytics engine/pipeline, change stream processor, snapshot builder/exporter, overlay exporter, graph identity/canonicalization, inspector transformer, SBOM lineage transformer, core logic, and end-to-end indexing flows. `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphAnalyticsEngineTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphAnalyticsPipelineTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphChangeStreamProcessorTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphSnapshotBuilderTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/SbomSnapshotExporterTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphOverlayExporterTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphIdentityTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphInspectorTransformerTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/SbomLineageTransformerTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphCoreLogicTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphIndexerEndToEndTests.cs`
+- TEST: Missing tests for hosted service scheduling loops, options validation (invalid intervals/backoff), snapshot exporter deterministic timestamps, change-stream lag parsing failures, and in-memory writer/idempotency retention/ordering. `src/Graph/StellaOps.Graph.Indexer/Analytics/GraphAnalyticsHostedService.cs` `src/Graph/StellaOps.Graph.Indexer/Incremental/GraphChangeStreamProcessor.cs` `src/Graph/StellaOps.Graph.Indexer/Ingestion/Sbom/SbomSnapshotExporter.cs` `src/Graph/StellaOps.Graph.Indexer/Incremental/GraphChangeStreamOptions.cs` `src/Graph/StellaOps.Graph.Indexer/Analytics/GraphAnalyticsOptions.cs` `src/Graph/StellaOps.Graph.Indexer/Ingestion/Sbom/InMemoryGraphDocumentWriter.cs` `src/Graph/StellaOps.Graph.Indexer/Incremental/InMemoryIdempotencyStore.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; inject TimeProvider into change stream/exporter; validate options on startup and either enforce or remove MaxBatchSize; gate in-memory stores to dev/test or add retention/persistence; make in-memory batch ordering explicit; add tests for hosted service scheduling, options validation, lag parsing, and deterministic timestamps.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/StellaOps.Graph.Indexer.Persistence.csproj
+- MAINT: TreatWarningsAsErrors is not set in the project file; warning discipline is relaxed. `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/StellaOps.Graph.Indexer.Persistence.csproj`
+- MAINT: GraphIndexerDbContext is a stub with no DbSets and is not registered; it is unused and adds dead code surface until scaffolding lands. `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/EfCore/Context/GraphIndexerDbContext.cs`
+- MAINT: Persistence options are configured but not validated on startup; invalid schema/connection settings fail late. `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Extensions/GraphIndexerPersistenceExtensions.cs`
+- MAINT: Repository timestamps use DateTimeOffset.UtcNow directly; time is not injectable for deterministic tests. `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Postgres/Repositories/PostgresIdempotencyStore.cs` `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Postgres/Repositories/PostgresGraphSnapshotProvider.cs` `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Postgres/Repositories/PostgresGraphDocumentWriter.cs`
+- MAINT: Batch and fallback node IDs use Guid.NewGuid, which produces nondeterministic stored documents. `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Postgres/Repositories/PostgresGraphDocumentWriter.cs`
+- MAINT: Snapshot enqueue serializes nodes/edges in input order without enforcing stable ordering, so persisted snapshots can vary with caller ordering. `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Postgres/Repositories/PostgresGraphSnapshotProvider.cs`
+- TEST: No tests in this project for repositories, schema initialization, or deterministic IDs/timestamps; dedicated tests project exists but is pending audit. `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/StellaOps.Graph.Indexer.Persistence.csproj` `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/StellaOps.Graph.Indexer.Persistence.Tests.csproj`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; validate PostgresOptions on startup; inject TimeProvider and ID generator; make snapshot node/edge ordering explicit and deterministic; require stable IDs or derive IDs deterministically when missing; add tests for schema creation, idempotency behavior, snapshot queue ordering, and deterministic timestamps/IDs.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/StellaOps.Graph.Indexer.Persistence.Tests.csproj
+- MAINT: Integration tests are tagged as Unit; category labels are misleading for CI selection. `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/GraphQueryDeterminismTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/GraphStorageMigrationTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/PostgresIdempotencyStoreTests.cs`
+- MAINT: Non-ASCII/mojibake characters appear in header comments, violating ASCII-only guidance. `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/GraphQueryDeterminismTests.cs`
+- MAINT: Tests use Guid.NewGuid for tokens, reducing determinism and repeatability. `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/GraphQueryDeterminismTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/PostgresIdempotencyStoreTests.cs`
+- MAINT: Schema assertions target tables/columns/indexes that do not exist in the current persistence DDL (expects graph_snapshots/graph_analytics/graph_idempotency and tenant_id/node_type/data/created_at). `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/GraphStorageMigrationTests.cs` `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Postgres/Repositories/PostgresGraphDocumentWriter.cs` `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Postgres/Repositories/PostgresGraphSnapshotProvider.cs` `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Postgres/Repositories/PostgresIdempotencyStore.cs`
+- MAINT: Migration test does not validate real migrations; fixture returns a no-op and no migrations exist, so schema creation is not exercised. `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/GraphIndexerPostgresFixture.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/GraphStorageMigrationTests.cs`
+- MAINT: Determinism tests compare boolean lists with order-insensitive assertions, so ordering guarantees are not actually verified. `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/GraphQueryDeterminismTests.cs`
+- TEST: Coverage exists for idempotency store basic behavior, schema introspection, and determinism smoke checks. `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/PostgresIdempotencyStoreTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/GraphStorageMigrationTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/GraphQueryDeterminismTests.cs`
+- TEST: Missing tests for PostgresGraphDocumentWriter, PostgresGraphSnapshotProvider, PostgresGraphAnalyticsWriter, schema initialization on first use, and deterministic ordering/ID generation. `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Postgres/Repositories/PostgresGraphDocumentWriter.cs` `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Postgres/Repositories/PostgresGraphSnapshotProvider.cs` `src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Postgres/Repositories/PostgresGraphAnalyticsWriter.cs`
+- Proposed changes (optional): reclassify integration tests, replace Guid.NewGuid with fixed IDs, update schema assertions to match current DDL or add migrations, make determinism assertions order-sensitive, and add tests for document/snapshot/analytics writers and schema creation paths.
+- Disposition: waived (test project; no apply changes).
+### src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj
+- MAINT: IsTestProject is not set; test discovery relies on naming conventions and shared props, which obscures intent. `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj`
+- MAINT: Test SDK/xUnit references are implicit via shared props; the project does not declare them locally, which obscures dependency ownership. `src/Directory.Build.props` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj`
+- MAINT: Tests mutate the process-wide STELLAOPS_GRAPH_SNAPSHOT_DIR environment variable without isolation, which can race with parallel tests. `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/SbomIngestServiceCollectionExtensionsTests.cs`
+- MAINT: Temp directory paths are randomized with Guid.NewGuid and cleanup is ad-hoc; prefer deterministic temp helpers to improve repeatability and cleanup consistency. `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/FileSystemSnapshotFileWriterTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/SbomIngestServiceCollectionExtensionsTests.cs`
+- MAINT: Graph Indexer tests also exist under src/Graph/__Tests/StellaOps.Graph.Indexer.Tests; ownership boundaries are unclear and risk duplicated coverage. `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj`
+- TEST: Coverage exists for SBOM/advisory/policy/VEX transformers, ingest processors, snapshot builder/exporter, file writer, graph identity, and service collection wiring. `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/SbomIngestTransformerTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/AdvisoryLinksetTransformerTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/PolicyOverlayTransformerTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/VexOverlayTransformerTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/SbomIngestProcessorTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/AdvisoryLinksetProcessorTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/PolicyOverlayProcessorTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/GraphSnapshotBuilderTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/SbomSnapshotExporterTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/FileSystemSnapshotFileWriterTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/GraphIdentityTests.cs` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/SbomIngestServiceCollectionExtensionsTests.cs`
+- TEST: Missing tests for default snapshot root resolution when neither options nor env var are set, invalid env var path handling, and FileSystemSnapshotFileWriter cancellation/invalid path behavior. `src/Graph/StellaOps.Graph.Indexer/Ingestion/Sbom/SbomIngestProcessorFactory.cs` `src/Graph/StellaOps.Graph.Indexer/Ingestion/Sbom/FileSystemSnapshotFileWriter.cs`
+- Proposed changes (optional): mark IsTestProject explicitly, add isolation for environment-variable tests (collection or lock), replace random temp paths with deterministic helpers, clarify ownership between duplicate test projects, and add tests for default snapshot root and file writer error/cancellation paths.
+- Disposition: waived (test project; no apply changes).
+### src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj
+- MAINT: IsTestProject is not set and there are no explicit test package references; discovery relies on naming conventions and shared props. `src/Directory.Build.props` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj`
+- MAINT: Tests use DateTimeOffset.UtcNow for generatedAt and provenance; nondeterministic timestamps can leak into manifests/overlays and reduce repeatability. `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphAnalyticsTestData.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphCoreLogicTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphIndexerEndToEndTests.cs`
+- MAINT: Temp directories are created with Guid.NewGuid and manual cleanup; deterministic temp helpers would improve repeatability. `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphOverlayExporterTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/SbomSnapshotExporterTests.cs`
+- MAINT: Non-ASCII arrow characters appear in comments, violating ASCII-only guidance. `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphCoreLogicTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphIndexerEndToEndTests.cs`
+- MAINT: End-to-end tests are tagged as Unit despite spanning multiple components; category labeling is ambiguous for CI selection. `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphIndexerEndToEndTests.cs`
+- MAINT: Graph Indexer tests also exist under src/__Tests/Graph; ownership boundaries are unclear and may duplicate coverage. `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj` `src/__Tests/Graph/StellaOps.Graph.Indexer.Tests/StellaOps.Graph.Indexer.Tests.csproj`
+- TEST: Coverage exists for analytics engine/pipeline, change stream processing, core graph logic, identity determinism, snapshot builder/exporter, inspector transformer, overlay exporter, and end-to-end ingestion flows. `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphAnalyticsEngineTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphAnalyticsPipelineTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphChangeStreamProcessorTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphCoreLogicTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphIdentityTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphSnapshotBuilderTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/SbomSnapshotExporterTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphInspectorTransformerTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphOverlayExporterTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/GraphIndexerEndToEndTests.cs` `src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/SbomLineageTransformerTests.cs`
+- TEST: Missing tests for GraphOverlayExporter manifest output, GraphAnalyticsOptions validation (invalid iterations/sample size), and change stream backfill path behavior. `src/Graph/StellaOps.Graph.Indexer/Analytics/GraphOverlayExporter.cs` `src/Graph/StellaOps.Graph.Indexer/Analytics/GraphAnalyticsOptions.cs` `src/Graph/StellaOps.Graph.Indexer/Incremental/GraphChangeStreamProcessor.cs`
+- Proposed changes (optional): mark IsTestProject explicitly, replace UtcNow with fixed timestamps, use deterministic temp helpers, update comments to ASCII, clarify E2E category, and add tests for overlay manifest, options validation, and backfill behavior.
+- Disposition: waived (test project; no apply changes).
+### src/__Libraries/StellaOps.Infrastructure.EfCore/StellaOps.Infrastructure.EfCore.csproj
+- MAINT: TreatWarningsAsErrors is explicitly disabled, relaxing warning discipline across this shared library. `src/__Libraries/StellaOps.Infrastructure.EfCore/StellaOps.Infrastructure.EfCore.csproj`
+- MAINT: DbContext wiring enables detailed errors unconditionally despite comment indicating dev-only; this can add overhead in production. `src/__Libraries/StellaOps.Infrastructure.EfCore/Extensions/DbContextServiceExtensions.cs`
+- MAINT: TenantConnectionInterceptor interpolates schema name into SQL without validation/quoting, which can break search_path or allow injection if schema name is untrusted. `src/__Libraries/StellaOps.Infrastructure.EfCore/Interceptors/TenantConnectionInterceptor.cs`
+- MAINT: DbContext registration logic is duplicated across three extension methods, increasing drift risk. `src/__Libraries/StellaOps.Infrastructure.EfCore/Extensions/DbContextServiceExtensions.cs`
+- TEST: No tests for tenant session configuration, schema wiring, or tenant accessors. `src/__Libraries/StellaOps.Infrastructure.EfCore/Extensions/DbContextServiceExtensions.cs` `src/__Libraries/StellaOps.Infrastructure.EfCore/Interceptors/TenantConnectionInterceptor.cs` `src/__Libraries/StellaOps.Infrastructure.EfCore/Tenancy/AsyncLocalTenantContextAccessor.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; gate EnableDetailedErrors behind environment/options; validate schema names (or quote identifiers) before building search_path; refactor shared DbContext configuration into a single helper; add tests for tenant session setup, interceptor behavior, and AsyncLocal scope behavior in a new infrastructure test project.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj
+- MAINT: TreatWarningsAsErrors is disabled, relaxing warning discipline in a shared library. `src/__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj`
+- MAINT: PostgresOptions are configured without validation or ValidateOnStart; required ConnectionString and option bounds are not enforced. `src/__Libraries/StellaOps.Infrastructure.Postgres/ServiceCollectionExtensions.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Options/PostgresOptions.cs`
+- MAINT: ConnectionIdleLifetimeSeconds is never applied to the Npgsql connection string, so configured values are ignored. `src/__Libraries/StellaOps.Infrastructure.Postgres/Connections/DataSourceBase.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Options/PostgresOptions.cs`
+- MAINT: Schema name is interpolated without quoting in session setup and migration SQL, which can break search_path or allow injection if schema names are untrusted. `src/__Libraries/StellaOps.Infrastructure.Postgres/Connections/DataSourceBase.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/StartupMigrationHost.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationRunner.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationServiceExtensions.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Testing/PostgresFixture.cs`
+- MAINT: MigrationTelemetry is unused and creates new instruments per call (RecordLockAcquired/RecordChecksumError), which can leak metrics registrations. `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationTelemetry.cs`
+- MAINT: Migration loading/checksum logic is duplicated across MigrationRunner/StartupMigrationHost/MigrationStatusService, increasing drift risk. `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationRunner.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/StartupMigrationHost.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationServiceExtensions.cs`
+- TEST: No tests in this project for DataSourceBase session configuration, migration runner/validator, status service, or exception helper; coverage (if any) lives under the separate tests project pending audit. `src/__Libraries/StellaOps.Infrastructure.Postgres/Connections/DataSourceBase.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationRunner.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationValidator.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationServiceExtensions.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Exceptions/PostgresExceptionHelper.cs` `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; add options validation + ValidateOnStart (ConnectionString, schema name, timeouts, pool bounds); apply ConnectionIdleLifetimeSeconds to the connection string; quote or validate schema identifiers across session/migration SQL; consolidate migration loading/checksum logic; wire or remove MigrationTelemetry; add tests for session setup, migrations, status, and exception helper.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing/StellaOps.Infrastructure.Postgres.Testing.csproj
+- MAINT: TreatWarningsAsErrors is disabled in the project file. `src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing/StellaOps.Infrastructure.Postgres.Testing.csproj`
+- MAINT: OutputType is set to Exe and UseAppHost true for a test infrastructure library with no entry point; prefer Library to avoid apphost churn. `src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing/StellaOps.Infrastructure.Postgres.Testing.csproj`
+- MAINT: MigrationTestAttribute and MigrationTestCollection are unused and their options are not wired into MigrationTestBase, so TruncateBefore/After settings are ignored. `src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing\MigrationTestAttribute.cs`
+- MAINT: Docker skip detection only handles specific ArgumentException patterns; other Testcontainers startup failures will fail the test run instead of skip. `src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing\PostgresIntegrationFixture.cs`
+- MAINT: Postgres image tag is not configurable or pinned to a digest; updates to postgres:16-alpine can change test behavior. `src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing\PostgresIntegrationFixture.cs`
+- TEST: No tests for fixture initialization, migration execution, truncation behavior, or skip logic in this helper library. `src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing\PostgresIntegrationFixture.cs` `src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing\MigrationTestAttribute.cs`
+- Proposed changes (optional): set OutputType to Library and remove UseAppHost; enable TreatWarningsAsErrors; either remove MigrationTestAttribute or make MigrationTestBase honor it; broaden Docker detection/skip logic and allow image override/pinning; add minimal tests for skip paths and truncate/migration helpers.
+- Disposition: waived (test project; no apply changes).
+### src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj
+- MAINT: Integration tests are labeled as Unit or lack category tags, which makes CI selection unreliable. `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/PostgresFixtureTests.cs` `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/Migrations/StartupMigrationHostTests.cs`
+- MAINT: Tests rely on Testcontainers without skip handling when Docker is unavailable; failures will block offline/CI runs. `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/PostgresFixtureTests.cs` `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/Migrations/StartupMigrationHostTests.cs`
+- MAINT: Schema names use Guid.NewGuid-derived values; nondeterministic schemas make logs harder to reproduce. `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/PostgresFixtureTests.cs` `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/Migrations/StartupMigrationHostTests.cs`
+- MAINT: Postgres image tag is hardcoded and not configurable or pinned to a digest. `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/PostgresFixtureTests.cs` `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/Migrations/StartupMigrationHostTests.cs`
+- TEST: Coverage exists for MigrationCategoryExtensions classification, StartupMigrationHost behaviors (pending/release/checksum/lock), and PostgresFixture schema/truncate/dispose. `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/Migrations/MigrationCategoryTests.cs` `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/Migrations/StartupMigrationHostTests.cs` `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/PostgresFixtureTests.cs`
+- TEST: Missing tests for MigrationRunner, MigrationStatusService, MigrationValidator error paths, MigrationTelemetry wiring, DataSourceBase session configuration, and PostgresExceptionHelper. `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationRunner.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationServiceExtensions.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationValidator.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationTelemetry.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Connections/DataSourceBase.cs` `src/__Libraries/StellaOps.Infrastructure.Postgres/Exceptions/PostgresExceptionHelper.cs`
+- Proposed changes (optional): tag integration tests as Integration (not Unit) and add categories for StartupMigrationHost tests; add Docker skip logic or conditional execution; allow image override/pinning; use deterministic schema naming based on test name; expand coverage for migration runner/status and session configuration.
+- Disposition: waived (test project; no apply changes).
+### src/__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj
+- MAINT: TreatWarningsAsErrors is not set in the project file, so warnings are not enforced. `src/__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj`
+- MAINT: ActivitySource and Meter are created without a version string, which makes instrumentation versions ambiguous in telemetry backends. `src/__Libraries/StellaOps.Ingestion.Telemetry/IngestionTelemetry.cs`
+- MAINT: Tag keys are repeated as literals and not centralized; phase/result values are free-form strings (RecordLatency/RecordWriteAttempt), which can drift and increase cardinality. `src/__Libraries/StellaOps.Ingestion.Telemetry/IngestionTelemetry.cs`
+- TEST: No tests for activity tags, metric tags, or phase/result validation. `src/__Libraries/StellaOps.Ingestion.Telemetry/IngestionTelemetry.cs`
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; add shared constants for tag keys; validate/normalize phase and result values against known constants; set ActivitySource/Meter version (assembly or explicit); add tests using ActivityListener/MeterListener to assert tags and invalid input handling.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/__Tests/Integration/StellaOps.Integration.AirGap/StellaOps.Integration.AirGap.csproj
+- MAINT: Fixture uses Guid.NewGuid and DateTime.UtcNow for IDs and timestamps, which makes test artifacts nondeterministic and harder to reproduce. `src/__Tests/Integration/StellaOps.Integration.AirGap/AirGapTestFixture.cs`
+- MAINT: Offline kit handling falls back to a default manifest when the file is missing, so tests can pass even if the offline kit is absent. `src/__Tests/Integration/StellaOps.Integration.AirGap/AirGapTestFixture.cs` `src/__Tests/Integration/StellaOps.Integration.AirGap/AirGapIntegrationTests.cs`
+- MAINT: DNS monitor is never invoked, so DNS-related tests only validate the stub, not actual behavior. `src/__Tests/Integration/StellaOps.Integration.AirGap/AirGapTestFixture.cs` `src/__Tests/Integration/StellaOps.Integration.AirGap/AirGapIntegrationTests.cs`
+- MAINT: Several usings are unused (System.Net, System.Net.Sockets, Moq), which adds noise to the test file. `src/__Tests/Integration/StellaOps.Integration.AirGap/AirGapIntegrationTests.cs`
+- TEST: Coverage exists for offline kit manifest, offline scan/replay/verification flows, and offline-network guard rails via fixture simulation. `src/__Tests/Integration/StellaOps.Integration.AirGap/AirGapIntegrationTests.cs`
+- TEST: No tests for actual scanner/attestor/CLI integration or verifying offline kit file copies, only simulated flows. `src/__Tests/Integration/StellaOps.Integration.AirGap/AirGapTestFixture.cs` `src/__Tests/Integration/StellaOps.Integration.AirGap/AirGapIntegrationTests.cs`
+- Proposed changes (optional): introduce deterministic time/IDs in the fixture; fail fast when offline kit is missing; wire DNS monitor hooks or drop the DNS test; remove unused usings; expand coverage to exercise real offline kit installation and at least one scanner/attestor integration path.
+- Disposition: waived (test project; no apply changes).
+### src/__Tests/Integration/StellaOps.Integration.Determinism/StellaOps.Integration.Determinism.csproj
+- MAINT: Several tests use DateTime.UtcNow when building fixtures, which can make snapshots nondeterministic across runs. `src/__Tests/Integration/StellaOps.Integration.Determinism/BinaryEvidenceDeterminismTests.cs`
+- MAINT: ConcurrentScoring_MaintainsDeterminism uses BeEquivalentTo, which ignores ordering; it won't detect order regressions. `src/__Tests/Integration/StellaOps.Integration.Determinism/DeterminismValidationTests.cs`
+- MAINT: Golden vector replay test does not assert a fixed expected hash, so it cannot detect regressions. `src/__Tests/Integration/StellaOps.Integration.Determinism/DeterminismValidationTests.cs`
+- MAINT: Determinism helpers generate outputs without asserting or using the determinism corpus on disk, so corpus drift can go unnoticed. `src/__Tests/Integration/StellaOps.Integration.Determinism/AirGapBundleDeterminismTests.cs` `src/__Tests/Integration/StellaOps.Integration.Determinism/SbomDeterminismTests.cs` `src/__Tests/Integration/StellaOps.Integration.Determinism/VexDeterminismTests.cs`
+- TEST: Coverage exists across airgap bundle, evidence bundle, SBOM, VEX, policy, reachability, triage output, verdict artifacts, and full verdict pipeline determinism. `src/__Tests/Integration/StellaOps.Integration.Determinism/AirGapBundleDeterminismTests.cs` `src/__Tests/Integration/StellaOps.Integration.Determinism/EvidenceBundleDeterminismTests.cs` `src/__Tests/Integration/StellaOps.Integration.Determinism/SbomDeterminismTests.cs` `src/__Tests/Integration/StellaOps.Integration.Determinism/VexDeterminismTests.cs` `src/__Tests/Integration/StellaOps.Integration.Determinism/PolicyDeterminismTests.cs` `src/__Tests/Integration/StellaOps.Integration.Determinism/ReachabilityEvidenceDeterminismTests.cs` `src/__Tests/Integration/StellaOps.Integration.Determinism/TriageOutputDeterminismTests.cs` `src/__Tests/Integration/StellaOps.Integration.Determinism/VerdictArtifactDeterminismTests.cs` `src/__Tests/Integration/StellaOps.Integration.Determinism/FullVerdictPipelineDeterminismTests.cs`
+- TEST: Missing explicit tests that assert determinism corpus outputs match on-disk fixtures; current tests mostly validate internal helpers. `src/__Tests/Integration/StellaOps.Integration.Determinism/StellaOps.Integration.Determinism.csproj`
+- Proposed changes (optional): replace DateTime.UtcNow in fixtures with fixed timestamps; make concurrency assertions order-sensitive; lock in golden hash values; load and compare against determinism corpus fixtures; add regression tests for corpus drift.
+- Disposition: waived (test project; no apply changes).
+### src/__Tests/Integration/StellaOps.Integration.E2E/StellaOps.Integration.E2E.csproj
+- MAINT: Non-ASCII/mojibake characters appear in comments and diff messages, violating ASCII-only guidance. `src/__Tests/Integration/StellaOps.Integration.E2E/StellaOps.Integration.E2E.csproj` `src/__Tests/Integration/StellaOps.Integration.E2E/ManifestComparer.cs`
+- MAINT: ManifestComparer CompareJson uses ToDictionary without a StringComparer, so JSON object property comparisons are culture-sensitive. `src/__Tests/Integration/StellaOps.Integration.E2E/ManifestComparer.cs`
+- MAINT: E2E fixture and reach-graph builders use Guid.NewGuid and DateTime.UtcNow, which introduce nondeterminism in an E2E reproducibility suite. `src/__Tests/Integration/StellaOps.Integration.E2E/E2EReproducibilityTestFixture.cs` `src/__Tests/Integration/StellaOps.Integration.E2E/ReachGraphE2ETests.cs`
+- MAINT: Testcontainers are used without Docker skip handling; tests will fail rather than skip when Docker is unavailable. `src/__Tests/Integration/StellaOps.Integration.E2E/E2EReproducibilityTestFixture.cs`
+- TEST: Coverage exists for end-to-end reproducibility (verdict hash, bundle manifest, frozen timestamps, parallel runs), manifest diffing, and reach-graph pipeline flows. `src/__Tests/Integration/StellaOps.Integration.E2E/E2EReproducibilityTests.cs` `src/__Tests/Integration/StellaOps.Integration.E2E/ManifestComparer.cs` `src/__Tests/Integration/StellaOps.Integration.E2E/ReachGraphE2ETests.cs`
+- TEST: No tests validate E2E reproducibility against the golden baseline fixtures in `baselines/` output, only internal comparisons. `src/__Tests/Integration/StellaOps.Integration.E2E/StellaOps.Integration.E2E.csproj`
+- Proposed changes (optional): normalize non-ASCII strings to ASCII; use StringComparer.Ordinal for JSON property comparison; remove Guid/DateTime.UtcNow from deterministic test data; add Docker skip logic; add baseline comparison tests using the determinism corpus fixtures.
+- Disposition: waived (test project; no apply changes).
+### src/__Tests/Integration/StellaOps.Integration.Performance/StellaOps.Integration.Performance.csproj
+- MAINT: Fixture writes baseline and report files to AppContext.BaseDirectory, which can be a build output directory and hard to inspect/clean. `src/__Tests/Integration/StellaOps.Integration.Performance/PerformanceTestFixture.cs`
+- MAINT: Baseline defaults are used when the file is missing, so tests can pass without baselines. `src/__Tests/Integration/StellaOps.Integration.Performance/PerformanceTestFixture.cs` `src/__Tests/Integration/StellaOps.Integration.Performance/PerformanceBaselineTests.cs`
+- MAINT: Performance report uses DateTime.UtcNow and sample manifest uses Guid.NewGuid, making reports nondeterministic. `src/__Tests/Integration/StellaOps.Integration.Performance/PerformanceBaselineTests.cs`
+- MAINT: Tests simulate async delays and random data rather than exercising real code paths; baselines may not reflect actual pipeline performance. `src/__Tests/Integration/StellaOps.Integration.Performance/PerformanceBaselineTests.cs`
+- TEST: Coverage exists for score computation, proof bundle, signing, call graph extraction, reachability, and regression reporting (simulated). `src/__Tests/Integration/StellaOps.Integration.Performance/PerformanceBaselineTests.cs`
+- TEST: No tests validate baselines against real fixture data or enforce that baselines exist. `src/__Tests/Integration/StellaOps.Integration.Performance/PerformanceTestFixture.cs` `src/__Tests/Integration/StellaOps.Integration.Performance/PerformanceBaselineTests.cs`
+- Proposed changes (optional): fail if baseline file is missing; write outputs under repo `artifacts/` or temp; use fixed timestamps/IDs in reports; introduce a minimal real-path benchmark (e.g., policy scoring on fixed fixtures) or mark these as synthetic; add guard for performance tests in CI.
+- Disposition: waived (test project; no apply changes).
+### src/__Tests/Integration/StellaOps.Integration.Platform/StellaOps.Integration.Platform.csproj
+- MAINT: Testcontainers used without Docker skip handling; tests will fail instead of skipping when Docker is unavailable. `src/__Tests/Integration/StellaOps.Integration.Platform/PostgresOnlyStartupTests.cs`
+- MAINT: Tests create schemas/tables without cleanup, which can leak state across runs. `src/__Tests/Integration/StellaOps.Integration.Platform/PostgresOnlyStartupTests.cs`
+- MAINT: Uses DateTime.UtcNow implicitly via DEFAULT NOW() and no frozen time in migration test, so results can vary. `src/__Tests/Integration/StellaOps.Integration.Platform/PostgresOnlyStartupTests.cs`
+- MAINT: EnvironmentVariables_ContainNoMongoDbReferences inspects only keys, not values; it can miss MongoDB references in values. `src/__Tests/Integration/StellaOps.Integration.Platform/PostgresOnlyStartupTests.cs`
+- TEST: Coverage exists for PostgreSQL container startup, CRUD, migration DDL, extension creation, and basic config checks. `src/__Tests/Integration/StellaOps.Integration.Platform/PostgresOnlyStartupTests.cs`
+- TEST: No tests validate service startup wiring or log scanning for MongoDB connection attempts; currently only checks configuration patterns. `src/__Tests/Integration/StellaOps.Integration.Platform/PostgresOnlyStartupTests.cs`
+- Proposed changes (optional): add Docker skip handling; clean up schemas/tables in DisposeAsync; use deterministic timestamps where asserted; scan env var values for Mongo references; add log capture to assert no MongoDB connection attempts.
+- Disposition: waived (test project; no apply changes).
+### src/__Tests/Integration/StellaOps.Integration.ProofChain/StellaOps.Integration.ProofChain.csproj
+- MAINT: Tests generate SBOM timestamps with DateTimeOffset.UtcNow, which makes inputs nondeterministic. `src/__Tests/Integration/StellaOps.Integration.ProofChain/ProofChainIntegrationTests.cs`
+- MAINT: Testcontainers used without Docker skip handling; tests fail when Docker is unavailable. `src/__Tests/Integration/StellaOps.Integration.ProofChain/ProofChainTestFixture.cs`
+- MAINT: Tests do not clean up scan data between runs; repeated runs can accumulate data in the test database. `src/__Tests/Integration/StellaOps.Integration.ProofChain/ProofChainIntegrationTests.cs`
+- TEST: Coverage exists for scan submission → manifest, deterministic scoring, proof bundle generation, proof verification, tamper detection, and score replay. `src/__Tests/Integration/StellaOps.Integration.ProofChain/ProofChainIntegrationTests.cs`
+- TEST: No tests validate fixed expected hashes or deterministic timestamps in manifests/proofs. `src/__Tests/Integration/StellaOps.Integration.ProofChain/ProofChainIntegrationTests.cs`
+- Proposed changes (optional): use fixed timestamps in SBOM creation; add Docker skip handling; delete created scans or reset DB between tests; add assertions for deterministic timestamps/hash outputs.
+- Disposition: waived (test project; no apply changes).
+### src/__Tests/Integration/StellaOps.Integration.Reachability/StellaOps.Integration.Reachability.csproj
+- MAINT: Java corpus test returns early without explicit skip, hiding missing fixtures and reducing signal. `src/__Tests/Integration/StellaOps.Integration.Reachability/ReachabilityIntegrationTests.cs`
+- MAINT: Reachability tests rely on corpus JSON only and never exercise actual reachability engine code paths. `src/__Tests/Integration/StellaOps.Integration.Reachability/ReachabilityIntegrationTests.cs`
+- MAINT: No deterministic hashing or validation of corpus fixture integrity (hashes/signatures), so fixture drift can go unnoticed. `src/__Tests/Integration/StellaOps.Integration.Reachability/ReachabilityTestFixture.cs`
+- TEST: Coverage exists for corpus parsing, entrypoint discovery, ground truth reachability, explanation tiers, and VEX presence. `src/__Tests/Integration/StellaOps.Integration.Reachability/ReachabilityIntegrationTests.cs`
+- TEST: Missing tests that validate unreachable paths or detect missing corpus languages explicitly. `src/__Tests/Integration/StellaOps.Integration.Reachability/ReachabilityIntegrationTests.cs`
+- Proposed changes (optional): replace early return with explicit skip reason; add fixture integrity checks (hash list); add tests that assert unreachable cases; add at least one test that runs reachability computation against the corpus.
+- Disposition: waived (test project; no apply changes).
+### src/__Tests/Integration/StellaOps.Integration.Unknowns/StellaOps.Integration.Unknowns.csproj
+- MAINT: Project references Microsoft.AspNetCore.Mvc.Testing, Testcontainers, and Testcontainers.PostgreSql but no tests use them; extra dependencies add noise and maintenance. `src/__Tests/Integration/StellaOps.Integration.Unknowns/StellaOps.Integration.Unknowns.csproj`
+- MAINT: UnknownsWorkflowTests defines its own UnknownEntry/UnknownRanker; integration tests are not exercising Policy.Unknowns/Policy.Scoring and risk drift. `src/__Tests/Integration/StellaOps.Integration.Unknowns/UnknownsWorkflowTests.cs`
+- MAINT: Tests use DateTimeOffset.UtcNow and Guid.NewGuid in inputs and assertions (BeCloseTo against current time); nondeterministic and can be flaky. `src/__Tests/Integration/StellaOps.Integration.Unknowns/UnknownsWorkflowTests.cs`
+- TEST: Coverage exists for ranking determinism, band thresholds, escalation, resolution, and band history, but only on the local helper types. `src/__Tests/Integration/StellaOps.Integration.Unknowns/UnknownsWorkflowTests.cs`
+- TEST: No tests cover actual unknowns workflow integration (policy models, scoring integration, persistence/API paths). `src/__Tests/Integration/StellaOps.Integration.Unknowns/UnknownsWorkflowTests.cs`
+- Proposed changes (optional): use production unknowns models/ranker, add a simple integration path through Policy.Unknowns and Policy.Scoring, remove unused packages or add tests that use them, and pin deterministic timestamps for assertions.
+- Disposition: waived (test project; no apply changes).
+### src/__Libraries/StellaOps.Interop/StellaOps.Interop.csproj
+- MAINT: RunAsync has no timeout and does not terminate the process on cancellation, which can leave orphaned tools. `src/__Libraries/StellaOps.Interop/ToolManager.cs`
+- MAINT: FindOnPath only checks .exe on Windows and ignores PATHEXT (.cmd/.bat), so script-based tools may not resolve. `src/__Libraries/StellaOps.Interop/ToolManager.cs`
+- MAINT: Argument handling uses a raw string; there is no helper for safe ArgumentList construction or quoting. `src/__Libraries/StellaOps.Interop/ToolManager.cs`
+- MAINT: WorkingDirectory is accepted as-is; a missing directory throws a Win32Exception instead of a clear preflight error. `src/__Libraries/StellaOps.Interop/ToolManager.cs`
+- TEST: No automated tests for ToolManager path resolution, run success/failure handling, or cancellation behavior.
+- Proposed changes (pending approval): add timeout support and cancel-safe process termination; support PATHEXT on Windows; add an ArgumentList helper; validate working directory; add unit tests for path resolution and RunAsync error/cancellation paths.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/__Tests/interop/StellaOps.Interop.Tests/StellaOps.Interop.Tests.csproj
+- MAINT: Test project lacks Microsoft.NET.Test.Sdk and xUnit packages; tests will not be discovered or run in CI. `src/__Tests/interop/StellaOps.Interop.Tests/StellaOps.Interop.Tests.csproj`
+- MAINT: Tests reimplement ToolManager locally and do not reference the production interop library, which invites drift. `src/__Tests/interop/StellaOps.Interop.Tests/ToolManager.cs`
+- MAINT: Tests depend on external tools and container images with no explicit skip when tools are missing or network is unavailable; CI/local failures are likely. `src/__Tests/interop/StellaOps.Interop.Tests/InteropTestHarness.cs`, `src/__Tests/interop/StellaOps.Interop.Tests/CycloneDx/CycloneDxRoundTripTests.cs`, `src/__Tests/interop/StellaOps.Interop.Tests/Spdx/SpdxRoundTripTests.cs`
+- MAINT: Cosign attestation test exits early with a bare return instead of an explicit skip, hiding skipped coverage. `src/__Tests/interop/StellaOps.Interop.Tests/CycloneDx/CycloneDxRoundTripTests.cs`
+- TEST: Parity test uses placeholder findings parsing (always empty) and therefore does not validate parity in practice. `src/__Tests/interop/StellaOps.Interop.Tests/InteropTestHarness.cs`, `src/__Tests/interop/StellaOps.Interop.Tests/CycloneDx/CycloneDxRoundTripTests.cs`
+- TEST: Schema validation tests only check string presence; TODOs remain for real SPDX/CycloneDX schema validation and consumer compatibility. `src/__Tests/interop/StellaOps.Interop.Tests/CycloneDx/CycloneDxRoundTripTests.cs`, `src/__Tests/interop/StellaOps.Interop.Tests/Spdx/SpdxRoundTripTests.cs`
+- Proposed changes (optional): add test SDK and xUnit packages; reference the production interop library; add explicit skips for missing tools and offline mode; implement Grype parsing and parity assertions; replace TODOs with real schema validation using local schema files.
+- Disposition: waived (test project; no apply changes).
+### src/__Libraries/StellaOps.IssuerDirectory.Client/StellaOps.IssuerDirectory.Client.csproj
+- MAINT: TreatWarningsAsErrors is disabled, reducing warning discipline in a shared client library. `src/__Libraries/StellaOps.IssuerDirectory.Client/StellaOps.IssuerDirectory.Client.csproj`
+- MAINT: GetIssuer* trims tenant/issuer, but Set/Delete do not trim issuerId; this can send whitespace and fail cache invalidation for trimmed keys. `src/__Libraries/StellaOps.IssuerDirectory.Client/IssuerDirectoryClient.cs`
+- MAINT: CacheKey concatenates raw segments with `|` without escaping; tenant/issuer values containing `|` can collide. `src/__Libraries/StellaOps.IssuerDirectory.Client/IssuerDirectoryClient.cs`
+- MAINT: Cache TTL options are not validated (zero/negative values accepted) and validation failures are swallowed without context in options registration. `src/__Libraries/StellaOps.IssuerDirectory.Client/IssuerDirectoryClientOptions.cs`, `src/__Libraries/StellaOps.IssuerDirectory.Client/ServiceCollectionExtensions.cs`
+- TEST: No unit tests for options validation, header injection, cache behavior, or HTTP failure handling.
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; normalize issuerId in Set/Delete; escape cache key segments; validate cache TTLs and surface validation errors; add unit tests with stubbed HttpMessageHandler and MemoryCache.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core/StellaOps.IssuerDirectory.Core.csproj
+- MAINT: TreatWarningsAsErrors is disabled, reducing warning discipline in a core library. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core/StellaOps.IssuerDirectory.Core.csproj`
+- MAINT: CreateAsync uses repository Upsert without an existence check; “create” can overwrite existing issuers without a clear conflict path. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core/Services/IssuerDirectoryService.cs`
+- MAINT: Key IDs are generated with Guid.NewGuid in service methods; tests and replay tooling cannot pin deterministic IDs without additional indirection. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core/Services/IssuerKeyService.cs`
+- MAINT: Seed refresh updates existing system seeds without writing audit entries or metrics, which can violate auditability expectations. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core/Services/IssuerDirectoryService.cs`
+- TEST: No unit tests in this project; coverage depends on Core.Tests (not assessed here) for validator, service, and audit behaviors.
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; add create conflict checks or rename to Upsert semantics; inject a key ID generator for determinism; add audit/metrics for seed refresh; add unit tests covering validator error paths, rotate/revoke flows, and audit metadata.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
+### src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/StellaOps.IssuerDirectory.Core.Tests.csproj
+- MAINT: Test project lacks Microsoft.NET.Test.Sdk and xUnit packages; discovery/running may rely on transitive TestKit behavior and is brittle. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/StellaOps.IssuerDirectory.Core.Tests.csproj`
+- MAINT: IssuerDirectoryClient tests live in Core.Tests and use reflection to instantiate internal client types, which couples tests to internal implementation details. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/IssuerDirectoryClientTests.cs`
+- MAINT: Some fake repositories throw NotImplementedException for list methods; untested paths can mask regressions when list endpoints are exercised. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/Services/IssuerKeyServiceTests.cs`, `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/Services/IssuerTrustServiceTests.cs`
+- TEST: Coverage exists for create/update/delete flows, key add/rotate/revoke, trust set/get/delete, and client header/cache behavior. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/Services/IssuerDirectoryServiceTests.cs`, `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/Services/IssuerKeyServiceTests.cs`, `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/Services/IssuerTrustServiceTests.cs`, `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/IssuerDirectoryClientTests.cs`
+- TEST: Missing coverage for list ordering/dedup, key validation failure paths (invalid material/expired keys), and audit metadata contents for seed refresh. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/Services/IssuerDirectoryServiceTests.cs`, `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Core.Tests/Services/IssuerKeyServiceTests.cs`
+- Proposed changes (optional): add test SDK/xUnit packages explicitly; move client tests to the client test project and expose internals via InternalsVisibleTo or a factory; implement fake list methods or add coverage for list semantics; add validator and audit metadata tests.
+- Disposition: waived (test project; no apply changes).
+### src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/StellaOps.IssuerDirectory.Infrastructure.csproj
+- MAINT: TreatWarningsAsErrors is disabled, reducing warning discipline in a core infrastructure library. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/StellaOps.IssuerDirectory.Infrastructure.csproj`
+- MAINT: InMemory key/trust repositories build cache keys as `${tenant}|${issuer}` without escaping; tenant/issuer values containing `|` can collide. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/InMemory/InMemoryIssuerKeyRepository.cs`, `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/InMemory/InMemoryIssuerTrustRepository.cs`
+- MAINT: InMemoryIssuerAuditSink discards entries silently once MaxEntries is exceeded; no metrics or visibility when truncation occurs. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/InMemory/InMemoryIssuerAuditSink.cs`
+- MAINT: Seed loader accepts data without validating required URL fields beyond Uri construction; bad inputs surface as UriFormatException without field context. `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/Seed/CsafPublisherSeedLoader.cs`
+- TEST: No test project for Infrastructure; seed loader and in-memory repositories lack coverage for ordering, collision, and parsing failures.
+- Proposed changes (pending approval): enable TreatWarningsAsErrors; escape key segments in in-memory stores; emit a counter or log when audit entries are dropped; add explicit validation errors for seed fields; add tests for seed parsing and in-memory ordering/collisions.
+- Disposition: pending implementation (non-test project; apply recommendations remain open).
## Notes
- Example projects waived at requester direction; APPLY tasks closed with no changes.
- APPLY tasks remain pending approval of proposed changes for non-example projects.
@@ -3060,3 +3574,8 @@
+
+
+
+
+
diff --git a/docs/implplan/SPRINT_20251230_001_BE_backport_resolver_tiered_evidence.md b/docs/implplan/SPRINT_20251230_001_BE_backport_resolver_tiered_evidence.md
index 8e58456cb..8aeafd8d3 100644
--- a/docs/implplan/SPRINT_20251230_001_BE_backport_resolver_tiered_evidence.md
+++ b/docs/implplan/SPRINT_20251230_001_BE_backport_resolver_tiered_evidence.md
@@ -63,70 +63,70 @@ var isPatched = string.Compare(package.InstalledVersion, fixedVersion, StringCom
| Task ID | Description | Status | Assignee | Notes |
|---------|-------------|--------|----------|-------|
-| BP-101 | Create IVersionComparatorFactory interface | TODO | | DI registration |
-| BP-102 | Wire comparators into BackportStatusService | TODO | | RPM, DEB, APK |
-| BP-103 | Update EvaluateBoundaryRules with proof lines | TODO | | Audit trail |
-| BP-104 | Unit tests for version comparison edge cases | TODO | | Golden datasets |
-| BP-105 | Integration test: epoch handling | TODO | | `1:2.0` vs `3.0` |
+| BP-101 | Create IVersionComparatorFactory interface | DONE | Agent | Created IVersionComparatorFactory.cs |
+| BP-102 | Wire comparators into BackportStatusService | DONE | Agent | RPM, DEB, APK via factory |
+| BP-103 | Update EvaluateBoundaryRules with proof lines | DONE | Agent | Audit trail in BackportVerdict |
+| BP-104 | Unit tests for version comparison edge cases | DONE | Agent | BackportStatusServiceVersionComparerTests.cs |
+| BP-105 | Integration test: epoch handling | DONE | Agent | Theory tests with epoch cases |
### Phase 2: RangeRule Implementation (P0)
| Task ID | Description | Status | Assignee | Notes |
|---------|-------------|--------|----------|-------|
-| BP-201 | Implement EvaluateRangeRules with comparators | TODO | | Min/max bounds |
-| BP-202 | Handle inclusive/exclusive boundaries | TODO | | `[` vs `(` |
-| BP-203 | Add Low confidence for NVD-sourced ranges | TODO | | Tier 5 |
-| BP-204 | Unit tests for range edge cases | TODO | | Open/closed |
-| BP-205 | Integration test: NVD fallback path | TODO | | E2E flow |
+| BP-201 | Implement EvaluateRangeRules with comparators | DONE | Agent | Full range logic |
+| BP-202 | Handle inclusive/exclusive boundaries | DONE | Agent | Both `[` and `(` supported |
+| BP-203 | Add Low confidence for NVD-sourced ranges | DONE | Agent | Tier 5 returns Low |
+| BP-204 | Unit tests for range edge cases | DONE | Agent | Open/closed boundary tests |
+| BP-205 | Integration test: NVD fallback path | DONE | Agent | NvdFallbackIntegrationTests.cs |
### Phase 3: Derivative Distro Mapping (P1)
| Task ID | Description | Status | Assignee | Notes |
|---------|-------------|--------|----------|-------|
-| BP-301 | Create DistroDerivativeMapping model | TODO | | Canonical/derivative |
-| BP-302 | Add RHEL ↔ Alma/Rocky/CentOS mappings | TODO | | Major release |
-| BP-303 | Add Ubuntu ↔ LinuxMint mappings | TODO | | |
-| BP-304 | Add Debian ↔ Ubuntu mappings | TODO | | |
-| BP-305 | Integrate into rule fetching with confidence penalty | TODO | | 0.95x multiplier |
-| BP-306 | Unit tests for derivative lookup | TODO | | |
-| BP-307 | Integration test: cross-distro OVAL | TODO | | RHEL→Rocky |
+| BP-301 | Create DistroDerivativeMapping model | DONE | Agent | StellaOps.DistroIntel library |
+| BP-302 | Add RHEL ↔ Alma/Rocky/CentOS mappings | DONE | Agent | Major releases 7-10 |
+| BP-303 | Add Ubuntu ↔ LinuxMint mappings | DONE | Agent | Mint, Pop!_OS |
+| BP-304 | Add Debian ↔ Ubuntu mappings | DONE | Agent | Bullseye, Bookworm |
+| BP-305 | Integrate into rule fetching with confidence penalty | DONE | Agent | 0.95x/0.80x multipliers |
+| BP-306 | Unit tests for derivative lookup | DONE | Agent | DistroMappingsTests.cs |
+| BP-307 | Integration test: cross-distro OVAL | DONE | Agent | CrossDistroOvalIntegrationTests.cs |
### Phase 4: Bug ID → CVE Mapping (P1)
| Task ID | Description | Status | Assignee | Notes |
|---------|-------------|--------|----------|-------|
-| BP-401 | Add Debian bug regex extraction | TODO | | `Closes: #123` |
-| BP-402 | Add RHBZ bug regex extraction | TODO | | `RHBZ#123` |
-| BP-403 | Add Launchpad bug regex extraction | TODO | | `LP: #123` |
-| BP-404 | Create IBugCveMappingService interface | TODO | | Async lookup |
-| BP-405 | Implement DebianSecurityTrackerClient | TODO | | API client |
-| BP-406 | Implement RedHatErrataClient | TODO | | API client |
-| BP-407 | Cache layer for bug→CVE mappings | TODO | | 24h TTL |
-| BP-408 | Unit tests for bug ID extraction | TODO | | Regex patterns |
-| BP-409 | Integration test: Debian tracker lookup | TODO | | Live API |
+| BP-401 | Add Debian bug regex extraction | DONE | Agent | `Closes: #123456` pattern |
+| BP-402 | Add RHBZ bug regex extraction | DONE | Agent | `RHBZ#123456` pattern |
+| BP-403 | Add Launchpad bug regex extraction | DONE | Agent | `LP: #123456` pattern |
+| BP-404 | Create IBugCveMappingService interface | DONE | Agent | Async lookup interface |
+| BP-405 | Implement DebianSecurityTrackerClient | DONE | Agent | API client with caching |
+| BP-406 | Implement RedHatErrataClient | DONE | Agent | Security API + Bugzilla fallback |
+| BP-407 | Cache layer for bug→CVE mappings | DONE | Agent | BugCveMappingRouter with 24h TTL |
+| BP-408 | Unit tests for bug ID extraction | DONE | Agent | 34 tests in BugIdExtractionTests.cs |
+| BP-409 | Integration test: Debian tracker lookup | DONE | Agent | BugCveMappingIntegrationTests.cs |
### Phase 5: Affected Functions Extraction (P2)
| Task ID | Description | Status | Assignee | Notes |
|---------|-------------|--------|----------|-------|
-| BP-501 | Create function signature regex patterns | TODO | | C, Go, Python |
-| BP-502 | Implement ExtractFunctionsFromContext | TODO | | In HunkSigExtractor |
-| BP-503 | Add C/C++ function pattern | TODO | | `void foo(` |
-| BP-504 | Add Go function pattern | TODO | | `func (r *R) M(` |
-| BP-505 | Add Python function pattern | TODO | | `def foo(` |
-| BP-506 | Add Rust function pattern | TODO | | `fn foo(` |
-| BP-507 | Unit tests for function extraction | TODO | | Multi-language |
-| BP-508 | Enable fuzzy function matching in Tier 3/4 | TODO | | Similarity score |
+| BP-501 | Create function signature regex patterns | DONE | Agent | C, Go, Python, Rust, Java, JS |
+| BP-502 | Implement ExtractFunctionsFromContext | DONE | Agent | FunctionSignatureExtractor.cs |
+| BP-503 | Add C/C++ function pattern | DONE | Agent | GeneratedRegex with modifiers |
+| BP-504 | Add Go function pattern | DONE | Agent | `func (r *R) M(` with returns |
+| BP-505 | Add Python function pattern | DONE | Agent | `def foo(` + async |
+| BP-506 | Add Rust function pattern | DONE | Agent | `fn foo(` + pub/async/unsafe |
+| BP-507 | Unit tests for function extraction | DONE | Agent | 47 tests all pass |
+| BP-508 | Enable fuzzy function matching in Tier 3/4 | DONE | Agent | Levenshtein similarity, 58 tests pass |
### Phase 6: Confidence Tier Alignment (P2)
| Task ID | Description | Status | Assignee | Notes |
|---------|-------------|--------|----------|-------|
-| BP-601 | Expand RulePriority enum | TODO | | 9 levels |
-| BP-602 | Update BackportStatusService priority logic | TODO | | Tier ordering |
-| BP-603 | Add confidence multipliers per tier | TODO | | |
-| BP-604 | Update EvidencePointer with TierSource | TODO | | Audit |
-| BP-605 | Unit tests for tier precedence | TODO | | |
+| BP-601 | Expand RulePriority enum | DONE | Agent | 9 levels from Tier 1-5 |
+| BP-602 | Update BackportStatusService priority logic | DONE | Agent | Tier ordering |
+| BP-603 | Add confidence multipliers per tier | DONE | Agent | In DistroMappings |
+| BP-604 | Update EvidencePointer with TierSource | DONE | Agent | Added EvidenceTier enum |
+| BP-605 | Unit tests for tier precedence | DONE | Agent | 34 tests in TierPrecedenceTests.cs |
---
@@ -154,21 +154,21 @@ var isPatched = string.Compare(package.InstalledVersion, fixedVersion, StringCom
### P0 Tasks (Must complete)
-- [ ] `BackportStatusService` uses proper version comparators for all ecosystems
-- [ ] `RangeRule` evaluation returns correct verdicts with Low confidence
-- [ ] All existing tests pass
-- [ ] New golden tests for version edge cases
+- [x] `BackportStatusService` uses proper version comparators for all ecosystems
+- [x] `RangeRule` evaluation returns correct verdicts with Low confidence
+- [x] All existing tests pass
+- [x] New golden tests for version edge cases
### P1 Tasks (Should complete)
-- [ ] Derivative distro mapping works for RHEL family
-- [ ] Bug ID extraction finds Debian/RHBZ/LP references
-- [ ] Bug→CVE mapping lookup is cached
+- [x] Derivative distro mapping works for RHEL family
+- [x] Bug ID extraction finds Debian/RHBZ/LP references
+- [x] Bug→CVE mapping lookup is cached
### P2 Tasks (Nice to have)
-- [ ] Function extraction works for C, Go, Python, Rust
-- [ ] Confidence tiers aligned to five-tier hierarchy
+- [x] Function extraction works for C, Go, Python, Rust, Java, JavaScript
+- [x] Confidence tiers aligned to five-tier hierarchy (RulePriority enum expanded)
---
@@ -211,6 +211,22 @@ Location: `src/__Tests/__Datasets/backport-resolver/`
| Date | Event | Details |
|------|-------|---------|
| 2025-12-30 | Sprint created | Initial planning and gap analysis |
+| 2026-01-02 | Phase 1 completed | Created IVersionComparatorFactory, wired RPM/Deb/APK comparators into BackportStatusService, added proof lines |
+| 2026-01-02 | Phase 2 completed | Implemented EvaluateRangeRules with inclusive/exclusive boundaries, Low confidence for Tier 5 |
+| 2026-01-02 | Phase 3 partial | Created StellaOps.DistroIntel library with DistroMappings for RHEL/Ubuntu/Debian families |
+| 2026-01-02 | Phase 6 partial | Expanded RulePriority enum to 9-level 5-tier hierarchy |
+| 2026-01-02 | Tests added | BackportStatusServiceVersionComparerTests.cs, DistroMappingsTests.cs |
+| 2026-01-02 | Tests verified | All 61 BackportProof tests pass; P0 Acceptance Criteria complete |
+| 2026-01-02 | Phase 4 completed | Bug ID extraction (Debian/RHBZ/LP), IBugCveMappingService, API clients, BugCveMappingRouter with caching |
+| 2026-01-02 | Phase 5 completed | FunctionSignatureExtractor.cs with C/Go/Python/Rust/Java/JS patterns; 47 tests pass |
+| 2026-01-02 | BP-508 completed | Fuzzy function matching with Levenshtein similarity; FunctionMatchingExtensions class; 58 tests pass |
+| 2026-01-02 | BP-604 completed | Extended EvidencePointer with TierSource; added EvidenceTier enum |
+| 2026-01-02 | BP-605 completed | TierPrecedenceTests.cs with 34 tests for tier ordering |
+| 2026-01-02 | Phase 6 completed | All confidence tier alignment tasks done |
+| 2026-01-02 | BP-205 completed | NvdFallbackIntegrationTests.cs: E2E tests for NVD range fallback path |
+| 2026-01-02 | BP-307 completed | CrossDistroOvalIntegrationTests.cs: E2E tests for derivative distro mapping |
+| 2026-01-02 | BP-409 completed | BugCveMappingIntegrationTests.cs: E2E tests for bug ID to CVE mapping |
+| 2026-01-02 | Sprint complete | All 6 phases done; 125 BackportProof tests pass |
---
diff --git a/docs/implplan/MIGRATION_CONSOLIDATION_20251229.md b/docs/implplan/archived/MIGRATION_CONSOLIDATION_20251229.md
similarity index 100%
rename from docs/implplan/MIGRATION_CONSOLIDATION_20251229.md
rename to docs/implplan/archived/MIGRATION_CONSOLIDATION_20251229.md
diff --git a/src/AirGap/StellaOps.AirGap.Importer/Reconciliation/Signing/EvidenceGraphDsseSigner.cs b/src/AirGap/StellaOps.AirGap.Importer/Reconciliation/Signing/EvidenceGraphDsseSigner.cs
index 9ed86fc57..abdb28b8d 100644
--- a/src/AirGap/StellaOps.AirGap.Importer/Reconciliation/Signing/EvidenceGraphDsseSigner.cs
+++ b/src/AirGap/StellaOps.AirGap.Importer/Reconciliation/Signing/EvidenceGraphDsseSigner.cs
@@ -6,9 +6,9 @@ using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Asn1.X9;
using StellaOps.Cryptography;
-using StellaOps.AirGap.Importer.Validation;
using AttestorDsseEnvelope = StellaOps.Attestor.Envelope.DsseEnvelope;
using AttestorDsseSignature = StellaOps.Attestor.Envelope.DsseSignature;
+using AttestorDssePreAuthenticationEncoding = StellaOps.Attestor.Envelope.DssePreAuthenticationEncoding;
using StellaOps.Attestor.Envelope;
namespace StellaOps.AirGap.Importer.Reconciliation.Signing;
@@ -43,7 +43,7 @@ internal sealed class EvidenceGraphDsseSigner
var canonicalJson = serializer.Serialize(graph, pretty: false);
var payloadBytes = Encoding.UTF8.GetBytes(canonicalJson);
- var pae = DssePreAuthenticationEncoding.Encode(EvidenceGraphPayloadType, payloadBytes);
+ var pae = AttestorDssePreAuthenticationEncoding.Compute(EvidenceGraphPayloadType, payloadBytes);
var envelopeKey = LoadEcdsaEnvelopeKey(signingPrivateKeyPemPath, signingKeyId);
var signature = SignDeterministicEcdsa(pae, signingPrivateKeyPemPath, envelopeKey.AlgorithmId);
diff --git a/src/AirGap/StellaOps.AirGap.Importer/TASKS.md b/src/AirGap/StellaOps.AirGap.Importer/TASKS.md
index 0a0f136d3..0288d3e57 100644
--- a/src/AirGap/StellaOps.AirGap.Importer/TASKS.md
+++ b/src/AirGap/StellaOps.AirGap.Importer/TASKS.md
@@ -8,3 +8,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| AUDIT-0026-M | DONE | Maintainability audit for StellaOps.AirGap.Importer. |
| AUDIT-0026-T | DONE | Test coverage audit for StellaOps.AirGap.Importer. |
| AUDIT-0026-A | DOING | Pending approval for changes. |
+| VAL-SMOKE-001 | DONE | Resolved DSSE signer ambiguity; smoke build now proceeds. |
diff --git a/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/DsseVerifierTests.cs b/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/DsseVerifierTests.cs
index 6b7bb7203..0eca7481f 100644
--- a/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/DsseVerifierTests.cs
+++ b/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/DsseVerifierTests.cs
@@ -53,19 +53,8 @@ public class DsseVerifierTests
private static byte[] BuildPae(string payloadType, string payload)
{
- var parts = new[] { "DSSEv1", payloadType, payload };
- var paeBuilder = new System.Text.StringBuilder();
- paeBuilder.Append("PAE:");
- paeBuilder.Append(parts.Length);
- foreach (var part in parts)
- {
- paeBuilder.Append(' ');
- paeBuilder.Append(part.Length);
- paeBuilder.Append(' ');
- paeBuilder.Append(part);
- }
-
- return System.Text.Encoding.UTF8.GetBytes(paeBuilder.ToString());
+ var payloadBytes = System.Text.Encoding.UTF8.GetBytes(payload);
+ return StellaOps.Attestor.Envelope.DssePreAuthenticationEncoding.Compute(payloadType, payloadBytes);
}
private static string Fingerprint(byte[] pub)
diff --git a/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/ImportValidatorTests.cs b/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/ImportValidatorTests.cs
index 6a3a27717..b11c9cafa 100644
--- a/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/ImportValidatorTests.cs
+++ b/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/ImportValidatorTests.cs
@@ -94,6 +94,9 @@ public sealed class ImportValidatorTests
quarantine,
NullLogger.Instance);
+ var payloadEntries = new List { new("a.txt", new MemoryStream("data"u8.ToArray())) };
+ var merkleRoot = new MerkleRootCalculator().ComputeRoot(payloadEntries);
+ var manifestJson = $"{{\"version\":\"1.0.0\",\"merkleRoot\":\"{merkleRoot}\"}}";
var tempRoot = Path.Combine(Path.GetTempPath(), "stellaops-airgap-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
var bundlePath = Path.Combine(tempRoot, "bundle.tar.zst");
@@ -106,7 +109,7 @@ public sealed class ImportValidatorTests
BundleType: "offline-kit",
BundleDigest: "sha256:bundle",
BundlePath: bundlePath,
- ManifestJson: "{\"version\":\"1.0.0\"}",
+ ManifestJson: manifestJson,
ManifestVersion: "1.0.0",
ManifestCreatedAt: DateTimeOffset.Parse("2025-12-15T00:00:00Z"),
ForceActivate: false,
@@ -116,7 +119,7 @@ public sealed class ImportValidatorTests
RootJson: root,
SnapshotJson: snapshot,
TimestampJson: timestamp,
- PayloadEntries: new List { new("a.txt", new MemoryStream("data"u8.ToArray())) },
+ PayloadEntries: payloadEntries,
TrustStore: trustStore,
ApproverIds: new[] { "approver-1", "approver-2" });
@@ -146,19 +149,8 @@ public sealed class ImportValidatorTests
private static byte[] BuildPae(string payloadType, string payload)
{
- var parts = new[] { "DSSEv1", payloadType, payload };
- var paeBuilder = new System.Text.StringBuilder();
- paeBuilder.Append("PAE:");
- paeBuilder.Append(parts.Length);
- foreach (var part in parts)
- {
- paeBuilder.Append(' ');
- paeBuilder.Append(part.Length);
- paeBuilder.Append(' ');
- paeBuilder.Append(part);
- }
-
- return System.Text.Encoding.UTF8.GetBytes(paeBuilder.ToString());
+ var payloadBytes = System.Text.Encoding.UTF8.GetBytes(payload);
+ return StellaOps.Attestor.Envelope.DssePreAuthenticationEncoding.Compute(payloadType, payloadBytes);
}
private static string Fingerprint(byte[] pub) => Convert.ToHexString(SHA256.HashData(pub)).ToLowerInvariant();
diff --git a/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/TASKS.md b/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/TASKS.md
index 30568ffa5..eeda39210 100644
--- a/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/TASKS.md
+++ b/src/AirGap/__Tests/StellaOps.AirGap.Importer.Tests/TASKS.md
@@ -8,3 +8,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| AUDIT-0027-M | DONE | Maintainability audit for StellaOps.AirGap.Importer.Tests. |
| AUDIT-0027-T | DONE | Test coverage audit for StellaOps.AirGap.Importer.Tests. |
| AUDIT-0027-A | TODO | Pending approval for changes. |
+| VAL-SMOKE-001 | DONE | Align DSSE PAE test data and manifest merkle root; unit tests pass. |
diff --git a/src/Attestor/StellaOps.Attestor.Envelope/DsseDetachedPayloadReference.cs b/src/Attestor/StellaOps.Attestor.Envelope/DsseDetachedPayloadReference.cs
index 196f48223..1132a11b6 100644
--- a/src/Attestor/StellaOps.Attestor.Envelope/DsseDetachedPayloadReference.cs
+++ b/src/Attestor/StellaOps.Attestor.Envelope/DsseDetachedPayloadReference.cs
@@ -16,6 +16,11 @@ public sealed record DsseDetachedPayloadReference
throw new ArgumentException("Detached payload digest must be provided.", nameof(sha256));
}
+ if (!IsSha256Digest(sha256))
+ {
+ throw new ArgumentException("Detached payload digest must be a 64-character hex SHA256 value.", nameof(sha256));
+ }
+
Uri = uri;
Sha256 = sha256.ToLowerInvariant();
Length = length;
@@ -29,4 +34,27 @@ public sealed record DsseDetachedPayloadReference
public long? Length { get; }
public string? MediaType { get; }
+
+ private static bool IsSha256Digest(string value)
+ {
+ if (value.Length != 64)
+ {
+ return false;
+ }
+
+ foreach (var ch in value)
+ {
+ if (!IsHex(ch))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool IsHex(char ch)
+ => (ch >= '0' && ch <= '9')
+ || (ch >= 'a' && ch <= 'f')
+ || (ch >= 'A' && ch <= 'F');
}
diff --git a/src/Attestor/StellaOps.Attestor.Envelope/DsseEnvelopeSerializer.cs b/src/Attestor/StellaOps.Attestor.Envelope/DsseEnvelopeSerializer.cs
index 551507e5b..200266474 100644
--- a/src/Attestor/StellaOps.Attestor.Envelope/DsseEnvelopeSerializer.cs
+++ b/src/Attestor/StellaOps.Attestor.Envelope/DsseEnvelopeSerializer.cs
@@ -1,8 +1,6 @@
using System;
using System.Buffers;
using System.Collections.Generic;
-using System.IO;
-using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
@@ -18,10 +16,25 @@ public static class DsseEnvelopeSerializer
options ??= new DsseEnvelopeSerializationOptions();
+ if (!options.EmitCompactJson && !options.EmitExpandedJson)
+ {
+ throw new InvalidOperationException("At least one JSON format must be emitted.");
+ }
+
+ if (options.CompressionAlgorithm != DsseCompressionAlgorithm.None)
+ {
+ throw new NotSupportedException("Payload compression is not supported during serialization. Compress the payload before envelope creation and ensure payloadType/metadata reflect the compressed bytes.");
+ }
+
var originalPayload = envelope.Payload.ToArray();
- var processedPayload = ApplyCompression(originalPayload, options.CompressionAlgorithm);
var payloadSha256 = Convert.ToHexString(SHA256.HashData(originalPayload)).ToLowerInvariant();
- var payloadBase64 = Convert.ToBase64String(processedPayload);
+ var payloadBase64 = Convert.ToBase64String(originalPayload);
+
+ if (envelope.DetachedPayload is not null
+ && !string.Equals(payloadSha256, envelope.DetachedPayload.Sha256, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException("Detached payload digest does not match the envelope payload.");
+ }
byte[]? compactJson = null;
if (options.EmitCompactJson)
@@ -37,7 +50,7 @@ public static class DsseEnvelopeSerializer
payloadBase64,
payloadSha256,
originalPayload.Length,
- processedPayload.Length,
+ originalPayload.Length,
options,
originalPayload);
}
@@ -47,7 +60,7 @@ public static class DsseEnvelopeSerializer
expandedJson,
payloadSha256,
originalPayload.Length,
- processedPayload.Length,
+ originalPayload.Length, // No compression, so processed == original
options.CompressionAlgorithm,
envelope.DetachedPayload);
}
@@ -227,33 +240,6 @@ public static class DsseEnvelopeSerializer
}
}
- private static byte[] ApplyCompression(byte[] payload, DsseCompressionAlgorithm algorithm)
- {
- return algorithm switch
- {
- DsseCompressionAlgorithm.None => payload,
- DsseCompressionAlgorithm.Gzip => CompressWithStream(payload, static (stream) => new GZipStream(stream, CompressionLevel.SmallestSize, leaveOpen: true)),
- DsseCompressionAlgorithm.Brotli => CompressWithStream(payload, static (stream) => new BrotliStream(stream, CompressionLevel.SmallestSize, leaveOpen: true)),
- _ => throw new NotSupportedException($"Compression algorithm '{algorithm}' is not supported.")
- };
- }
-
- private static byte[] CompressWithStream(byte[] payload, Func streamFactory)
- {
- if (payload.Length == 0)
- {
- return Array.Empty();
- }
-
- using var output = new MemoryStream();
- using (var compressionStream = streamFactory(output))
- {
- compressionStream.Write(payload);
- }
-
- return output.ToArray();
- }
-
private static string GetCompressionName(DsseCompressionAlgorithm algorithm)
{
return algorithm switch
diff --git a/src/Attestor/StellaOps.Attestor.Envelope/DssePreAuthenticationEncoding.cs b/src/Attestor/StellaOps.Attestor.Envelope/DssePreAuthenticationEncoding.cs
new file mode 100644
index 000000000..e88a60cb1
--- /dev/null
+++ b/src/Attestor/StellaOps.Attestor.Envelope/DssePreAuthenticationEncoding.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Buffers;
+using System.Globalization;
+using System.Text;
+
+namespace StellaOps.Attestor.Envelope;
+
+///
+/// Computes DSSE pre-authentication encoding (PAE) for payload signing.
+///
+public static class DssePreAuthenticationEncoding
+{
+ private static readonly byte[] Prefix = Encoding.ASCII.GetBytes("DSSEv1");
+ private static readonly byte[] Space = new byte[] { (byte)' ' };
+
+ public static byte[] Compute(string payloadType, ReadOnlySpan payload)
+ {
+ if (payloadType is null)
+ {
+ throw new ArgumentNullException(nameof(payloadType));
+ }
+
+ var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadType);
+ var payloadTypeLength = Encoding.ASCII.GetBytes(payloadTypeBytes.Length.ToString(CultureInfo.InvariantCulture));
+ var payloadLength = Encoding.ASCII.GetBytes(payload.Length.ToString(CultureInfo.InvariantCulture));
+
+ var buffer = new ArrayBufferWriter();
+ Write(buffer, Prefix);
+ Write(buffer, Space);
+ Write(buffer, payloadTypeLength);
+ Write(buffer, Space);
+ Write(buffer, payloadTypeBytes);
+ Write(buffer, Space);
+ Write(buffer, payloadLength);
+ Write(buffer, Space);
+ Write(buffer, payload);
+
+ return buffer.WrittenSpan.ToArray();
+ }
+
+ private static void Write(ArrayBufferWriter writer, ReadOnlySpan bytes)
+ {
+ var span = writer.GetSpan(bytes.Length);
+ bytes.CopyTo(span);
+ writer.Advance(bytes.Length);
+ }
+}
diff --git a/src/Attestor/StellaOps.Attestor.Envelope/DsseSignature.cs b/src/Attestor/StellaOps.Attestor.Envelope/DsseSignature.cs
index 0a57bd87d..7108eaa05 100644
--- a/src/Attestor/StellaOps.Attestor.Envelope/DsseSignature.cs
+++ b/src/Attestor/StellaOps.Attestor.Envelope/DsseSignature.cs
@@ -11,6 +11,8 @@ public sealed record DsseSignature
throw new ArgumentException("Signature must be provided.", nameof(signature));
}
+ ValidateBase64(signature);
+
Signature = signature;
KeyId = keyId;
}
@@ -28,4 +30,19 @@ public sealed record DsseSignature
return new DsseSignature(Convert.ToBase64String(signature), keyId);
}
+
+ private static void ValidateBase64(string signature)
+ {
+ try
+ {
+ if (Convert.FromBase64String(signature).Length == 0)
+ {
+ throw new ArgumentException("Signature must not decode to an empty byte array.", nameof(signature));
+ }
+ }
+ catch (FormatException ex)
+ {
+ throw new ArgumentException("Signature must be valid base64.", nameof(signature), ex);
+ }
+ }
}
diff --git a/src/Attestor/StellaOps.Attestor.Envelope/EnvelopeSignatureService.cs b/src/Attestor/StellaOps.Attestor.Envelope/EnvelopeSignatureService.cs
index 5cc92a5f0..fa56b13f0 100644
--- a/src/Attestor/StellaOps.Attestor.Envelope/EnvelopeSignatureService.cs
+++ b/src/Attestor/StellaOps.Attestor.Envelope/EnvelopeSignatureService.cs
@@ -30,6 +30,19 @@ public sealed class EnvelopeSignatureService
};
}
+ public EnvelopeResult SignDsse(string payloadType, ReadOnlySpan payload, EnvelopeKey key, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(payloadType))
+ {
+ throw new ArgumentException("payloadType must be provided.", nameof(payloadType));
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var pae = DssePreAuthenticationEncoding.Compute(payloadType, payload);
+ return Sign(pae, key, cancellationToken);
+ }
+
public EnvelopeResult Verify(ReadOnlySpan payload, EnvelopeSignature signature, EnvelopeKey key, CancellationToken cancellationToken = default)
{
if (signature is null)
@@ -67,6 +80,19 @@ public sealed class EnvelopeSignatureService
};
}
+ public EnvelopeResult VerifyDsse(string payloadType, ReadOnlySpan payload, EnvelopeSignature signature, EnvelopeKey key, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(payloadType))
+ {
+ throw new ArgumentException("payloadType must be provided.", nameof(payloadType));
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var pae = DssePreAuthenticationEncoding.Compute(payloadType, payload);
+ return Verify(pae, signature, key, cancellationToken);
+ }
+
private static EnvelopeResult SignEd25519(ReadOnlySpan payload, EnvelopeKey key)
{
if (!key.HasPrivateMaterial)
diff --git a/src/Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj b/src/Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj
index 739e23e7d..2fdbe387f 100644
--- a/src/Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj
+++ b/src/Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj
@@ -5,7 +5,7 @@
preview
enable
enable
- false
+ true
diff --git a/src/Attestor/StellaOps.Attestor.Envelope/TASKS.md b/src/Attestor/StellaOps.Attestor.Envelope/TASKS.md
index c91c4015c..1d8d5f5db 100644
--- a/src/Attestor/StellaOps.Attestor.Envelope/TASKS.md
+++ b/src/Attestor/StellaOps.Attestor.Envelope/TASKS.md
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0051-M | DONE | Maintainability audit for StellaOps.Attestor.Envelope. |
| AUDIT-0051-T | DONE | Test coverage audit for StellaOps.Attestor.Envelope. |
-| AUDIT-0051-A | TODO | Pending approval for changes. |
+| AUDIT-0051-A | DONE | Applied audit remediation for envelope signing/serialization. |
diff --git a/src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/DsseEnvelopeSerializerTests.cs b/src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/DsseEnvelopeSerializerTests.cs
index 02281e3e0..59a690b62 100644
--- a/src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/DsseEnvelopeSerializerTests.cs
+++ b/src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/DsseEnvelopeSerializerTests.cs
@@ -1,13 +1,9 @@
using System;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
using System.Text;
using System.Text.Json;
using StellaOps.Attestor.Envelope;
using Xunit;
-
using StellaOps.TestKit;
namespace StellaOps.Attestor.Envelope.Tests;
@@ -50,8 +46,8 @@ public sealed class DsseEnvelopeSerializerTests
}
[Trait("Category", TestCategories.Unit)]
- [Fact]
- public void Serialize_WithCompressionEnabled_EmbedsCompressedPayloadMetadata()
+ [Fact]
+ public void Serialize_WithCompressionEnabled_Throws()
{
var payload = Encoding.UTF8.GetBytes("{\"foo\":\"bar\",\"count\":1}");
var envelope = new DsseEnvelope(
@@ -65,30 +61,7 @@ public sealed class DsseEnvelopeSerializerTests
CompressionAlgorithm = DsseCompressionAlgorithm.Gzip
};
- var result = DsseEnvelopeSerializer.Serialize(envelope, options);
-
- Assert.NotNull(result.CompactJson);
- var compactDoc = JsonDocument.Parse(result.CompactJson!);
- var payloadBase64 = compactDoc.RootElement.GetProperty("payload").GetString();
- Assert.False(string.IsNullOrEmpty(payloadBase64));
-
- var compressedBytes = Convert.FromBase64String(payloadBase64!);
- using var compressedStream = new MemoryStream(compressedBytes);
- using var gzip = new GZipStream(compressedStream, CompressionMode.Decompress);
- using var decompressed = new MemoryStream();
- gzip.CopyTo(decompressed);
- Assert.True(payload.SequenceEqual(decompressed.ToArray()));
-
- using var expanded = JsonDocument.Parse(result.ExpandedJson!);
- var info = expanded.RootElement.GetProperty("payloadInfo");
- Assert.Equal(payload.Length, info.GetProperty("length").GetInt32());
- var compression = info.GetProperty("compression");
- Assert.Equal("gzip", compression.GetProperty("algorithm").GetString());
- Assert.Equal(compressedBytes.Length, compression.GetProperty("compressedLength").GetInt32());
-
- Assert.Equal(DsseCompressionAlgorithm.Gzip, result.Compression);
- Assert.Equal(payload.Length, result.OriginalPayloadLength);
- Assert.Equal(compressedBytes.Length, result.EmbeddedPayloadLength);
+ Assert.Throws(() => DsseEnvelopeSerializer.Serialize(envelope, options));
}
[Trait("Category", TestCategories.Unit)]
@@ -96,9 +69,10 @@ public sealed class DsseEnvelopeSerializerTests
public void Serialize_WithDetachedReference_WritesMetadata()
{
var payload = Encoding.UTF8.GetBytes("detached payload preview");
+ var payloadSha256 = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(payload)).ToLowerInvariant();
var reference = new DsseDetachedPayloadReference(
"https://evidence.example.com/sbom.json",
- "abc123",
+ payloadSha256,
payload.Length,
"application/json");
@@ -123,7 +97,28 @@ public sealed class DsseEnvelopeSerializerTests
}
[Trait("Category", TestCategories.Unit)]
- [Fact]
+ [Fact]
+ public void Serialize_WithDetachedReferenceMismatch_Throws()
+ {
+ var payload = Encoding.UTF8.GetBytes("detached payload preview");
+ var reference = new DsseDetachedPayloadReference(
+ "https://evidence.example.com/sbom.json",
+ new string('a', 64),
+ payload.Length,
+ "application/json");
+
+ var envelope = new DsseEnvelope(
+ "application/vnd.in-toto+json",
+ payload,
+ new[] { new DsseSignature("AQID") },
+ "text/plain",
+ reference);
+
+ Assert.Throws(() => DsseEnvelopeSerializer.Serialize(envelope));
+ }
+
+ [Trait("Category", TestCategories.Unit)]
+ [Fact]
public void Serialize_CompactOnly_SkipsExpandedPayload()
{
var payload = Encoding.UTF8.GetBytes("payload");
@@ -142,4 +137,23 @@ public sealed class DsseEnvelopeSerializerTests
Assert.NotNull(result.CompactJson);
Assert.Null(result.ExpandedJson);
}
+
+ [Trait("Category", TestCategories.Unit)]
+ [Fact]
+ public void Serialize_WithNoFormats_Throws()
+ {
+ var payload = Encoding.UTF8.GetBytes("payload");
+ var envelope = new DsseEnvelope(
+ "application/vnd.in-toto+json",
+ payload,
+ new[] { new DsseSignature("AQID") });
+
+ var options = new DsseEnvelopeSerializationOptions
+ {
+ EmitCompactJson = false,
+ EmitExpandedJson = false
+ };
+
+ Assert.Throws(() => DsseEnvelopeSerializer.Serialize(envelope, options));
+ }
}
diff --git a/src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/EnvelopeSignatureServiceTests.cs b/src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/EnvelopeSignatureServiceTests.cs
new file mode 100644
index 000000000..c639c3469
--- /dev/null
+++ b/src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/EnvelopeSignatureServiceTests.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+using StellaOps.Attestor.Envelope;
+using StellaOps.Cryptography;
+using StellaOps.TestKit;
+using Xunit;
+
+namespace StellaOps.Attestor.Envelope.Tests;
+
+public sealed class EnvelopeSignatureServiceTests
+{
+ [Trait("Category", TestCategories.Unit)]
+ [Fact]
+ public void DssePreAuthenticationEncoding_UsesAsciiLengths()
+ {
+ var payloadType = "application/vnd.in-toto+json";
+ var payload = Encoding.UTF8.GetBytes("hello");
+
+ var pae = DssePreAuthenticationEncoding.Compute(payloadType, payload);
+ var expected = $"DSSEv1 {Encoding.UTF8.GetByteCount(payloadType)} {payloadType} {payload.Length} hello";
+
+ Assert.Equal(expected, Encoding.UTF8.GetString(pae));
+ }
+
+ [Trait("Category", TestCategories.Unit)]
+ [Fact]
+ public void SignDsse_MatchesSignOnPreAuthenticationEncoding()
+ {
+ using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
+ var parameters = ecdsa.ExportParameters(includePrivateParameters: true);
+ var key = EnvelopeKey.CreateEcdsaSigner(SignatureAlgorithms.Es256, parameters, "test-key");
+ var service = new EnvelopeSignatureService();
+ var payloadType = "application/vnd.in-toto+json";
+ var payload = Encoding.UTF8.GetBytes("payload");
+
+ var pae = DssePreAuthenticationEncoding.Compute(payloadType, payload);
+ var direct = service.Sign(pae, key);
+ var viaDsse = service.SignDsse(payloadType, payload, key);
+
+ Assert.True(direct.IsSuccess);
+ Assert.True(viaDsse.IsSuccess);
+ Assert.Equal(direct.Value.KeyId, viaDsse.Value.KeyId);
+ Assert.Equal(direct.Value.AlgorithmId, viaDsse.Value.AlgorithmId);
+
+ var verifyDirect = service.Verify(pae, direct.Value, key);
+ var verify = service.VerifyDsse(payloadType, payload, viaDsse.Value, key);
+ Assert.True(verifyDirect.IsSuccess);
+ Assert.True(verify.IsSuccess);
+ }
+
+ [Trait("Category", TestCategories.Unit)]
+ [Fact]
+ public void DsseSignature_WithInvalidBase64_Throws()
+ {
+ Assert.Throws(() => new DsseSignature("not base64"));
+ }
+}
diff --git a/src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/TASKS.md b/src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/TASKS.md
index f0529de7f..d1db63069 100644
--- a/src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/TASKS.md
+++ b/src/Attestor/StellaOps.Attestor.Envelope/__Tests/StellaOps.Attestor.Envelope.Tests/TASKS.md
@@ -8,3 +8,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| AUDIT-0052-M | DONE | Maintainability audit for StellaOps.Attestor.Envelope.Tests. |
| AUDIT-0052-T | DONE | Test coverage audit for StellaOps.Attestor.Envelope.Tests. |
| AUDIT-0052-A | TODO | Pending approval for changes. |
+| VAL-SMOKE-001 | DONE | Stabilized DSSE signature tests under xUnit v3. |
diff --git a/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/Program.cs b/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/Program.cs
index 138dd695a..6fc63f4e9 100644
--- a/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/Program.cs
+++ b/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/Program.cs
@@ -3,7 +3,8 @@ using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
-var generator = new Generator();
+var options = GeneratorOptions.Parse(args);
+var generator = new Generator(options);
generator.Run();
internal sealed class Generator
@@ -14,15 +15,17 @@ internal sealed class Generator
private readonly string _schemaDir;
private readonly string _tsDir;
private readonly string _goDir;
+ private readonly bool _pruneStaleSchemas;
- public Generator()
+ public Generator(GeneratorOptions options)
{
_registry = TypeRegistry.Build();
- _repoRoot = ResolveRepoRoot();
+ _repoRoot = ResolveRepoRoot(options.RepoRoot);
_moduleRoot = Path.Combine(_repoRoot, "src", "Attestor", "StellaOps.Attestor.Types");
_schemaDir = Path.Combine(_moduleRoot, "schemas");
_tsDir = Path.Combine(_moduleRoot, "generated", "ts");
_goDir = Path.Combine(_moduleRoot, "generated", "go");
+ _pruneStaleSchemas = options.PruneStaleSchemas;
}
public void Run()
@@ -31,11 +34,13 @@ internal sealed class Generator
Directory.CreateDirectory(_tsDir);
Directory.CreateDirectory(_goDir);
+ var expectedSchemas = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var root in _registry.RootObjects)
{
var schema = SchemaBuilder.Build(root);
var schemaPath = Path.Combine(_schemaDir, $"{root.SchemaFileStem}.schema.json");
WriteUtf8File(schemaPath, schema);
+ expectedSchemas.Add(schemaPath);
}
var tsCode = TypeScriptEmitter.Emit(_registry);
@@ -43,17 +48,72 @@ internal sealed class Generator
var goCode = GoEmitter.Emit(_registry);
WriteUtf8File(Path.Combine(_goDir, "types.go"), goCode);
+
+ if (_pruneStaleSchemas)
+ {
+ PruneStaleSchemas(expectedSchemas);
+ }
}
- private static string ResolveRepoRoot()
+ private static string ResolveRepoRoot(string? overridePath)
{
- var current = new DirectoryInfo(AppContext.BaseDirectory);
- for (var i = 0; i < 8; i++)
+ if (!string.IsNullOrWhiteSpace(overridePath))
{
- current = current?.Parent ?? throw new InvalidOperationException("Unable to locate repository root.");
+ var normalized = Path.GetFullPath(overridePath);
+ if (!Directory.Exists(normalized))
+ {
+ throw new DirectoryNotFoundException($"Repository root override not found: {normalized}");
+ }
+
+ return normalized;
}
- return current!.FullName;
+ var fromCurrent = FindRepoRoot(Directory.GetCurrentDirectory());
+ if (fromCurrent is not null)
+ {
+ return fromCurrent;
+ }
+
+ var fromBase = FindRepoRoot(AppContext.BaseDirectory);
+ if (fromBase is not null)
+ {
+ return fromBase;
+ }
+
+ throw new InvalidOperationException("Unable to locate repository root.");
+ }
+
+ private static string? FindRepoRoot(string startPath)
+ {
+ var current = new DirectoryInfo(startPath);
+ while (current is not null)
+ {
+ var gitPath = Path.Combine(current.FullName, ".git");
+ if (Directory.Exists(gitPath))
+ {
+ return current.FullName;
+ }
+
+ current = current.Parent;
+ }
+
+ return null;
+ }
+
+ private void PruneStaleSchemas(HashSet expectedSchemas)
+ {
+ foreach (var file in Directory.EnumerateFiles(_schemaDir, "*.schema.json", SearchOption.TopDirectoryOnly))
+ {
+ if (!expectedSchemas.Contains(file))
+ {
+ if (!IsGeneratedSchema(file))
+ {
+ continue;
+ }
+
+ File.Delete(file);
+ }
+ }
}
private static void WriteUtf8File(string path, string content)
@@ -61,6 +121,50 @@ internal sealed class Generator
var normalized = content.Replace("\r\n", "\n", StringComparison.Ordinal);
File.WriteAllText(path, normalized, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
}
+
+ private static bool IsGeneratedSchema(string path)
+ {
+ using var stream = File.OpenRead(path);
+ using var doc = JsonDocument.Parse(stream);
+
+ if (!doc.RootElement.TryGetProperty("$comment", out var comment))
+ {
+ return false;
+ }
+
+ return string.Equals(comment.GetString(), "Generated by StellaOps.Attestor.Types.Generator.", StringComparison.Ordinal);
+ }
+}
+
+internal sealed record GeneratorOptions(string? RepoRoot, bool PruneStaleSchemas)
+{
+ public static GeneratorOptions Parse(string[] args)
+ {
+ string? repoRoot = null;
+ var pruneStaleSchemas = true;
+
+ for (var i = 0; i < args.Length; i++)
+ {
+ var arg = args[i];
+ if (string.Equals(arg, "--repo-root", StringComparison.OrdinalIgnoreCase))
+ {
+ if (i + 1 >= args.Length)
+ {
+ throw new ArgumentException("Missing value for --repo-root.");
+ }
+
+ repoRoot = args[++i];
+ continue;
+ }
+
+ if (string.Equals(arg, "--no-prune", StringComparison.OrdinalIgnoreCase))
+ {
+ pruneStaleSchemas = false;
+ }
+ }
+
+ return new GeneratorOptions(repoRoot, pruneStaleSchemas);
+ }
}
internal sealed class TypeRegistry
@@ -468,7 +572,8 @@ internal static class SchemaBuilder
var schema = new JsonObject
{
["$schema"] = "https://json-schema.org/draft/2020-12/schema",
- ["$id"] = $"https://stella-ops.org/schemas/attestor/{root.SchemaFileStem}.json",
+ ["$id"] = $"https://stella-ops.org/schemas/attestor/{root.SchemaFileStem}.schema.json",
+ ["$comment"] = "Generated by StellaOps.Attestor.Types.Generator.",
["title"] = root.Summary,
["type"] = "object",
["additionalProperties"] = false
@@ -678,6 +783,14 @@ internal static class TypeScriptEmitter
builder.AppendLine();
}
+ foreach (var obj in orderedObjects)
+ {
+ var orderedKeys = obj.Properties.Select(p => p.Name).OrderBy(n => n, StringComparer.Ordinal);
+ var keysLiteral = string.Join(", ", orderedKeys.Select(key => $"'{key.Replace("'", "\\'")}'"));
+ AppendLine(builder, 0, $"const {obj.Name}Keys = Object.freeze([{keysLiteral}] as const);");
+ builder.AppendLine();
+ }
+
AppendLine(builder, 0, "function isRecord(value: unknown): value is Record {");
AppendLine(builder, 1, "return typeof value === 'object' && value !== null && !Array.isArray(value);");
AppendLine(builder, 0, "}");
@@ -688,12 +801,22 @@ internal static class TypeScriptEmitter
AppendLine(builder, 0, "}");
builder.AppendLine();
+ AppendLine(builder, 0, "function assertNoUnknownKeys(value: Record, allowed: readonly string[], path: string[]): void {");
+ AppendLine(builder, 1, "for (const key of Object.keys(value)) {");
+ AppendLine(builder, 2, "if (!allowed.includes(key)) {");
+ AppendLine(builder, 3, "throw new Error(`${pathString(path)} has unknown property '${key}'.`);");
+ AppendLine(builder, 2, "}");
+ AppendLine(builder, 1, "}");
+ AppendLine(builder, 0, "}");
+ builder.AppendLine();
+
foreach (var obj in orderedObjects)
{
AppendLine(builder, 0, $"function assert{obj.Name}(value: unknown, path: string[]): asserts value is {obj.Name} {{");
AppendLine(builder, 1, "if (!isRecord(value)) {");
AppendLine(builder, 2, "throw new Error(`${pathString(path)} must be an object.`);");
AppendLine(builder, 1, "}");
+ AppendLine(builder, 1, $"assertNoUnknownKeys(value, {obj.Name}Keys, path);");
foreach (var property in obj.Properties)
{
@@ -734,23 +857,55 @@ internal static class TypeScriptEmitter
}
AppendLine(builder, 0, "function canonicalStringify(input: unknown): string {");
- AppendLine(builder, 1, "return JSON.stringify(sortValue(input));");
+ AppendLine(builder, 1, "return canonicalizeValue(input);");
AppendLine(builder, 0, "}");
builder.AppendLine();
- AppendLine(builder, 0, "function sortValue(value: unknown): unknown {");
+ AppendLine(builder, 0, "function canonicalizeValue(value: unknown): string {");
+ AppendLine(builder, 1, "if (value === null) {");
+ AppendLine(builder, 2, "return 'null';");
+ AppendLine(builder, 1, "}");
+ AppendLine(builder, 1, "if (typeof value === 'string') {");
+ AppendLine(builder, 2, "return JSON.stringify(value);");
+ AppendLine(builder, 1, "}");
+ AppendLine(builder, 1, "if (typeof value === 'number') {");
+ AppendLine(builder, 2, "return formatNumber(value);");
+ AppendLine(builder, 1, "}");
+ AppendLine(builder, 1, "if (typeof value === 'boolean') {");
+ AppendLine(builder, 2, "return value ? 'true' : 'false';");
+ AppendLine(builder, 1, "}");
AppendLine(builder, 1, "if (Array.isArray(value)) {");
- AppendLine(builder, 2, "return value.map(sortValue);");
+ AppendLine(builder, 2, "return `[${value.map(canonicalizeValue).join(',')}]`;");
AppendLine(builder, 1, "}");
AppendLine(builder, 1, "if (isRecord(value)) {");
- AppendLine(builder, 2, "const ordered: Record = {};");
- AppendLine(builder, 2, "const keys = Object.keys(value).sort();");
- AppendLine(builder, 2, "for (const key of keys) {");
- AppendLine(builder, 3, "ordered[key] = sortValue(value[key]);");
- AppendLine(builder, 2, "}");
- AppendLine(builder, 2, "return ordered;");
+ AppendLine(builder, 2, "return canonicalizeObject(value);");
AppendLine(builder, 1, "}");
- AppendLine(builder, 1, "return value;");
+ AppendLine(builder, 1, "throw new Error('Unsupported value for canonical JSON.');");
+ AppendLine(builder, 0, "}");
+ builder.AppendLine();
+
+ AppendLine(builder, 0, "function canonicalizeObject(value: Record): string {");
+ AppendLine(builder, 1, "const keys = Object.keys(value).sort();");
+ AppendLine(builder, 1, "const entries: string[] = [];");
+ AppendLine(builder, 1, "for (const key of keys) {");
+ AppendLine(builder, 2, "const entry = value[key];");
+ AppendLine(builder, 2, "if (entry === undefined) {");
+ AppendLine(builder, 3, "continue;");
+ AppendLine(builder, 2, "}");
+ AppendLine(builder, 2, "entries.push(`${JSON.stringify(key)}:${canonicalizeValue(entry)}`);");
+ AppendLine(builder, 1, "}");
+ AppendLine(builder, 1, "return `{${entries.join(',')}}`;");
+ AppendLine(builder, 0, "}");
+ builder.AppendLine();
+
+ AppendLine(builder, 0, "function formatNumber(value: number): string {");
+ AppendLine(builder, 1, "if (!Number.isFinite(value)) {");
+ AppendLine(builder, 2, "throw new Error('Non-finite numbers are not allowed in canonical JSON.');");
+ AppendLine(builder, 1, "}");
+ AppendLine(builder, 1, "if (Object.is(value, -0)) {");
+ AppendLine(builder, 2, "return '0';");
+ AppendLine(builder, 1, "}");
+ AppendLine(builder, 1, "return value.toString();");
AppendLine(builder, 0, "}");
builder.AppendLine();
@@ -932,13 +1087,40 @@ internal static class GoEmitter
AppendLine(builder, 0, "package attesttypes");
builder.AppendLine();
+ var patternRegistry = PatternRegistry.Build(registry);
+ var imports = new List
+ {
+ "bytes",
+ "encoding/json",
+ "errors",
+ "fmt",
+ "sort"
+ };
+
+ if (patternRegistry.HasPatterns)
+ {
+ imports.Add("regexp");
+ }
+
AppendLine(builder, 0, "import (");
- AppendLine(builder, 1, "\"encoding/json\"");
- AppendLine(builder, 1, "\"errors\"");
- AppendLine(builder, 1, "\"fmt\"");
+ foreach (var importName in imports)
+ {
+ AppendLine(builder, 1, $"\"{importName}\"");
+ }
AppendLine(builder, 0, ")");
builder.AppendLine();
+ if (patternRegistry.HasPatterns)
+ {
+ AppendLine(builder, 0, "var (");
+ foreach (var pattern in patternRegistry.Patterns)
+ {
+ AppendLine(builder, 1, $"{pattern.Value} = regexp.MustCompile(\"{EscapeGoString(pattern.Key)}\")");
+ }
+ AppendLine(builder, 0, ")");
+ builder.AppendLine();
+ }
+
foreach (var enumSpec in registry.Enums.Values.OrderBy(e => e.Name, StringComparer.Ordinal))
{
EmitEnum(builder, enumSpec);
@@ -959,7 +1141,9 @@ internal static class GoEmitter
{
EmitStruct(builder, obj);
builder.AppendLine();
- EmitValidateMethod(builder, obj);
+ EmitUnmarshalMethod(builder, obj);
+ builder.AppendLine();
+ EmitValidateMethod(builder, obj, patternRegistry);
builder.AppendLine();
}
@@ -969,9 +1153,50 @@ internal static class GoEmitter
builder.AppendLine();
}
+ EmitCanonicalHelpers(builder);
+
return builder.ToString();
}
+ private sealed record PatternRegistry(IReadOnlyDictionary Patterns)
+ {
+ public bool HasPatterns => Patterns.Count > 0;
+
+ public static PatternRegistry Build(TypeRegistry registry)
+ {
+ var patterns = registry.Objects.Values
+ .SelectMany(o => o.Properties)
+ .Select(p => p.Type)
+ .OfType()
+ .Where(p => p.Kind == PrimitiveKind.String && !string.IsNullOrWhiteSpace(p.Pattern))
+ .Select(p => p.Pattern!)
+ .Distinct(StringComparer.Ordinal)
+ .OrderBy(p => p, StringComparer.Ordinal)
+ .ToList();
+
+ var map = new Dictionary(StringComparer.Ordinal);
+ for (var i = 0; i < patterns.Count; i++)
+ {
+ map[patterns[i]] = $"pattern{i}";
+ }
+
+ return new PatternRegistry(map);
+ }
+
+ public string GetPatternName(string pattern)
+ {
+ if (!Patterns.TryGetValue(pattern, out var name))
+ {
+ throw new InvalidOperationException($"Pattern not registered: {pattern}");
+ }
+
+ return name;
+ }
+ }
+
+ private static string EscapeGoString(string value)
+ => value.Replace("\\", "\\\\").Replace("\"", "\\\"");
+
private static void EmitEnum(StringBuilder builder, EnumSpec enumSpec)
{
AppendLine(builder, 0, $"type {enumSpec.Name} string");
@@ -1005,7 +1230,22 @@ internal static class GoEmitter
AppendLine(builder, 0, "}");
}
- private static void EmitValidateMethod(StringBuilder builder, ObjectSpec obj)
+ private static void EmitUnmarshalMethod(StringBuilder builder, ObjectSpec obj)
+ {
+ AppendLine(builder, 0, $"func (value *{obj.Name}) UnmarshalJSON(data []byte) error {{");
+ AppendLine(builder, 1, $"type Alias {obj.Name}");
+ AppendLine(builder, 1, "dec := json.NewDecoder(bytes.NewReader(data))");
+ AppendLine(builder, 1, "dec.DisallowUnknownFields()");
+ AppendLine(builder, 1, "var aux Alias");
+ AppendLine(builder, 1, "if err := dec.Decode(&aux); err != nil {");
+ AppendLine(builder, 2, $"return fmt.Errorf(\"failed to decode {obj.Name}: %w\", err)");
+ AppendLine(builder, 1, "}");
+ AppendLine(builder, 1, $"*value = {obj.Name}(aux)");
+ AppendLine(builder, 1, "return nil");
+ AppendLine(builder, 0, "}");
+ }
+
+ private static void EmitValidateMethod(StringBuilder builder, ObjectSpec obj, PatternRegistry patternRegistry)
{
AppendLine(builder, 0, $"func (value *{obj.Name}) Validate() error {{");
AppendLine(builder, 1, "if value == nil {");
@@ -1014,7 +1254,7 @@ internal static class GoEmitter
foreach (var property in obj.Properties)
{
- EmitPropertyValidation(builder, property, $"value.{ToExported(property.Name)}", $"{obj.Name}.{ToExported(property.Name)}", 1);
+ EmitPropertyValidation(builder, property, $"value.{ToExported(property.Name)}", $"{obj.Name}.{ToExported(property.Name)}", patternRegistry, 1);
}
AppendLine(builder, 1, "return nil");
@@ -1027,20 +1267,100 @@ internal static class GoEmitter
AppendLine(builder, 1, "if err := value.Validate(); err != nil {");
AppendLine(builder, 2, "return nil, err");
AppendLine(builder, 1, "}");
- AppendLine(builder, 1, "buf, err := json.Marshal(value)");
+ AppendLine(builder, 1, "raw, err := json.Marshal(value)");
AppendLine(builder, 1, "if err != nil {");
AppendLine(builder, 2, $"return nil, fmt.Errorf(\"failed to marshal {typeName}: %w\", err)");
AppendLine(builder, 1, "}");
- AppendLine(builder, 1, "return buf, nil");
+ AppendLine(builder, 1, "dec := json.NewDecoder(bytes.NewReader(raw))");
+ AppendLine(builder, 1, "dec.UseNumber()");
+ AppendLine(builder, 1, "var decoded any");
+ AppendLine(builder, 1, "if err := dec.Decode(&decoded); err != nil {");
+ AppendLine(builder, 2, $"return nil, fmt.Errorf(\"failed to parse {typeName}: %w\", err)");
+ AppendLine(builder, 1, "}");
+ AppendLine(builder, 1, "return canonicalizeJSON(decoded)");
AppendLine(builder, 0, "}");
}
- private static void EmitPropertyValidation(StringBuilder builder, PropertySpec property, string accessor, string path, int indent)
+ private static void EmitCanonicalHelpers(StringBuilder builder)
+ {
+ AppendLine(builder, 0, "func canonicalizeJSON(value any) ([]byte, error) {");
+ AppendLine(builder, 1, "var buf bytes.Buffer");
+ AppendLine(builder, 1, "if err := writeCanonicalValue(&buf, value); err != nil {");
+ AppendLine(builder, 2, "return nil, err");
+ AppendLine(builder, 1, "}");
+ AppendLine(builder, 1, "return buf.Bytes(), nil");
+ AppendLine(builder, 0, "}");
+ builder.AppendLine();
+
+ AppendLine(builder, 0, "func writeCanonicalValue(buf *bytes.Buffer, value any) error {");
+ AppendLine(builder, 1, "switch v := value.(type) {");
+ AppendLine(builder, 1, "case nil:");
+ AppendLine(builder, 2, "buf.WriteString(\"null\")");
+ AppendLine(builder, 1, "case bool:");
+ AppendLine(builder, 2, "if v {");
+ AppendLine(builder, 3, "buf.WriteString(\"true\")");
+ AppendLine(builder, 2, "} else {");
+ AppendLine(builder, 3, "buf.WriteString(\"false\")");
+ AppendLine(builder, 2, "}");
+ AppendLine(builder, 1, "case string:");
+ AppendLine(builder, 2, "encoded, err := json.Marshal(v)");
+ AppendLine(builder, 2, "if err != nil {");
+ AppendLine(builder, 3, "return err");
+ AppendLine(builder, 2, "}");
+ AppendLine(builder, 2, "buf.Write(encoded)");
+ AppendLine(builder, 1, "case json.Number:");
+ AppendLine(builder, 2, "text := v.String()");
+ AppendLine(builder, 2, "if text == \"-0\" {");
+ AppendLine(builder, 3, "text = \"0\"");
+ AppendLine(builder, 2, "}");
+ AppendLine(builder, 2, "buf.WriteString(text)");
+ AppendLine(builder, 1, "case []any:");
+ AppendLine(builder, 2, "buf.WriteByte('[')");
+ AppendLine(builder, 2, "for i, item := range v {");
+ AppendLine(builder, 3, "if i > 0 {");
+ AppendLine(builder, 4, "buf.WriteByte(',')");
+ AppendLine(builder, 3, "}");
+ AppendLine(builder, 3, "if err := writeCanonicalValue(buf, item); err != nil {");
+ AppendLine(builder, 4, "return err");
+ AppendLine(builder, 3, "}");
+ AppendLine(builder, 2, "}");
+ AppendLine(builder, 2, "buf.WriteByte(']')");
+ AppendLine(builder, 1, "case map[string]any:");
+ AppendLine(builder, 2, "keys := make([]string, 0, len(v))");
+ AppendLine(builder, 2, "for key := range v {");
+ AppendLine(builder, 3, "keys = append(keys, key)");
+ AppendLine(builder, 2, "}");
+ AppendLine(builder, 2, "sort.Strings(keys)");
+ AppendLine(builder, 2, "buf.WriteByte('{')");
+ AppendLine(builder, 2, "for i, key := range keys {");
+ AppendLine(builder, 3, "if i > 0 {");
+ AppendLine(builder, 4, "buf.WriteByte(',')");
+ AppendLine(builder, 3, "}");
+ AppendLine(builder, 3, "encoded, err := json.Marshal(key)");
+ AppendLine(builder, 3, "if err != nil {");
+ AppendLine(builder, 4, "return err");
+ AppendLine(builder, 3, "}");
+ AppendLine(builder, 3, "buf.Write(encoded)");
+ AppendLine(builder, 3, "buf.WriteByte(':')");
+ AppendLine(builder, 3, "if err := writeCanonicalValue(buf, v[key]); err != nil {");
+ AppendLine(builder, 4, "return err");
+ AppendLine(builder, 3, "}");
+ AppendLine(builder, 2, "}");
+ AppendLine(builder, 2, "buf.WriteByte('}')");
+ AppendLine(builder, 1, "default:");
+ AppendLine(builder, 2, "return fmt.Errorf(\"unsupported canonical type %T\", value)");
+ AppendLine(builder, 1, "}");
+ AppendLine(builder, 1, "return nil");
+ AppendLine(builder, 0, "}");
+ builder.AppendLine();
+ }
+
+ private static void EmitPropertyValidation(StringBuilder builder, PropertySpec property, string accessor, string path, PatternRegistry patternRegistry, int indent)
{
switch (property.Type)
{
case PrimitiveShape primitive:
- EmitPrimitiveValidation(builder, primitive, accessor, path, property.Required, indent);
+ EmitPrimitiveValidation(builder, primitive, accessor, path, property.Required, patternRegistry, indent);
break;
case EnumShape enumShape:
EmitEnumValidation(builder, enumShape, accessor, path, property.Required, indent);
@@ -1057,10 +1377,10 @@ internal static class GoEmitter
}
}
- private static void EmitPrimitiveValidation(StringBuilder builder, PrimitiveShape primitive, string accessor, string path, bool required, int indent)
+ private static void EmitPrimitiveValidation(StringBuilder builder, PrimitiveShape primitive, string accessor, string path, bool required, PatternRegistry patternRegistry, int indent)
{
var pointer = UsesPointer(primitive, required);
- if (!TryBuildPrimitiveChecks(primitive, pointer ? $"*{accessor}" : accessor, path, out var lines))
+ if (!TryBuildPrimitiveChecks(primitive, pointer ? $"*{accessor}" : accessor, path, patternRegistry, out var lines))
{
return;
}
@@ -1083,7 +1403,7 @@ internal static class GoEmitter
}
}
- private static bool TryBuildPrimitiveChecks(PrimitiveShape primitive, string target, string path, out List lines)
+ private static bool TryBuildPrimitiveChecks(PrimitiveShape primitive, string target, string path, PatternRegistry patternRegistry, out List lines)
{
lines = new List();
@@ -1108,7 +1428,14 @@ internal static class GoEmitter
lines.Add("}");
}
- // No pattern validation for now.
+ if (primitive.Kind == PrimitiveKind.String && !string.IsNullOrWhiteSpace(primitive.Pattern))
+ {
+ var patternName = patternRegistry.GetPatternName(primitive.Pattern!);
+ lines.Add($"if !{patternName}.MatchString({target}) {{");
+ lines.Add($"\treturn fmt.Errorf(\"{path} must match {primitive.Pattern}\")");
+ lines.Add("}");
+ }
+
if (lines.Count == 0)
{
return false;
diff --git a/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/StellaOps.Attestor.Types.Generator.csproj b/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/StellaOps.Attestor.Types.Generator.csproj
index fcc7af6b6..dab6122f1 100644
--- a/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/StellaOps.Attestor.Types.Generator.csproj
+++ b/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/StellaOps.Attestor.Types.Generator.csproj
@@ -4,6 +4,6 @@
net10.0
enable
enable
- false
+ true
diff --git a/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/TASKS.md b/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/TASKS.md
index 8541c3c3c..ad714bd80 100644
--- a/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/TASKS.md
+++ b/src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/TASKS.md
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0069-M | DONE | Maintainability audit for StellaOps.Attestor.Types.Generator. |
| AUDIT-0069-T | DONE | Test coverage audit for StellaOps.Attestor.Types.Generator. |
-| AUDIT-0069-A | TODO | Pending approval for changes. |
+| AUDIT-0069-A | DONE | Applied repo-root override, schema id fix, canonicalization, strict validation, prune, and tests. |
diff --git a/src/Attestor/StellaOps.Attestor.Types/generated/go/types.go b/src/Attestor/StellaOps.Attestor.Types/generated/go/types.go
index 7fa7ebf49..d3edb8bc7 100644
--- a/src/Attestor/StellaOps.Attestor.Types/generated/go/types.go
+++ b/src/Attestor/StellaOps.Attestor.Types/generated/go/types.go
@@ -2,9 +2,17 @@
package attesttypes
import (
+ "bytes"
"encoding/json"
"errors"
"fmt"
+ "sort"
+ "regexp"
+)
+
+var (
+ pattern0 = regexp.MustCompile("^[A-Fa-f0-9]{64}$")
+ pattern1 = regexp.MustCompile("^sha256:[A-Fa-f0-9]{64}$")
)
type FindingStatus string
@@ -191,6 +199,18 @@ type BuildMetadata struct {
BuildInvocationId *string `json:"buildInvocationId,omitempty"`
}
+func (value *BuildMetadata) UnmarshalJSON(data []byte) error {
+ type Alias BuildMetadata
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode BuildMetadata: %w", err)
+ }
+ *value = BuildMetadata(aux)
+ return nil
+}
+
func (value *BuildMetadata) Validate() error {
if value == nil {
return errors.New("BuildMetadata is nil")
@@ -207,6 +227,18 @@ type BuildProvenance struct {
Environment *EnvironmentMetadata `json:"environment,omitempty"`
}
+func (value *BuildProvenance) UnmarshalJSON(data []byte) error {
+ type Alias BuildProvenance
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode BuildProvenance: %w", err)
+ }
+ *value = BuildProvenance(aux)
+ return nil
+}
+
func (value *BuildProvenance) Validate() error {
if value == nil {
return errors.New("BuildProvenance is nil")
@@ -242,6 +274,18 @@ type BuilderIdentity struct {
Platform *string `json:"platform,omitempty"`
}
+func (value *BuilderIdentity) UnmarshalJSON(data []byte) error {
+ type Alias BuilderIdentity
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode BuilderIdentity: %w", err)
+ }
+ *value = BuilderIdentity(aux)
+ return nil
+}
+
func (value *BuilderIdentity) Validate() error {
if value == nil {
return errors.New("BuilderIdentity is nil")
@@ -257,6 +301,18 @@ type CustomEvidence struct {
Properties []CustomProperty `json:"properties,omitempty"`
}
+func (value *CustomEvidence) UnmarshalJSON(data []byte) error {
+ type Alias CustomEvidence
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode CustomEvidence: %w", err)
+ }
+ *value = CustomEvidence(aux)
+ return nil
+}
+
func (value *CustomEvidence) Validate() error {
if value == nil {
return errors.New("CustomEvidence is nil")
@@ -264,6 +320,9 @@ func (value *CustomEvidence) Validate() error {
if value.SchemaVersion != "StellaOps.CustomEvidence@1" {
return fmt.Errorf("CustomEvidence.SchemaVersion must equal StellaOps.CustomEvidence@1")
}
+ if !pattern1.MatchString(value.SubjectDigest) {
+ return fmt.Errorf("CustomEvidence.SubjectDigest must match ^sha256:[A-Fa-f0-9]{64}$")
+ }
for i := range value.Properties {
if err := value.Properties[i].Validate(); err != nil {
return fmt.Errorf("invalid CustomEvidence.Properties[%d]: %w", i, err)
@@ -277,6 +336,18 @@ type CustomProperty struct {
Value string `json:"value"`
}
+func (value *CustomProperty) UnmarshalJSON(data []byte) error {
+ type Alias CustomProperty
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode CustomProperty: %w", err)
+ }
+ *value = CustomProperty(aux)
+ return nil
+}
+
func (value *CustomProperty) Validate() error {
if value == nil {
return errors.New("CustomProperty is nil")
@@ -290,6 +361,18 @@ type DiffHunk struct {
Content *string `json:"content,omitempty"`
}
+func (value *DiffHunk) UnmarshalJSON(data []byte) error {
+ type Alias DiffHunk
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode DiffHunk: %w", err)
+ }
+ *value = DiffHunk(aux)
+ return nil
+}
+
func (value *DiffHunk) Validate() error {
if value == nil {
return errors.New("DiffHunk is nil")
@@ -312,6 +395,18 @@ type DiffPayload struct {
PackagesRemoved []PackageRef `json:"packagesRemoved,omitempty"`
}
+func (value *DiffPayload) UnmarshalJSON(data []byte) error {
+ type Alias DiffPayload
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode DiffPayload: %w", err)
+ }
+ *value = DiffPayload(aux)
+ return nil
+}
+
func (value *DiffPayload) Validate() error {
if value == nil {
return errors.New("DiffPayload is nil")
@@ -344,10 +439,25 @@ type DigestReference struct {
Value string `json:"value"`
}
+func (value *DigestReference) UnmarshalJSON(data []byte) error {
+ type Alias DigestReference
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode DigestReference: %w", err)
+ }
+ *value = DigestReference(aux)
+ return nil
+}
+
func (value *DigestReference) Validate() error {
if value == nil {
return errors.New("DigestReference is nil")
}
+ if !pattern0.MatchString(value.Value) {
+ return fmt.Errorf("DigestReference.Value must match ^[A-Fa-f0-9]{64}$")
+ }
return nil
}
@@ -356,6 +466,18 @@ type EnvironmentMetadata struct {
ImageDigest *DigestReference `json:"imageDigest,omitempty"`
}
+func (value *EnvironmentMetadata) UnmarshalJSON(data []byte) error {
+ type Alias EnvironmentMetadata
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode EnvironmentMetadata: %w", err)
+ }
+ *value = EnvironmentMetadata(aux)
+ return nil
+}
+
func (value *EnvironmentMetadata) Validate() error {
if value == nil {
return errors.New("EnvironmentMetadata is nil")
@@ -375,6 +497,18 @@ type FileChange struct {
ToHash *string `json:"toHash,omitempty"`
}
+func (value *FileChange) UnmarshalJSON(data []byte) error {
+ type Alias FileChange
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode FileChange: %w", err)
+ }
+ *value = FileChange(aux)
+ return nil
+}
+
func (value *FileChange) Validate() error {
if value == nil {
return errors.New("FileChange is nil")
@@ -393,6 +527,18 @@ type FindingKey struct {
CveId string `json:"cveId"`
}
+func (value *FindingKey) UnmarshalJSON(data []byte) error {
+ type Alias FindingKey
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode FindingKey: %w", err)
+ }
+ *value = FindingKey(aux)
+ return nil
+}
+
func (value *FindingKey) Validate() error {
if value == nil {
return errors.New("FindingKey is nil")
@@ -406,10 +552,25 @@ type ImageReference struct {
Tag *string `json:"tag,omitempty"`
}
+func (value *ImageReference) UnmarshalJSON(data []byte) error {
+ type Alias ImageReference
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode ImageReference: %w", err)
+ }
+ *value = ImageReference(aux)
+ return nil
+}
+
func (value *ImageReference) Validate() error {
if value == nil {
return errors.New("ImageReference is nil")
}
+ if !pattern1.MatchString(value.Digest) {
+ return fmt.Errorf("ImageReference.Digest must match ^sha256:[A-Fa-f0-9]{64}$")
+ }
return nil
}
@@ -418,6 +579,18 @@ type LicenseDelta struct {
Removed []string `json:"removed,omitempty"`
}
+func (value *LicenseDelta) UnmarshalJSON(data []byte) error {
+ type Alias LicenseDelta
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode LicenseDelta: %w", err)
+ }
+ *value = LicenseDelta(aux)
+ return nil
+}
+
func (value *LicenseDelta) Validate() error {
if value == nil {
return errors.New("LicenseDelta is nil")
@@ -434,6 +607,18 @@ type MaterialChange struct {
PriorityScore *float64 `json:"priorityScore,omitempty"`
}
+func (value *MaterialChange) UnmarshalJSON(data []byte) error {
+ type Alias MaterialChange
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode MaterialChange: %w", err)
+ }
+ *value = MaterialChange(aux)
+ return nil
+}
+
func (value *MaterialChange) Validate() error {
if value == nil {
return errors.New("MaterialChange is nil")
@@ -468,6 +653,18 @@ type MaterialReference struct {
Note *string `json:"note,omitempty"`
}
+func (value *MaterialReference) UnmarshalJSON(data []byte) error {
+ type Alias MaterialReference
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode MaterialReference: %w", err)
+ }
+ *value = MaterialReference(aux)
+ return nil
+}
+
func (value *MaterialReference) Validate() error {
if value == nil {
return errors.New("MaterialReference is nil")
@@ -491,6 +688,18 @@ type PackageChange struct {
LicenseDelta *LicenseDelta `json:"licenseDelta,omitempty"`
}
+func (value *PackageChange) UnmarshalJSON(data []byte) error {
+ type Alias PackageChange
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode PackageChange: %w", err)
+ }
+ *value = PackageChange(aux)
+ return nil
+}
+
func (value *PackageChange) Validate() error {
if value == nil {
return errors.New("PackageChange is nil")
@@ -509,6 +718,18 @@ type PackageRef struct {
Purl *string `json:"purl,omitempty"`
}
+func (value *PackageRef) UnmarshalJSON(data []byte) error {
+ type Alias PackageRef
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode PackageRef: %w", err)
+ }
+ *value = PackageRef(aux)
+ return nil
+}
+
func (value *PackageRef) Validate() error {
if value == nil {
return errors.New("PackageRef is nil")
@@ -524,6 +745,18 @@ type PolicyDecision struct {
Remediation *string `json:"remediation,omitempty"`
}
+func (value *PolicyDecision) UnmarshalJSON(data []byte) error {
+ type Alias PolicyDecision
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode PolicyDecision: %w", err)
+ }
+ *value = PolicyDecision(aux)
+ return nil
+}
+
func (value *PolicyDecision) Validate() error {
if value == nil {
return errors.New("PolicyDecision is nil")
@@ -543,6 +776,18 @@ type PolicyEvaluation struct {
Decisions []PolicyDecision `json:"decisions"`
}
+func (value *PolicyEvaluation) UnmarshalJSON(data []byte) error {
+ type Alias PolicyEvaluation
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode PolicyEvaluation: %w", err)
+ }
+ *value = PolicyEvaluation(aux)
+ return nil
+}
+
func (value *PolicyEvaluation) Validate() error {
if value == nil {
return errors.New("PolicyEvaluation is nil")
@@ -550,6 +795,9 @@ func (value *PolicyEvaluation) Validate() error {
if value.SchemaVersion != "StellaOps.PolicyEvaluation@1" {
return fmt.Errorf("PolicyEvaluation.SchemaVersion must equal StellaOps.PolicyEvaluation@1")
}
+ if !pattern1.MatchString(value.SubjectDigest) {
+ return fmt.Errorf("PolicyEvaluation.SubjectDigest must match ^sha256:[A-Fa-f0-9]{64}$")
+ }
if err := value.Outcome.Validate(); err != nil {
return fmt.Errorf("invalid PolicyEvaluation.Outcome: %w", err)
}
@@ -569,6 +817,18 @@ type ReachabilityGate struct {
Rationale *string `json:"rationale,omitempty"`
}
+func (value *ReachabilityGate) UnmarshalJSON(data []byte) error {
+ type Alias ReachabilityGate
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode ReachabilityGate: %w", err)
+ }
+ *value = ReachabilityGate(aux)
+ return nil
+}
+
func (value *ReachabilityGate) Validate() error {
if value == nil {
return errors.New("ReachabilityGate is nil")
@@ -588,6 +848,18 @@ type RiskFactor struct {
Description *string `json:"description,omitempty"`
}
+func (value *RiskFactor) UnmarshalJSON(data []byte) error {
+ type Alias RiskFactor
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode RiskFactor: %w", err)
+ }
+ *value = RiskFactor(aux)
+ return nil
+}
+
func (value *RiskFactor) Validate() error {
if value == nil {
return errors.New("RiskFactor is nil")
@@ -610,6 +882,18 @@ type RiskProfileEvidence struct {
Factors []RiskFactor `json:"factors"`
}
+func (value *RiskProfileEvidence) UnmarshalJSON(data []byte) error {
+ type Alias RiskProfileEvidence
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode RiskProfileEvidence: %w", err)
+ }
+ *value = RiskProfileEvidence(aux)
+ return nil
+}
+
func (value *RiskProfileEvidence) Validate() error {
if value == nil {
return errors.New("RiskProfileEvidence is nil")
@@ -617,6 +901,9 @@ func (value *RiskProfileEvidence) Validate() error {
if value.SchemaVersion != "StellaOps.RiskProfileEvidence@1" {
return fmt.Errorf("RiskProfileEvidence.SchemaVersion must equal StellaOps.RiskProfileEvidence@1")
}
+ if !pattern1.MatchString(value.SubjectDigest) {
+ return fmt.Errorf("RiskProfileEvidence.SubjectDigest must match ^sha256:[A-Fa-f0-9]{64}$")
+ }
if value.RiskScore < 0 {
return fmt.Errorf("RiskProfileEvidence.RiskScore must be >= 0")
}
@@ -643,6 +930,18 @@ type RiskState struct {
PolicyFlags []string `json:"policyFlags,omitempty"`
}
+func (value *RiskState) UnmarshalJSON(data []byte) error {
+ type Alias RiskState
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode RiskState: %w", err)
+ }
+ *value = RiskState(aux)
+ return nil
+}
+
func (value *RiskState) Validate() error {
if value == nil {
return errors.New("RiskState is nil")
@@ -667,6 +966,18 @@ type RuntimeContext struct {
User *UserContext `json:"user,omitempty"`
}
+func (value *RuntimeContext) UnmarshalJSON(data []byte) error {
+ type Alias RuntimeContext
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode RuntimeContext: %w", err)
+ }
+ *value = RuntimeContext(aux)
+ return nil
+}
+
func (value *RuntimeContext) Validate() error {
if value == nil {
return errors.New("RuntimeContext is nil")
@@ -689,6 +1000,18 @@ type SbomAttestation struct {
Packages []SbomPackage `json:"packages,omitempty"`
}
+func (value *SbomAttestation) UnmarshalJSON(data []byte) error {
+ type Alias SbomAttestation
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode SbomAttestation: %w", err)
+ }
+ *value = SbomAttestation(aux)
+ return nil
+}
+
func (value *SbomAttestation) Validate() error {
if value == nil {
return errors.New("SbomAttestation is nil")
@@ -696,6 +1019,9 @@ func (value *SbomAttestation) Validate() error {
if value.SchemaVersion != "StellaOps.SBOMAttestation@1" {
return fmt.Errorf("SbomAttestation.SchemaVersion must equal StellaOps.SBOMAttestation@1")
}
+ if !pattern1.MatchString(value.SubjectDigest) {
+ return fmt.Errorf("SbomAttestation.SubjectDigest must match ^sha256:[A-Fa-f0-9]{64}$")
+ }
if err := value.SbomFormat.Validate(); err != nil {
return fmt.Errorf("invalid SbomAttestation.SbomFormat: %w", err)
}
@@ -719,6 +1045,18 @@ type SbomPackage struct {
Licenses []string `json:"licenses,omitempty"`
}
+func (value *SbomPackage) UnmarshalJSON(data []byte) error {
+ type Alias SbomPackage
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode SbomPackage: %w", err)
+ }
+ *value = SbomPackage(aux)
+ return nil
+}
+
func (value *SbomPackage) Validate() error {
if value == nil {
return errors.New("SbomPackage is nil")
@@ -740,6 +1078,18 @@ type ScanFinding struct {
References []string `json:"references,omitempty"`
}
+func (value *ScanFinding) UnmarshalJSON(data []byte) error {
+ type Alias ScanFinding
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode ScanFinding: %w", err)
+ }
+ *value = ScanFinding(aux)
+ return nil
+}
+
func (value *ScanFinding) Validate() error {
if value == nil {
return errors.New("ScanFinding is nil")
@@ -773,6 +1123,18 @@ type ScanResults struct {
Findings []ScanFinding `json:"findings"`
}
+func (value *ScanResults) UnmarshalJSON(data []byte) error {
+ type Alias ScanResults
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode ScanResults: %w", err)
+ }
+ *value = ScanResults(aux)
+ return nil
+}
+
func (value *ScanResults) Validate() error {
if value == nil {
return errors.New("ScanResults is nil")
@@ -780,6 +1142,9 @@ func (value *ScanResults) Validate() error {
if value.SchemaVersion != "StellaOps.ScanResults@1" {
return fmt.Errorf("ScanResults.SchemaVersion must equal StellaOps.ScanResults@1")
}
+ if !pattern1.MatchString(value.SubjectDigest) {
+ return fmt.Errorf("ScanResults.SubjectDigest must match ^sha256:[A-Fa-f0-9]{64}$")
+ }
for i := range value.Findings {
if err := value.Findings[i].Validate(); err != nil {
return fmt.Errorf("invalid ScanResults.Findings[%d]: %w", i, err)
@@ -794,6 +1159,18 @@ type ScannerInfo struct {
Ruleset *string `json:"ruleset,omitempty"`
}
+func (value *ScannerInfo) UnmarshalJSON(data []byte) error {
+ type Alias ScannerInfo
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode ScannerInfo: %w", err)
+ }
+ *value = ScannerInfo(aux)
+ return nil
+}
+
func (value *ScannerInfo) Validate() error {
if value == nil {
return errors.New("ScannerInfo is nil")
@@ -813,6 +1190,18 @@ type SmartDiffPredicate struct {
MaterialChanges []MaterialChange `json:"materialChanges,omitempty"`
}
+func (value *SmartDiffPredicate) UnmarshalJSON(data []byte) error {
+ type Alias SmartDiffPredicate
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode SmartDiffPredicate: %w", err)
+ }
+ *value = SmartDiffPredicate(aux)
+ return nil
+}
+
func (value *SmartDiffPredicate) Validate() error {
if value == nil {
return errors.New("SmartDiffPredicate is nil")
@@ -859,6 +1248,18 @@ type UserContext struct {
Caps []string `json:"caps,omitempty"`
}
+func (value *UserContext) UnmarshalJSON(data []byte) error {
+ type Alias UserContext
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode UserContext: %w", err)
+ }
+ *value = UserContext(aux)
+ return nil
+}
+
func (value *UserContext) Validate() error {
if value == nil {
return errors.New("UserContext is nil")
@@ -883,6 +1284,18 @@ type VexAttestation struct {
Statements []VexStatement `json:"statements"`
}
+func (value *VexAttestation) UnmarshalJSON(data []byte) error {
+ type Alias VexAttestation
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode VexAttestation: %w", err)
+ }
+ *value = VexAttestation(aux)
+ return nil
+}
+
func (value *VexAttestation) Validate() error {
if value == nil {
return errors.New("VexAttestation is nil")
@@ -890,6 +1303,9 @@ func (value *VexAttestation) Validate() error {
if value.SchemaVersion != "StellaOps.VEXAttestation@1" {
return fmt.Errorf("VexAttestation.SchemaVersion must equal StellaOps.VEXAttestation@1")
}
+ if !pattern1.MatchString(value.SubjectDigest) {
+ return fmt.Errorf("VexAttestation.SubjectDigest must match ^sha256:[A-Fa-f0-9]{64}$")
+ }
if len(value.Statements) < 1 {
return fmt.Errorf("VexAttestation.Statements must contain at least 1 item(s)")
}
@@ -911,6 +1327,18 @@ type VexStatement struct {
References []string `json:"references,omitempty"`
}
+func (value *VexStatement) UnmarshalJSON(data []byte) error {
+ type Alias VexStatement
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ var aux Alias
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("failed to decode VexStatement: %w", err)
+ }
+ *value = VexStatement(aux)
+ return nil
+}
+
func (value *VexStatement) Validate() error {
if value == nil {
return errors.New("VexStatement is nil")
@@ -928,87 +1356,204 @@ func (value *BuildProvenance) CanonicalJSON() ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
- buf, err := json.Marshal(value)
+ raw, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal BuildProvenance: %w", err)
}
- return buf, nil
+ dec := json.NewDecoder(bytes.NewReader(raw))
+ dec.UseNumber()
+ var decoded any
+ if err := dec.Decode(&decoded); err != nil {
+ return nil, fmt.Errorf("failed to parse BuildProvenance: %w", err)
+ }
+ return canonicalizeJSON(decoded)
}
func (value *CustomEvidence) CanonicalJSON() ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
- buf, err := json.Marshal(value)
+ raw, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal CustomEvidence: %w", err)
}
- return buf, nil
+ dec := json.NewDecoder(bytes.NewReader(raw))
+ dec.UseNumber()
+ var decoded any
+ if err := dec.Decode(&decoded); err != nil {
+ return nil, fmt.Errorf("failed to parse CustomEvidence: %w", err)
+ }
+ return canonicalizeJSON(decoded)
}
func (value *PolicyEvaluation) CanonicalJSON() ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
- buf, err := json.Marshal(value)
+ raw, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal PolicyEvaluation: %w", err)
}
- return buf, nil
+ dec := json.NewDecoder(bytes.NewReader(raw))
+ dec.UseNumber()
+ var decoded any
+ if err := dec.Decode(&decoded); err != nil {
+ return nil, fmt.Errorf("failed to parse PolicyEvaluation: %w", err)
+ }
+ return canonicalizeJSON(decoded)
}
func (value *RiskProfileEvidence) CanonicalJSON() ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
- buf, err := json.Marshal(value)
+ raw, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal RiskProfileEvidence: %w", err)
}
- return buf, nil
+ dec := json.NewDecoder(bytes.NewReader(raw))
+ dec.UseNumber()
+ var decoded any
+ if err := dec.Decode(&decoded); err != nil {
+ return nil, fmt.Errorf("failed to parse RiskProfileEvidence: %w", err)
+ }
+ return canonicalizeJSON(decoded)
}
func (value *SbomAttestation) CanonicalJSON() ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
- buf, err := json.Marshal(value)
+ raw, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal SbomAttestation: %w", err)
}
- return buf, nil
+ dec := json.NewDecoder(bytes.NewReader(raw))
+ dec.UseNumber()
+ var decoded any
+ if err := dec.Decode(&decoded); err != nil {
+ return nil, fmt.Errorf("failed to parse SbomAttestation: %w", err)
+ }
+ return canonicalizeJSON(decoded)
}
func (value *ScanResults) CanonicalJSON() ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
- buf, err := json.Marshal(value)
+ raw, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal ScanResults: %w", err)
}
- return buf, nil
+ dec := json.NewDecoder(bytes.NewReader(raw))
+ dec.UseNumber()
+ var decoded any
+ if err := dec.Decode(&decoded); err != nil {
+ return nil, fmt.Errorf("failed to parse ScanResults: %w", err)
+ }
+ return canonicalizeJSON(decoded)
}
func (value *SmartDiffPredicate) CanonicalJSON() ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
- buf, err := json.Marshal(value)
+ raw, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal SmartDiffPredicate: %w", err)
}
- return buf, nil
+ dec := json.NewDecoder(bytes.NewReader(raw))
+ dec.UseNumber()
+ var decoded any
+ if err := dec.Decode(&decoded); err != nil {
+ return nil, fmt.Errorf("failed to parse SmartDiffPredicate: %w", err)
+ }
+ return canonicalizeJSON(decoded)
}
func (value *VexAttestation) CanonicalJSON() ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
- buf, err := json.Marshal(value)
+ raw, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal VexAttestation: %w", err)
}
- return buf, nil
+ dec := json.NewDecoder(bytes.NewReader(raw))
+ dec.UseNumber()
+ var decoded any
+ if err := dec.Decode(&decoded); err != nil {
+ return nil, fmt.Errorf("failed to parse VexAttestation: %w", err)
+ }
+ return canonicalizeJSON(decoded)
+}
+
+func canonicalizeJSON(value any) ([]byte, error) {
+ var buf bytes.Buffer
+ if err := writeCanonicalValue(&buf, value); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func writeCanonicalValue(buf *bytes.Buffer, value any) error {
+ switch v := value.(type) {
+ case nil:
+ buf.WriteString("null")
+ case bool:
+ if v {
+ buf.WriteString("true")
+ } else {
+ buf.WriteString("false")
+ }
+ case string:
+ encoded, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+ buf.Write(encoded)
+ case json.Number:
+ text := v.String()
+ if text == "-0" {
+ text = "0"
+ }
+ buf.WriteString(text)
+ case []any:
+ buf.WriteByte('[')
+ for i, item := range v {
+ if i > 0 {
+ buf.WriteByte(',')
+ }
+ if err := writeCanonicalValue(buf, item); err != nil {
+ return err
+ }
+ }
+ buf.WriteByte(']')
+ case map[string]any:
+ keys := make([]string, 0, len(v))
+ for key := range v {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+ buf.WriteByte('{')
+ for i, key := range keys {
+ if i > 0 {
+ buf.WriteByte(',')
+ }
+ encoded, err := json.Marshal(key)
+ if err != nil {
+ return err
+ }
+ buf.Write(encoded)
+ buf.WriteByte(':')
+ if err := writeCanonicalValue(buf, v[key]); err != nil {
+ return err
+ }
+ }
+ buf.WriteByte('}')
+ default:
+ return fmt.Errorf("unsupported canonical type %T", value)
+ }
+ return nil
}
diff --git a/src/Attestor/StellaOps.Attestor.Types/generated/ts/index.ts b/src/Attestor/StellaOps.Attestor.Types/generated/ts/index.ts
index c3feb341c..9de3422d8 100644
--- a/src/Attestor/StellaOps.Attestor.Types/generated/ts/index.ts
+++ b/src/Attestor/StellaOps.Attestor.Types/generated/ts/index.ts
@@ -275,6 +275,72 @@ export interface VexStatement {
references?: Array;
}
+const BuildMetadataKeys = Object.freeze(['buildFinishedOn', 'buildInvocationId', 'buildStartedOn', 'reproducible'] as const);
+
+const BuildProvenanceKeys = Object.freeze(['buildType', 'builder', 'environment', 'materials', 'metadata', 'schemaVersion'] as const);
+
+const BuilderIdentityKeys = Object.freeze(['id', 'platform', 'version'] as const);
+
+const CustomEvidenceKeys = Object.freeze(['generatedAt', 'kind', 'properties', 'schemaVersion', 'subjectDigest'] as const);
+
+const CustomPropertyKeys = Object.freeze(['key', 'value'] as const);
+
+const DiffHunkKeys = Object.freeze(['content', 'lineCount', 'startLine'] as const);
+
+const DiffPayloadKeys = Object.freeze(['filesAdded', 'filesChanged', 'filesRemoved', 'packagesAdded', 'packagesChanged', 'packagesRemoved'] as const);
+
+const DigestReferenceKeys = Object.freeze(['algorithm', 'value'] as const);
+
+const EnvironmentMetadataKeys = Object.freeze(['imageDigest', 'platform'] as const);
+
+const FileChangeKeys = Object.freeze(['fromHash', 'hunks', 'path', 'toHash'] as const);
+
+const FindingKeyKeys = Object.freeze(['componentPurl', 'componentVersion', 'cveId'] as const);
+
+const ImageReferenceKeys = Object.freeze(['digest', 'name', 'tag'] as const);
+
+const LicenseDeltaKeys = Object.freeze(['added', 'removed'] as const);
+
+const MaterialChangeKeys = Object.freeze(['changeType', 'currentState', 'findingKey', 'previousState', 'priorityScore', 'reason'] as const);
+
+const MaterialReferenceKeys = Object.freeze(['digests', 'note', 'uri'] as const);
+
+const PackageChangeKeys = Object.freeze(['from', 'licenseDelta', 'name', 'purl', 'to'] as const);
+
+const PackageRefKeys = Object.freeze(['name', 'purl', 'version'] as const);
+
+const PolicyDecisionKeys = Object.freeze(['effect', 'policyId', 'reason', 'remediation', 'ruleId'] as const);
+
+const PolicyEvaluationKeys = Object.freeze(['decisions', 'evaluatedAt', 'outcome', 'policyVersion', 'schemaVersion', 'subjectDigest'] as const);
+
+const ReachabilityGateKeys = Object.freeze(['class', 'configActivated', 'rationale', 'reachable', 'runningUser'] as const);
+
+const RiskFactorKeys = Object.freeze(['description', 'name', 'weight'] as const);
+
+const RiskProfileEvidenceKeys = Object.freeze(['factors', 'generatedAt', 'riskLevel', 'riskScore', 'schemaVersion', 'subjectDigest'] as const);
+
+const RiskStateKeys = Object.freeze(['epssScore', 'inAffectedRange', 'kev', 'policyFlags', 'reachable', 'vexStatus'] as const);
+
+const RuntimeContextKeys = Object.freeze(['entrypoint', 'env', 'user'] as const);
+
+const SbomAttestationKeys = Object.freeze(['componentCount', 'packages', 'sbomDigest', 'sbomFormat', 'sbomUri', 'schemaVersion', 'subjectDigest'] as const);
+
+const SbomPackageKeys = Object.freeze(['licenses', 'purl', 'version'] as const);
+
+const ScanFindingKeys = Object.freeze(['cvssScore', 'description', 'id', 'packageName', 'packageVersion', 'references', 'severity', 'status'] as const);
+
+const ScanResultsKeys = Object.freeze(['findings', 'generatedAt', 'scannerName', 'scannerVersion', 'schemaVersion', 'subjectDigest'] as const);
+
+const ScannerInfoKeys = Object.freeze(['name', 'ruleset', 'version'] as const);
+
+const SmartDiffPredicateKeys = Object.freeze(['baseImage', 'context', 'diff', 'materialChanges', 'reachabilityGate', 'scanner', 'schemaVersion', 'suppressedCount', 'targetImage'] as const);
+
+const UserContextKeys = Object.freeze(['caps', 'gid', 'uid'] as const);
+
+const VexAttestationKeys = Object.freeze(['generatedAt', 'schemaVersion', 'statements', 'subjectDigest'] as const);
+
+const VexStatementKeys = Object.freeze(['actionStatement', 'impactStatement', 'justification', 'references', 'status', 'timestamp', 'vulnerabilityId'] as const);
+
function isRecord(value: unknown): value is Record {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
@@ -283,10 +349,19 @@ function pathString(path: string[]): string {
return path.length === 0 ? 'value' : `value.${path.join('.')}`;
}
+function assertNoUnknownKeys(value: Record, allowed: readonly string[], path: string[]): void {
+ for (const key of Object.keys(value)) {
+ if (!allowed.includes(key)) {
+ throw new Error(`${pathString(path)} has unknown property '${key}'.`);
+ }
+ }
+}
+
function assertBuildMetadata(value: unknown, path: string[]): asserts value is BuildMetadata {
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, BuildMetadataKeys, path);
if (value.buildStartedOn === undefined) {
throw new Error(`${pathString([...path, 'buildStartedOn'])} is required.`);
}
@@ -315,6 +390,7 @@ function assertBuildProvenance(value: unknown, path: string[]): asserts value is
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, BuildProvenanceKeys, path);
if (value.schemaVersion === undefined) {
throw new Error(`${pathString([...path, 'schemaVersion'])} is required.`);
}
@@ -359,6 +435,7 @@ function assertBuilderIdentity(value: unknown, path: string[]): asserts value is
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, BuilderIdentityKeys, path);
if (value.id === undefined) {
throw new Error(`${pathString([...path, 'id'])} is required.`);
}
@@ -381,6 +458,7 @@ function assertCustomEvidence(value: unknown, path: string[]): asserts value is
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, CustomEvidenceKeys, path);
if (value.schemaVersion === undefined) {
throw new Error(`${pathString([...path, 'schemaVersion'])} is required.`);
}
@@ -425,6 +503,7 @@ function assertCustomProperty(value: unknown, path: string[]): asserts value is
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, CustomPropertyKeys, path);
if (value.key === undefined) {
throw new Error(`${pathString([...path, 'key'])} is required.`);
}
@@ -443,6 +522,7 @@ function assertDiffHunk(value: unknown, path: string[]): asserts value is DiffHu
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, DiffHunkKeys, path);
if (value.startLine === undefined) {
throw new Error(`${pathString([...path, 'startLine'])} is required.`);
}
@@ -472,6 +552,7 @@ function assertDiffPayload(value: unknown, path: string[]): asserts value is Dif
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, DiffPayloadKeys, path);
if (value.filesAdded !== undefined) {
if (!Array.isArray(value.filesAdded)) {
throw new Error(`${pathString([...path, 'filesAdded'])} must be an array.`);
@@ -530,6 +611,7 @@ function assertDigestReference(value: unknown, path: string[]): asserts value is
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, DigestReferenceKeys, path);
if (value.algorithm === undefined) {
throw new Error(`${pathString([...path, 'algorithm'])} is required.`);
}
@@ -551,6 +633,7 @@ function assertEnvironmentMetadata(value: unknown, path: string[]): asserts valu
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, EnvironmentMetadataKeys, path);
if (value.platform !== undefined) {
if (typeof value.platform !== 'string') {
throw new Error(`${pathString([...path, 'platform'])} must be a string.`);
@@ -565,6 +648,7 @@ function assertFileChange(value: unknown, path: string[]): asserts value is File
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, FileChangeKeys, path);
if (value.path === undefined) {
throw new Error(`${pathString([...path, 'path'])} is required.`);
}
@@ -595,6 +679,7 @@ function assertFindingKey(value: unknown, path: string[]): asserts value is Find
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, FindingKeyKeys, path);
if (value.componentPurl === undefined) {
throw new Error(`${pathString([...path, 'componentPurl'])} is required.`);
}
@@ -619,6 +704,7 @@ function assertImageReference(value: unknown, path: string[]): asserts value is
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, ImageReferenceKeys, path);
if (value.digest === undefined) {
throw new Error(`${pathString([...path, 'digest'])} is required.`);
}
@@ -644,6 +730,7 @@ function assertLicenseDelta(value: unknown, path: string[]): asserts value is Li
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, LicenseDeltaKeys, path);
if (value.added !== undefined) {
if (!Array.isArray(value.added)) {
throw new Error(`${pathString([...path, 'added'])} must be an array.`);
@@ -670,6 +757,7 @@ function assertMaterialChange(value: unknown, path: string[]): asserts value is
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, MaterialChangeKeys, path);
if (value.findingKey === undefined) {
throw new Error(`${pathString([...path, 'findingKey'])} is required.`);
}
@@ -706,6 +794,7 @@ function assertMaterialReference(value: unknown, path: string[]): asserts value
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, MaterialReferenceKeys, path);
if (value.uri === undefined) {
throw new Error(`${pathString([...path, 'uri'])} is required.`);
}
@@ -735,6 +824,7 @@ function assertPackageChange(value: unknown, path: string[]): asserts value is P
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, PackageChangeKeys, path);
if (value.name === undefined) {
throw new Error(`${pathString([...path, 'name'])} is required.`);
}
@@ -767,6 +857,7 @@ function assertPackageRef(value: unknown, path: string[]): asserts value is Pack
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, PackageRefKeys, path);
if (value.name === undefined) {
throw new Error(`${pathString([...path, 'name'])} is required.`);
}
@@ -790,6 +881,7 @@ function assertPolicyDecision(value: unknown, path: string[]): asserts value is
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, PolicyDecisionKeys, path);
if (value.policyId === undefined) {
throw new Error(`${pathString([...path, 'policyId'])} is required.`);
}
@@ -824,6 +916,7 @@ function assertPolicyEvaluation(value: unknown, path: string[]): asserts value i
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, PolicyEvaluationKeys, path);
if (value.schemaVersion === undefined) {
throw new Error(`${pathString([...path, 'schemaVersion'])} is required.`);
}
@@ -875,6 +968,7 @@ function assertReachabilityGate(value: unknown, path: string[]): asserts value i
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, ReachabilityGateKeys, path);
if (value.reachable !== undefined) {
if (typeof value.reachable !== 'boolean') {
throw new Error(`${pathString([...path, 'reachable'])} must be a boolean.`);
@@ -913,6 +1007,7 @@ function assertRiskFactor(value: unknown, path: string[]): asserts value is Risk
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, RiskFactorKeys, path);
if (value.name === undefined) {
throw new Error(`${pathString([...path, 'name'])} is required.`);
}
@@ -942,6 +1037,7 @@ function assertRiskProfileEvidence(value: unknown, path: string[]): asserts valu
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, RiskProfileEvidenceKeys, path);
if (value.schemaVersion === undefined) {
throw new Error(`${pathString([...path, 'schemaVersion'])} is required.`);
}
@@ -999,6 +1095,7 @@ function assertRiskState(value: unknown, path: string[]): asserts value is RiskS
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, RiskStateKeys, path);
if (value.reachable !== undefined) {
if (typeof value.reachable !== 'boolean') {
throw new Error(`${pathString([...path, 'reachable'])} must be a boolean.`);
@@ -1048,6 +1145,7 @@ function assertRuntimeContext(value: unknown, path: string[]): asserts value is
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, RuntimeContextKeys, path);
if (value.entrypoint !== undefined) {
if (!Array.isArray(value.entrypoint)) {
throw new Error(`${pathString([...path, 'entrypoint'])} must be an array.`);
@@ -1079,6 +1177,7 @@ function assertSbomAttestation(value: unknown, path: string[]): asserts value is
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, SbomAttestationKeys, path);
if (value.schemaVersion === undefined) {
throw new Error(`${pathString([...path, 'schemaVersion'])} is required.`);
}
@@ -1135,6 +1234,7 @@ function assertSbomPackage(value: unknown, path: string[]): asserts value is Sbo
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, SbomPackageKeys, path);
if (value.purl === undefined) {
throw new Error(`${pathString([...path, 'purl'])} is required.`);
}
@@ -1165,6 +1265,7 @@ function assertScanFinding(value: unknown, path: string[]): asserts value is Sca
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, ScanFindingKeys, path);
if (value.id === undefined) {
throw new Error(`${pathString([...path, 'id'])} is required.`);
}
@@ -1229,6 +1330,7 @@ function assertScanResults(value: unknown, path: string[]): asserts value is Sca
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, ScanResultsKeys, path);
if (value.schemaVersion === undefined) {
throw new Error(`${pathString([...path, 'schemaVersion'])} is required.`);
}
@@ -1280,6 +1382,7 @@ function assertScannerInfo(value: unknown, path: string[]): asserts value is Sca
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, ScannerInfoKeys, path);
if (value.name === undefined) {
throw new Error(`${pathString([...path, 'name'])} is required.`);
}
@@ -1303,6 +1406,7 @@ function assertSmartDiffPredicate(value: unknown, path: string[]): asserts value
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, SmartDiffPredicateKeys, path);
if (value.schemaVersion === undefined) {
throw new Error(`${pathString([...path, 'schemaVersion'])} is required.`);
}
@@ -1357,6 +1461,7 @@ function assertUserContext(value: unknown, path: string[]): asserts value is Use
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, UserContextKeys, path);
if (value.uid !== undefined) {
if (typeof value.uid !== 'number') {
throw new Error(`${pathString([...path, 'uid'])} must be a number.`);
@@ -1389,6 +1494,7 @@ function assertVexAttestation(value: unknown, path: string[]): asserts value is
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, VexAttestationKeys, path);
if (value.schemaVersion === undefined) {
throw new Error(`${pathString([...path, 'schemaVersion'])} is required.`);
}
@@ -1431,6 +1537,7 @@ function assertVexStatement(value: unknown, path: string[]): asserts value is Ve
if (!isRecord(value)) {
throw new Error(`${pathString(path)} must be an object.`);
}
+ assertNoUnknownKeys(value, VexStatementKeys, path);
if (value.vulnerabilityId === undefined) {
throw new Error(`${pathString([...path, 'vulnerabilityId'])} is required.`);
}
@@ -1560,21 +1667,51 @@ export function canonicalizeVexAttestation(value: VexAttestation): string {
}
function canonicalStringify(input: unknown): string {
- return JSON.stringify(sortValue(input));
+ return canonicalizeValue(input);
}
-function sortValue(value: unknown): unknown {
+function canonicalizeValue(value: unknown): string {
+ if (value === null) {
+ return 'null';
+ }
+ if (typeof value === 'string') {
+ return JSON.stringify(value);
+ }
+ if (typeof value === 'number') {
+ return formatNumber(value);
+ }
+ if (typeof value === 'boolean') {
+ return value ? 'true' : 'false';
+ }
if (Array.isArray(value)) {
- return value.map(sortValue);
+ return `[${value.map(canonicalizeValue).join(',')}]`;
}
if (isRecord(value)) {
- const ordered: Record = {};
- const keys = Object.keys(value).sort();
- for (const key of keys) {
- ordered[key] = sortValue(value[key]);
- }
- return ordered;
+ return canonicalizeObject(value);
}
- return value;
+ throw new Error('Unsupported value for canonical JSON.');
+}
+
+function canonicalizeObject(value: Record): string {
+ const keys = Object.keys(value).sort();
+ const entries: string[] = [];
+ for (const key of keys) {
+ const entry = value[key];
+ if (entry === undefined) {
+ continue;
+ }
+ entries.push(`${JSON.stringify(key)}:${canonicalizeValue(entry)}`);
+ }
+ return `{${entries.join(',')}}`;
+}
+
+function formatNumber(value: number): string {
+ if (!Number.isFinite(value)) {
+ throw new Error('Non-finite numbers are not allowed in canonical JSON.');
+ }
+ if (Object.is(value, -0)) {
+ return '0';
+ }
+ return value.toString();
}
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/attestation-common.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/attestation-common.v1.schema.json
index aec3d2d51..78a15f398 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/attestation-common.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/attestation-common.v1.schema.json
@@ -1,372 +1 @@
-{
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://schemas.stella-ops.org/attestations/common/v1",
- "title": "StellaOps Attestation Common Definitions v1",
- "type": "object",
- "description": "Shared schema components reused across StellaOps attestation predicates.",
- "$defs": {
- "schemaVersion": {
- "type": "string",
- "pattern": "^1\\.0\\.\\d+$",
- "description": "Semantic version identifier for predicate schema revisions. Initial release is 1.0.x."
- },
- "digestSet": {
- "type": "object",
- "description": "Map of hashing algorithm to lowercase hexadecimal digest.",
- "minProperties": 1,
- "maxProperties": 4,
- "patternProperties": {
- "^[A-Za-z0-9]+$": {
- "type": "string",
- "pattern": "^[a-f0-9]{32,128}$"
- }
- },
- "additionalProperties": false
- },
- "subject": {
- "type": "object",
- "additionalProperties": false,
- "required": [
- "subjectKind",
- "digest"
- ],
- "properties": {
- "subjectKind": {
- "type": "string",
- "enum": [
- "container-image",
- "sbom",
- "scan-report",
- "policy-report",
- "vex-statement",
- "risk-profile",
- "artifact"
- ]
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 512
- },
- "uri": {
- "type": "string",
- "format": "uri"
- },
- "digest": {
- "$ref": "#/$defs/digestSet"
- },
- "annotations": {
- "type": "object",
- "additionalProperties": {
- "type": "string",
- "maxLength": 256
- }
- },
- "imageDigest": {
- "type": "string",
- "pattern": "^sha256:[a-f0-9]{64}$"
- },
- "mediaType": {
- "type": "string",
- "maxLength": 128
- },
- "sizeBytes": {
- "type": "integer",
- "minimum": 0
- }
- }
- },
- "subjectList": {
- "type": "array",
- "minItems": 1,
- "uniqueItems": true,
- "items": {
- "$ref": "#/$defs/subject"
- }
- },
- "issuer": {
- "type": "object",
- "description": "Identity metadata describing the signer of the attestation predicate.",
- "additionalProperties": false,
- "required": [
- "issuerType",
- "id",
- "signingKey"
- ],
- "properties": {
- "issuerType": {
- "type": "string",
- "enum": [
- "service",
- "user",
- "automation",
- "device"
- ]
- },
- "id": {
- "type": "string",
- "minLength": 4,
- "maxLength": 256
- },
- "tenantId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128
- },
- "displayName": {
- "type": "string",
- "maxLength": 256
- },
- "email": {
- "type": "string",
- "format": "email"
- },
- "workload": {
- "type": "object",
- "additionalProperties": false,
- "required": [
- "service"
- ],
- "properties": {
- "service": {
- "type": "string",
- "maxLength": 128
- },
- "cluster": {
- "type": "string",
- "maxLength": 128
- },
- "namespace": {
- "type": "string",
- "maxLength": 128
- },
- "region": {
- "type": "string",
- "maxLength": 64
- }
- }
- },
- "signingKey": {
- "type": "object",
- "additionalProperties": false,
- "required": [
- "keyId",
- "mode",
- "algorithm"
- ],
- "properties": {
- "keyId": {
- "type": "string",
- "maxLength": 256
- },
- "mode": {
- "type": "string",
- "enum": [
- "keyless",
- "kms",
- "hsm",
- "fido2",
- "offline"
- ]
- },
- "algorithm": {
- "type": "string",
- "maxLength": 64
- },
- "issuer": {
- "type": "string",
- "maxLength": 256
- },
- "certificateChain": {
- "type": "array",
- "maxItems": 5,
- "items": {
- "type": "string",
- "minLength": 1
- }
- },
- "proof": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "fulcioIdentity": {
- "type": "string",
- "maxLength": 256
- },
- "hardwareClass": {
- "type": "string",
- "maxLength": 128
- }
- }
- }
- }
- }
- }
- },
- "material": {
- "type": "object",
- "additionalProperties": false,
- "required": [
- "uri"
- ],
- "properties": {
- "uri": {
- "type": "string",
- "minLength": 1,
- "maxLength": 512
- },
- "digest": {
- "$ref": "#/$defs/digestSet"
- },
- "mediaType": {
- "type": "string",
- "maxLength": 128
- },
- "role": {
- "type": "string",
- "maxLength": 64
- },
- "annotations": {
- "type": "object",
- "additionalProperties": {
- "type": "string",
- "maxLength": 128
- }
- }
- }
- },
- "transparencyLog": {
- "type": "object",
- "additionalProperties": false,
- "required": [
- "logId",
- "logUrl",
- "uuid"
- ],
- "properties": {
- "logId": {
- "type": "string",
- "maxLength": 128
- },
- "logUrl": {
- "type": "string",
- "format": "uri"
- },
- "uuid": {
- "type": "string",
- "maxLength": 128
- },
- "index": {
- "type": "integer",
- "minimum": 0
- },
- "checkpoint": {
- "type": "object",
- "additionalProperties": false,
- "required": [
- "origin",
- "size",
- "rootHash",
- "timestamp"
- ],
- "properties": {
- "origin": {
- "type": "string",
- "maxLength": 128
- },
- "size": {
- "type": "integer",
- "minimum": 0
- },
- "rootHash": {
- "type": "string",
- "pattern": "^[A-Za-z0-9\\+/=]{16,128}$"
- },
- "timestamp": {
- "type": "string",
- "format": "date-time"
- }
- }
- },
- "witnessed": {
- "type": "boolean"
- }
- }
- },
- "transparencyLogList": {
- "type": "array",
- "items": {
- "$ref": "#/$defs/transparencyLog"
- }
- },
- "policyContext": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "policyId": {
- "type": "string",
- "maxLength": 128
- },
- "policyVersion": {
- "type": "string",
- "maxLength": 32
- },
- "revisionDigest": {
- "$ref": "#/$defs/digestSet"
- },
- "mode": {
- "type": "string",
- "enum": [
- "enforce",
- "dry-run"
- ]
- }
- }
- },
- "vexStatus": {
- "type": "string",
- "enum": [
- "not_affected",
- "affected",
- "fixed",
- "under_investigation"
- ]
- },
- "severity": {
- "type": "string",
- "enum": [
- "critical",
- "high",
- "medium",
- "low",
- "informational"
- ]
- },
- "explainReference": {
- "type": "object",
- "additionalProperties": false,
- "required": [
- "id",
- "type"
- ],
- "properties": {
- "id": {
- "type": "string",
- "maxLength": 128
- },
- "type": {
- "type": "string",
- "enum": [
- "rule",
- "step",
- "binding"
- ]
- },
- "message": {
- "type": "string",
- "maxLength": 2048
- }
- }
- }
- }
-}
+{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.stella-ops.org/attestations/common/v1", "title": "StellaOps Attestation Common Definitions v1", "type": "object", "description": "Shared schema components reused across StellaOps attestation predicates.", "$defs": { "schemaVersion": { "type": "string", "pattern": "^1\\.0\\.\\d+$", "description": "Semantic version identifier for predicate schema revisions. Initial release is 1.0.x." }, "digestSet": { "type": "object", "description": "Map of hashing algorithm to lowercase hexadecimal digest.", "minProperties": 1, "maxProperties": 4, "patternProperties": { "^[A-Za-z0-9]+$": { "type": "string", "pattern": "^[a-f0-9]{32,128}$" } }, "additionalProperties": false }, "subject": { "type": "object", "additionalProperties": false, "required": [ "subjectKind", "digest" ], "properties": { "subjectKind": { "type": "string", "enum": [ "container-image", "sbom", "scan-report", "policy-report", "vex-statement", "risk-profile", "artifact" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 512 }, "uri": { "type": "string", "format": "uri" }, "digest": { "$ref": "#/$defs/digestSet" }, "annotations": { "type": "object", "additionalProperties": { "type": "string", "maxLength": 256 } }, "imageDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, "mediaType": { "type": "string", "maxLength": 128 }, "sizeBytes": { "type": "integer", "minimum": 0 } } }, "subjectList": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/subject" } }, "issuer": { "type": "object", "description": "Identity metadata describing the signer of the attestation predicate.", "additionalProperties": false, "required": [ "issuerType", "id", "signingKey" ], "properties": { "issuerType": { "type": "string", "enum": [ "service", "user", "automation", "device" ] }, "id": { "type": "string", "minLength": 4, "maxLength": 256 }, "tenantId": { "type": "string", "minLength": 1, "maxLength": 128 }, "displayName": { "type": "string", "maxLength": 256 }, "email": { "type": "string", "format": "email" }, "workload": { "type": "object", "additionalProperties": false, "required": [ "service" ], "properties": { "service": { "type": "string", "maxLength": 128 }, "cluster": { "type": "string", "maxLength": 128 }, "namespace": { "type": "string", "maxLength": 128 }, "region": { "type": "string", "maxLength": 64 } } }, "signingKey": { "type": "object", "additionalProperties": false, "required": [ "keyId", "mode", "algorithm" ], "properties": { "keyId": { "type": "string", "maxLength": 256 }, "mode": { "type": "string", "enum": [ "keyless", "kms", "hsm", "fido2", "offline" ] }, "algorithm": { "type": "string", "maxLength": 64 }, "issuer": { "type": "string", "maxLength": 256 }, "certificateChain": { "type": "array", "maxItems": 5, "items": { "type": "string", "minLength": 1 } }, "proof": { "type": "object", "additionalProperties": false, "properties": { "fulcioIdentity": { "type": "string", "maxLength": 256 }, "hardwareClass": { "type": "string", "maxLength": 128 } } } } } } }, "material": { "type": "object", "additionalProperties": false, "required": [ "uri" ], "properties": { "uri": { "type": "string", "minLength": 1, "maxLength": 512 }, "digest": { "$ref": "#/$defs/digestSet" }, "mediaType": { "type": "string", "maxLength": 128 }, "role": { "type": "string", "maxLength": 64 }, "annotations": { "type": "object", "additionalProperties": { "type": "string", "maxLength": 128 } } } }, "transparencyLog": { "type": "object", "additionalProperties": false, "required": [ "logId", "logUrl", "uuid" ], "properties": { "logId": { "type": "string", "maxLength": 128 }, "logUrl": { "type": "string", "format": "uri" }, "uuid": { "type": "string", "maxLength": 128 }, "index": { "type": "integer", "minimum": 0 }, "checkpoint": { "type": "object", "additionalProperties": false, "required": [ "origin", "size", "rootHash", "timestamp" ], "properties": { "origin": { "type": "string", "maxLength": 128 }, "size": { "type": "integer", "minimum": 0 }, "rootHash": { "type": "string", "pattern": "^[A-Za-z0-9\\+/=]{16,128}$" }, "timestamp": { "type": "string", "format": "date-time" } } }, "witnessed": { "type": "boolean" } } }, "transparencyLogList": { "type": "array", "items": { "$ref": "#/$defs/transparencyLog" } }, "policyContext": { "type": "object", "additionalProperties": false, "properties": { "policyId": { "type": "string", "maxLength": 128 }, "policyVersion": { "type": "string", "maxLength": 32 }, "revisionDigest": { "$ref": "#/$defs/digestSet" }, "mode": { "type": "string", "enum": [ "enforce", "dry-run" ] } } }, "vexStatus": { "type": "string", "enum": [ "not_affected", "affected", "fixed", "under_investigation" ] }, "severity": { "type": "string", "enum": [ "critical", "high", "medium", "low", "informational" ] }, "explainReference": { "type": "object", "additionalProperties": false, "required": [ "id", "type" ], "properties": { "id": { "type": "string", "maxLength": 128 }, "type": { "type": "string", "enum": [ "rule", "step", "binding" ] }, "message": { "type": "string", "maxLength": 2048 } } } } }
\ No newline at end of file
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-build-provenance.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-build-provenance.v1.schema.json
index 8c770c7a3..575269b44 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-build-provenance.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-build-provenance.v1.schema.json
@@ -1,6 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://stella-ops.org/schemas/attestor/stellaops-build-provenance.v1.json",
+ "$id": "https://stella-ops.org/schemas/attestor/stellaops-build-provenance.v1.schema.json",
+ "$comment": "Generated by StellaOps.Attestor.Types.Generator.",
"title": "Build provenance evidence capturing builder inputs and outputs.",
"type": "object",
"additionalProperties": false,
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-custom-evidence.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-custom-evidence.v1.schema.json
index e218134c4..20909d061 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-custom-evidence.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-custom-evidence.v1.schema.json
@@ -1,6 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://stella-ops.org/schemas/attestor/stellaops-custom-evidence.v1.json",
+ "$id": "https://stella-ops.org/schemas/attestor/stellaops-custom-evidence.v1.schema.json",
+ "$comment": "Generated by StellaOps.Attestor.Types.Generator.",
"title": "Generic evidence payload for bespoke attestations.",
"type": "object",
"additionalProperties": false,
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-policy-evaluation.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-policy-evaluation.v1.schema.json
index 506ea70c5..3de168bd0 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-policy-evaluation.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-policy-evaluation.v1.schema.json
@@ -1,6 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://stella-ops.org/schemas/attestor/stellaops-policy-evaluation.v1.json",
+ "$id": "https://stella-ops.org/schemas/attestor/stellaops-policy-evaluation.v1.schema.json",
+ "$comment": "Generated by StellaOps.Attestor.Types.Generator.",
"title": "Policy evaluation outcome for an artifact.",
"type": "object",
"additionalProperties": false,
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-risk-profile.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-risk-profile.v1.schema.json
index 4a5f6bfed..285be49e3 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-risk-profile.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-risk-profile.v1.schema.json
@@ -1,6 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://stella-ops.org/schemas/attestor/stellaops-risk-profile.v1.json",
+ "$id": "https://stella-ops.org/schemas/attestor/stellaops-risk-profile.v1.schema.json",
+ "$comment": "Generated by StellaOps.Attestor.Types.Generator.",
"title": "Risk scoring evidence summarising exposure for an artifact.",
"type": "object",
"additionalProperties": false,
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-sbom-attestation.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-sbom-attestation.v1.schema.json
index c7a10cc58..9fe6ebd5b 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-sbom-attestation.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-sbom-attestation.v1.schema.json
@@ -1,6 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://stella-ops.org/schemas/attestor/stellaops-sbom-attestation.v1.json",
+ "$id": "https://stella-ops.org/schemas/attestor/stellaops-sbom-attestation.v1.schema.json",
+ "$comment": "Generated by StellaOps.Attestor.Types.Generator.",
"title": "SBOM attestation linking an SBOM document to an artifact.",
"type": "object",
"additionalProperties": false,
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-scan-results.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-scan-results.v1.schema.json
index cc04c797e..0e826ac46 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-scan-results.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-scan-results.v1.schema.json
@@ -1,6 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://stella-ops.org/schemas/attestor/stellaops-scan-results.v1.json",
+ "$id": "https://stella-ops.org/schemas/attestor/stellaops-scan-results.v1.schema.json",
+ "$comment": "Generated by StellaOps.Attestor.Types.Generator.",
"title": "Scanner findings for an artifact at a point in time.",
"type": "object",
"additionalProperties": false,
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-smart-diff.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-smart-diff.v1.schema.json
index e157f1d4a..3866c3da2 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-smart-diff.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-smart-diff.v1.schema.json
@@ -1,6 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://stella-ops.org/schemas/attestor/stellaops-smart-diff.v1.json",
+ "$id": "https://stella-ops.org/schemas/attestor/stellaops-smart-diff.v1.schema.json",
+ "$comment": "Generated by StellaOps.Attestor.Types.Generator.",
"title": "Smart-Diff predicate describing differential analysis between two scans.",
"type": "object",
"additionalProperties": false,
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-vex-attestation.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-vex-attestation.v1.schema.json
index 6edb2b271..af2864655 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-vex-attestation.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-vex-attestation.v1.schema.json
@@ -1,6 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://stella-ops.org/schemas/attestor/stellaops-vex-attestation.v1.json",
+ "$id": "https://stella-ops.org/schemas/attestor/stellaops-vex-attestation.v1.schema.json",
+ "$comment": "Generated by StellaOps.Attestor.Types.Generator.",
"title": "VEX attestation describing vulnerability status for an artifact.",
"type": "object",
"additionalProperties": false,
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/uncertainty-budget-statement.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/uncertainty-budget-statement.v1.schema.json
index a18d59414..e1ba41bef 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/uncertainty-budget-statement.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/uncertainty-budget-statement.v1.schema.json
@@ -1,187 +1 @@
-{
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://stella-ops.org/schemas/attestation/uncertainty-budget-statement.v1.json",
- "title": "Uncertainty Budget Statement",
- "description": "In-toto predicate type for uncertainty budget evaluation attestations. Sprint: SPRINT_4300_0002_0002 (UATT-007).",
- "type": "object",
- "required": ["_type", "subject", "predicateType", "predicate"],
- "properties": {
- "_type": {
- "type": "string",
- "const": "https://in-toto.io/Statement/v1"
- },
- "subject": {
- "type": "array",
- "minItems": 1,
- "items": {
- "type": "object",
- "required": ["digest"],
- "properties": {
- "name": {
- "type": "string",
- "description": "Subject identifier (e.g., environment name or image reference)"
- },
- "digest": {
- "type": "object",
- "description": "Cryptographic digest of the subject",
- "additionalProperties": {
- "type": "string",
- "pattern": "^[a-fA-F0-9]+$"
- }
- }
- }
- }
- },
- "predicateType": {
- "type": "string",
- "const": "uncertainty-budget.stella/v1"
- },
- "predicate": {
- "$ref": "#/$defs/UncertaintyBudgetPredicate"
- }
- },
- "$defs": {
- "UncertaintyBudgetPredicate": {
- "type": "object",
- "required": ["environment", "isWithinBudget", "action", "totalUnknowns", "evaluatedAt"],
- "properties": {
- "environment": {
- "type": "string",
- "description": "Environment against which budget was evaluated (e.g., production, staging)"
- },
- "isWithinBudget": {
- "type": "boolean",
- "description": "Whether the evaluation passed the budget check"
- },
- "action": {
- "type": "string",
- "enum": ["pass", "warn", "block"],
- "description": "Recommended action based on budget evaluation"
- },
- "totalUnknowns": {
- "type": "integer",
- "minimum": 0,
- "description": "Total count of unknowns in evaluation"
- },
- "totalLimit": {
- "type": "integer",
- "minimum": 0,
- "description": "Configured total unknown limit for this environment"
- },
- "percentageUsed": {
- "type": "number",
- "minimum": 0,
- "maximum": 100,
- "description": "Percentage of budget consumed"
- },
- "violationCount": {
- "type": "integer",
- "minimum": 0,
- "description": "Number of budget rule violations"
- },
- "violations": {
- "type": "array",
- "description": "Detailed violation information",
- "items": {
- "$ref": "#/$defs/BudgetViolation"
- }
- },
- "budget": {
- "$ref": "#/$defs/BudgetDefinition",
- "description": "Budget definition that was applied"
- },
- "message": {
- "type": "string",
- "description": "Human-readable budget status message"
- },
- "evaluatedAt": {
- "type": "string",
- "format": "date-time",
- "description": "ISO-8601 timestamp of budget evaluation"
- },
- "policyRevisionId": {
- "type": "string",
- "description": "Policy revision ID containing the budget rules"
- },
- "imageDigest": {
- "type": "string",
- "pattern": "^sha256:[a-fA-F0-9]{64}$",
- "description": "Optional container image digest"
- },
- "uncertaintyStatementId": {
- "type": "string",
- "description": "Reference to the linked uncertainty statement attestation ID"
- }
- }
- },
- "BudgetViolation": {
- "type": "object",
- "required": ["reasonCode", "count", "limit"],
- "properties": {
- "reasonCode": {
- "type": "string",
- "enum": ["U-RCH", "U-ID", "U-PROV", "U-VEX", "U-FEED", "U-CONFIG", "U-ANALYZER"],
- "description": "Unknown reason code that violated the budget"
- },
- "count": {
- "type": "integer",
- "minimum": 0,
- "description": "Actual count of unknowns for this reason"
- },
- "limit": {
- "type": "integer",
- "minimum": 0,
- "description": "Configured limit for this reason"
- },
- "severity": {
- "type": "string",
- "enum": ["low", "medium", "high", "critical"],
- "description": "Severity of the violation"
- }
- }
- },
- "BudgetDefinition": {
- "type": "object",
- "required": ["name", "environment"],
- "properties": {
- "name": {
- "type": "string",
- "description": "Budget rule name"
- },
- "environment": {
- "type": "string",
- "description": "Target environment"
- },
- "totalLimit": {
- "type": "integer",
- "minimum": 0,
- "description": "Total unknown limit"
- },
- "tierMax": {
- "type": "string",
- "enum": ["T1", "T2", "T3", "T4"],
- "description": "Maximum allowed uncertainty tier"
- },
- "entropyMax": {
- "type": "number",
- "minimum": 0,
- "maximum": 1,
- "description": "Maximum allowed mean entropy"
- },
- "reasonLimits": {
- "type": "object",
- "description": "Per-reason-code limits",
- "additionalProperties": {
- "type": "integer",
- "minimum": 0
- }
- },
- "action": {
- "type": "string",
- "enum": ["warn", "block", "warnUnlessException"],
- "description": "Action to take when budget is exceeded"
- }
- }
- }
- }
-}
+{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://stella-ops.org/schemas/attestation/uncertainty-budget-statement.v1.json", "title": "Uncertainty Budget Statement", "description": "In-toto predicate type for uncertainty budget evaluation attestations. Sprint: SPRINT_4300_0002_0002 (UATT-007).", "type": "object", "required": ["_type", "subject", "predicateType", "predicate"], "properties": { "_type": { "type": "string", "const": "https://in-toto.io/Statement/v1" }, "subject": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["digest"], "properties": { "name": { "type": "string", "description": "Subject identifier (e.g., environment name or image reference)" }, "digest": { "type": "object", "description": "Cryptographic digest of the subject", "additionalProperties": { "type": "string", "pattern": "^[a-fA-F0-9]+$" } } } } }, "predicateType": { "type": "string", "const": "uncertainty-budget.stella/v1" }, "predicate": { "$ref": "#/$defs/UncertaintyBudgetPredicate" } }, "$defs": { "UncertaintyBudgetPredicate": { "type": "object", "required": ["environment", "isWithinBudget", "action", "totalUnknowns", "evaluatedAt"], "properties": { "environment": { "type": "string", "description": "Environment against which budget was evaluated (e.g., production, staging)" }, "isWithinBudget": { "type": "boolean", "description": "Whether the evaluation passed the budget check" }, "action": { "type": "string", "enum": ["pass", "warn", "block"], "description": "Recommended action based on budget evaluation" }, "totalUnknowns": { "type": "integer", "minimum": 0, "description": "Total count of unknowns in evaluation" }, "totalLimit": { "type": "integer", "minimum": 0, "description": "Configured total unknown limit for this environment" }, "percentageUsed": { "type": "number", "minimum": 0, "maximum": 100, "description": "Percentage of budget consumed" }, "violationCount": { "type": "integer", "minimum": 0, "description": "Number of budget rule violations" }, "violations": { "type": "array", "description": "Detailed violation information", "items": { "$ref": "#/$defs/BudgetViolation" } }, "budget": { "$ref": "#/$defs/BudgetDefinition", "description": "Budget definition that was applied" }, "message": { "type": "string", "description": "Human-readable budget status message" }, "evaluatedAt": { "type": "string", "format": "date-time", "description": "ISO-8601 timestamp of budget evaluation" }, "policyRevisionId": { "type": "string", "description": "Policy revision ID containing the budget rules" }, "imageDigest": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Optional container image digest" }, "uncertaintyStatementId": { "type": "string", "description": "Reference to the linked uncertainty statement attestation ID" } } }, "BudgetViolation": { "type": "object", "required": ["reasonCode", "count", "limit"], "properties": { "reasonCode": { "type": "string", "enum": ["U-RCH", "U-ID", "U-PROV", "U-VEX", "U-FEED", "U-CONFIG", "U-ANALYZER"], "description": "Unknown reason code that violated the budget" }, "count": { "type": "integer", "minimum": 0, "description": "Actual count of unknowns for this reason" }, "limit": { "type": "integer", "minimum": 0, "description": "Configured limit for this reason" }, "severity": { "type": "string", "enum": ["low", "medium", "high", "critical"], "description": "Severity of the violation" } } }, "BudgetDefinition": { "type": "object", "required": ["name", "environment"], "properties": { "name": { "type": "string", "description": "Budget rule name" }, "environment": { "type": "string", "description": "Target environment" }, "totalLimit": { "type": "integer", "minimum": 0, "description": "Total unknown limit" }, "tierMax": { "type": "string", "enum": ["T1", "T2", "T3", "T4"], "description": "Maximum allowed uncertainty tier" }, "entropyMax": { "type": "number", "minimum": 0, "maximum": 1, "description": "Maximum allowed mean entropy" }, "reasonLimits": { "type": "object", "description": "Per-reason-code limits", "additionalProperties": { "type": "integer", "minimum": 0 } }, "action": { "type": "string", "enum": ["warn", "block", "warnUnlessException"], "description": "Action to take when budget is exceeded" } } } } }
\ No newline at end of file
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/uncertainty-statement.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/uncertainty-statement.v1.schema.json
index 1709a92e1..b99826ec3 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/uncertainty-statement.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/uncertainty-statement.v1.schema.json
@@ -1,119 +1 @@
-{
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://stella-ops.org/schemas/attestation/uncertainty-statement.v1.json",
- "title": "Uncertainty Statement",
- "description": "In-toto predicate type for uncertainty state attestations. Sprint: SPRINT_4300_0002_0002 (UATT-007).",
- "type": "object",
- "required": ["_type", "subject", "predicateType", "predicate"],
- "properties": {
- "_type": {
- "type": "string",
- "const": "https://in-toto.io/Statement/v1"
- },
- "subject": {
- "type": "array",
- "minItems": 1,
- "items": {
- "type": "object",
- "required": ["digest"],
- "properties": {
- "name": {
- "type": "string",
- "description": "Subject identifier (e.g., SBOM file name or image reference)"
- },
- "digest": {
- "type": "object",
- "description": "Cryptographic digest of the subject",
- "additionalProperties": {
- "type": "string",
- "pattern": "^[a-fA-F0-9]+$"
- }
- }
- }
- }
- },
- "predicateType": {
- "type": "string",
- "const": "uncertainty.stella/v1"
- },
- "predicate": {
- "$ref": "#/$defs/UncertaintyPredicate"
- }
- },
- "$defs": {
- "UncertaintyPredicate": {
- "type": "object",
- "required": ["graphRevisionId", "aggregateTier", "meanEntropy", "unknownCount", "evaluatedAt"],
- "properties": {
- "graphRevisionId": {
- "type": "string",
- "description": "Unique identifier for the knowledge graph revision used in evaluation"
- },
- "aggregateTier": {
- "type": "string",
- "enum": ["T1", "T2", "T3", "T4"],
- "description": "Aggregate uncertainty tier (T1 = highest uncertainty, T4 = lowest)"
- },
- "meanEntropy": {
- "type": "number",
- "minimum": 0,
- "maximum": 1,
- "description": "Mean entropy across all unknowns (0.0 = certain, 1.0 = maximum uncertainty)"
- },
- "unknownCount": {
- "type": "integer",
- "minimum": 0,
- "description": "Total count of unknowns in this evaluation"
- },
- "markers": {
- "type": "array",
- "description": "Breakdown of unknowns by marker kind",
- "items": {
- "$ref": "#/$defs/UnknownMarker"
- }
- },
- "evaluatedAt": {
- "type": "string",
- "format": "date-time",
- "description": "ISO-8601 timestamp of uncertainty evaluation"
- },
- "policyRevisionId": {
- "type": "string",
- "description": "Optional policy revision ID if uncertainty was evaluated with policy"
- },
- "imageDigest": {
- "type": "string",
- "pattern": "^sha256:[a-fA-F0-9]{64}$",
- "description": "Optional container image digest"
- }
- }
- },
- "UnknownMarker": {
- "type": "object",
- "required": ["kind", "count", "entropy"],
- "properties": {
- "kind": {
- "type": "string",
- "enum": ["U-RCH", "U-ID", "U-PROV", "U-VEX", "U-FEED", "U-CONFIG", "U-ANALYZER"],
- "description": "Unknown marker kind code"
- },
- "count": {
- "type": "integer",
- "minimum": 0,
- "description": "Count of unknowns with this marker"
- },
- "entropy": {
- "type": "number",
- "minimum": 0,
- "maximum": 1,
- "description": "Mean entropy for this marker kind"
- },
- "tier": {
- "type": "string",
- "enum": ["T1", "T2", "T3", "T4"],
- "description": "Uncertainty tier for this marker kind"
- }
- }
- }
- }
-}
+{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://stella-ops.org/schemas/attestation/uncertainty-statement.v1.json", "title": "Uncertainty Statement", "description": "In-toto predicate type for uncertainty state attestations. Sprint: SPRINT_4300_0002_0002 (UATT-007).", "type": "object", "required": ["_type", "subject", "predicateType", "predicate"], "properties": { "_type": { "type": "string", "const": "https://in-toto.io/Statement/v1" }, "subject": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["digest"], "properties": { "name": { "type": "string", "description": "Subject identifier (e.g., SBOM file name or image reference)" }, "digest": { "type": "object", "description": "Cryptographic digest of the subject", "additionalProperties": { "type": "string", "pattern": "^[a-fA-F0-9]+$" } } } } }, "predicateType": { "type": "string", "const": "uncertainty.stella/v1" }, "predicate": { "$ref": "#/$defs/UncertaintyPredicate" } }, "$defs": { "UncertaintyPredicate": { "type": "object", "required": ["graphRevisionId", "aggregateTier", "meanEntropy", "unknownCount", "evaluatedAt"], "properties": { "graphRevisionId": { "type": "string", "description": "Unique identifier for the knowledge graph revision used in evaluation" }, "aggregateTier": { "type": "string", "enum": ["T1", "T2", "T3", "T4"], "description": "Aggregate uncertainty tier (T1 = highest uncertainty, T4 = lowest)" }, "meanEntropy": { "type": "number", "minimum": 0, "maximum": 1, "description": "Mean entropy across all unknowns (0.0 = certain, 1.0 = maximum uncertainty)" }, "unknownCount": { "type": "integer", "minimum": 0, "description": "Total count of unknowns in this evaluation" }, "markers": { "type": "array", "description": "Breakdown of unknowns by marker kind", "items": { "$ref": "#/$defs/UnknownMarker" } }, "evaluatedAt": { "type": "string", "format": "date-time", "description": "ISO-8601 timestamp of uncertainty evaluation" }, "policyRevisionId": { "type": "string", "description": "Optional policy revision ID if uncertainty was evaluated with policy" }, "imageDigest": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Optional container image digest" } } }, "UnknownMarker": { "type": "object", "required": ["kind", "count", "entropy"], "properties": { "kind": { "type": "string", "enum": ["U-RCH", "U-ID", "U-PROV", "U-VEX", "U-FEED", "U-CONFIG", "U-ANALYZER"], "description": "Unknown marker kind code" }, "count": { "type": "integer", "minimum": 0, "description": "Count of unknowns with this marker" }, "entropy": { "type": "number", "minimum": 0, "maximum": 1, "description": "Mean entropy for this marker kind" }, "tier": { "type": "string", "enum": ["T1", "T2", "T3", "T4"], "description": "Uncertainty tier for this marker kind" } } } } }
\ No newline at end of file
diff --git a/src/Attestor/StellaOps.Attestor.Types/schemas/verification-policy.v1.schema.json b/src/Attestor/StellaOps.Attestor.Types/schemas/verification-policy.v1.schema.json
index 554869dc5..f81c414c7 100644
--- a/src/Attestor/StellaOps.Attestor.Types/schemas/verification-policy.v1.schema.json
+++ b/src/Attestor/StellaOps.Attestor.Types/schemas/verification-policy.v1.schema.json
@@ -1,151 +1 @@
-{
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://stellaops.io/schemas/verification-policy.v1.json",
- "title": "VerificationPolicy",
- "description": "Attestation verification policy configuration for StellaOps",
- "type": "object",
- "required": ["policyId", "version", "predicateTypes", "signerRequirements"],
- "properties": {
- "policyId": {
- "type": "string",
- "description": "Unique policy identifier",
- "pattern": "^[a-z0-9-]+$",
- "examples": ["default-verification-policy", "strict-slsa-policy"]
- },
- "version": {
- "type": "string",
- "description": "Policy version (SemVer)",
- "pattern": "^\\d+\\.\\d+\\.\\d+$",
- "examples": ["1.0.0", "2.1.0"]
- },
- "description": {
- "type": "string",
- "description": "Human-readable policy description"
- },
- "tenantScope": {
- "type": "string",
- "description": "Tenant ID this policy applies to, or '*' for all tenants",
- "default": "*"
- },
- "predicateTypes": {
- "type": "array",
- "description": "Allowed attestation predicate types",
- "items": {
- "type": "string"
- },
- "minItems": 1,
- "examples": [
- ["stella.ops/sbom@v1", "stella.ops/vex@v1"]
- ]
- },
- "signerRequirements": {
- "$ref": "#/$defs/SignerRequirements"
- },
- "validityWindow": {
- "$ref": "#/$defs/ValidityWindow"
- },
- "metadata": {
- "type": "object",
- "description": "Free-form metadata",
- "additionalProperties": true
- }
- },
- "$defs": {
- "SignerRequirements": {
- "type": "object",
- "description": "Requirements for attestation signers",
- "properties": {
- "minimumSignatures": {
- "type": "integer",
- "minimum": 1,
- "default": 1,
- "description": "Minimum number of valid signatures required"
- },
- "trustedKeyFingerprints": {
- "type": "array",
- "items": {
- "type": "string",
- "pattern": "^sha256:[a-f0-9]{64}$"
- },
- "description": "List of trusted signer key fingerprints (SHA-256)"
- },
- "trustedIssuers": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- },
- "description": "List of trusted issuer identities (OIDC issuers)"
- },
- "requireRekor": {
- "type": "boolean",
- "default": false,
- "description": "Require Sigstore Rekor transparency log entry"
- },
- "algorithms": {
- "type": "array",
- "items": {
- "type": "string",
- "enum": ["ES256", "ES384", "ES512", "RS256", "RS384", "RS512", "EdDSA"]
- },
- "description": "Allowed signing algorithms",
- "default": ["ES256", "RS256", "EdDSA"]
- }
- }
- },
- "ValidityWindow": {
- "type": "object",
- "description": "Time-based validity constraints",
- "properties": {
- "notBefore": {
- "type": "string",
- "format": "date-time",
- "description": "Policy not valid before this time (ISO-8601)"
- },
- "notAfter": {
- "type": "string",
- "format": "date-time",
- "description": "Policy not valid after this time (ISO-8601)"
- },
- "maxAttestationAge": {
- "type": "integer",
- "minimum": 0,
- "description": "Maximum age of attestation in seconds (0 = no limit)"
- }
- }
- }
- },
- "examples": [
- {
- "policyId": "default-verification-policy",
- "version": "1.0.0",
- "description": "Default verification policy for StellaOps attestations",
- "tenantScope": "*",
- "predicateTypes": [
- "stella.ops/sbom@v1",
- "stella.ops/vex@v1",
- "stella.ops/vexDecision@v1",
- "stella.ops/policy@v1",
- "stella.ops/promotion@v1",
- "stella.ops/evidence@v1",
- "stella.ops/graph@v1",
- "stella.ops/replay@v1",
- "https://slsa.dev/provenance/v1",
- "https://cyclonedx.org/bom",
- "https://spdx.dev/Document",
- "https://openvex.dev/ns"
- ],
- "signerRequirements": {
- "minimumSignatures": 1,
- "trustedKeyFingerprints": [
- "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
- ],
- "requireRekor": false,
- "algorithms": ["ES256", "RS256", "EdDSA"]
- },
- "validityWindow": {
- "maxAttestationAge": 86400
- }
- }
- ]
-}
+{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://stellaops.io/schemas/verification-policy.v1.json", "title": "VerificationPolicy", "description": "Attestation verification policy configuration for StellaOps", "type": "object", "required": ["policyId", "version", "predicateTypes", "signerRequirements"], "properties": { "policyId": { "type": "string", "description": "Unique policy identifier", "pattern": "^[a-z0-9-]+$", "examples": ["default-verification-policy", "strict-slsa-policy"] }, "version": { "type": "string", "description": "Policy version (SemVer)", "pattern": "^\\d+\\.\\d+\\.\\d+$", "examples": ["1.0.0", "2.1.0"] }, "description": { "type": "string", "description": "Human-readable policy description" }, "tenantScope": { "type": "string", "description": "Tenant ID this policy applies to, or '*' for all tenants", "default": "*" }, "predicateTypes": { "type": "array", "description": "Allowed attestation predicate types", "items": { "type": "string" }, "minItems": 1, "examples": [ ["stella.ops/sbom@v1", "stella.ops/vex@v1"] ] }, "signerRequirements": { "$ref": "#/$defs/SignerRequirements" }, "validityWindow": { "$ref": "#/$defs/ValidityWindow" }, "metadata": { "type": "object", "description": "Free-form metadata", "additionalProperties": true } }, "$defs": { "SignerRequirements": { "type": "object", "description": "Requirements for attestation signers", "properties": { "minimumSignatures": { "type": "integer", "minimum": 1, "default": 1, "description": "Minimum number of valid signatures required" }, "trustedKeyFingerprints": { "type": "array", "items": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, "description": "List of trusted signer key fingerprints (SHA-256)" }, "trustedIssuers": { "type": "array", "items": { "type": "string", "format": "uri" }, "description": "List of trusted issuer identities (OIDC issuers)" }, "requireRekor": { "type": "boolean", "default": false, "description": "Require Sigstore Rekor transparency log entry" }, "algorithms": { "type": "array", "items": { "type": "string", "enum": ["ES256", "ES384", "ES512", "RS256", "RS384", "RS512", "EdDSA"] }, "description": "Allowed signing algorithms", "default": ["ES256", "RS256", "EdDSA"] } } }, "ValidityWindow": { "type": "object", "description": "Time-based validity constraints", "properties": { "notBefore": { "type": "string", "format": "date-time", "description": "Policy not valid before this time (ISO-8601)" }, "notAfter": { "type": "string", "format": "date-time", "description": "Policy not valid after this time (ISO-8601)" }, "maxAttestationAge": { "type": "integer", "minimum": 0, "description": "Maximum age of attestation in seconds (0 = no limit)" } } } }, "examples": [ { "policyId": "default-verification-policy", "version": "1.0.0", "description": "Default verification policy for StellaOps attestations", "tenantScope": "*", "predicateTypes": [ "stella.ops/sbom@v1", "stella.ops/vex@v1", "stella.ops/vexDecision@v1", "stella.ops/policy@v1", "stella.ops/promotion@v1", "stella.ops/evidence@v1", "stella.ops/graph@v1", "stella.ops/replay@v1", "https://slsa.dev/provenance/v1", "https://cyclonedx.org/bom", "https://spdx.dev/Document", "https://openvex.dev/ns" ], "signerRequirements": { "minimumSignatures": 1, "trustedKeyFingerprints": [ "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" ], "requireRekor": false, "algorithms": ["ES256", "RS256", "EdDSA"] }, "validityWindow": { "maxAttestationAge": 86400 } } ] }
\ No newline at end of file
diff --git a/src/Attestor/StellaOps.Attestor.Verify/AttestorVerificationEngine.cs b/src/Attestor/StellaOps.Attestor.Verify/AttestorVerificationEngine.cs
index 35d9e186e..b5c1f6837 100644
--- a/src/Attestor/StellaOps.Attestor.Verify/AttestorVerificationEngine.cs
+++ b/src/Attestor/StellaOps.Attestor.Verify/AttestorVerificationEngine.cs
@@ -1,7 +1,8 @@
-using System.Buffers.Binary;
using System.Collections.Immutable;
+using System.Formats.Asn1;
using System.IO;
using System.Linq;
+using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
@@ -234,8 +235,7 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
signatures.Add(signatureBytes);
}
- var verified = 0;
-
+ var expectedSignatures = new List();
foreach (var secret in _options.Security.SignerIdentity.KmsKeys)
{
if (!TryDecodeSecret(secret, out var secretBytes))
@@ -244,14 +244,15 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
}
using var hmac = new HMACSHA256(secretBytes);
- var computed = hmac.ComputeHash(preAuthEncoding);
+ expectedSignatures.Add(hmac.ComputeHash(preAuthEncoding));
+ }
- foreach (var candidate in signatures)
+ var verified = 0;
+ foreach (var candidate in signatures)
+ {
+ if (expectedSignatures.Any(expected => CryptographicOperations.FixedTimeEquals(expected, candidate)))
{
- if (CryptographicOperations.FixedTimeEquals(computed, candidate))
- {
- verified++;
- }
+ verified++;
}
}
@@ -294,11 +295,11 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
var leafCertificate = certificates[0];
var subjectAltName = GetSubjectAlternativeNames(leafCertificate).FirstOrDefault();
- if (_options.Security.SignerIdentity.FulcioRoots.Count > 0)
- {
- using var chain = new X509Chain
+ if (_options.Security.SignerIdentity.FulcioRoots.Count > 0)
{
- ChainPolicy =
+ using var chain = new X509Chain
+ {
+ ChainPolicy =
{
RevocationMode = X509RevocationMode.NoCheck,
VerificationFlags = X509VerificationFlags.NoFlag,
@@ -306,29 +307,34 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
}
};
- foreach (var rootPath in _options.Security.SignerIdentity.FulcioRoots)
- {
- try
+ foreach (var rootPath in _options.Security.SignerIdentity.FulcioRoots)
{
- if (File.Exists(rootPath))
+ try
{
- var rootCertificate = X509CertificateLoader.LoadCertificateFromFile(rootPath);
- chain.ChainPolicy.CustomTrustStore.Add(rootCertificate);
+ if (File.Exists(rootPath))
+ {
+ var rootCertificate = X509CertificateLoader.LoadCertificateFromFile(rootPath);
+ chain.ChainPolicy.CustomTrustStore.Add(rootCertificate);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to load Fulcio root {Root}", rootPath);
}
}
- catch (Exception ex)
+
+ for (var i = 1; i < certificates.Count; i++)
{
- _logger.LogWarning(ex, "Failed to load Fulcio root {Root}", rootPath);
+ chain.ChainPolicy.ExtraStore.Add(certificates[i]);
+ }
+
+ if (!chain.Build(leafCertificate))
+ {
+ var status = string.Join(";", chain.ChainStatus.Select(s => s.StatusInformation.Trim())).Trim(';');
+ issuerIssues.Add(string.IsNullOrEmpty(status) ? "certificate_chain_untrusted" : $"certificate_chain_untrusted:{status}");
}
}
- if (!chain.Build(leafCertificate))
- {
- var status = string.Join(";", chain.ChainStatus.Select(s => s.StatusInformation.Trim())).Trim(';');
- issuerIssues.Add(string.IsNullOrEmpty(status) ? "certificate_chain_untrusted" : $"certificate_chain_untrusted:{status}");
- }
- }
-
if (_options.Security.SignerIdentity.AllowedSans.Count > 0)
{
var sans = GetSubjectAlternativeNames(leafCertificate);
@@ -775,14 +781,44 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
{
if (string.Equals(extension.Oid?.Value, "2.5.29.17", StringComparison.Ordinal))
{
- var formatted = extension.Format(true);
- var lines = formatted.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
- foreach (var line in lines)
+ AsnReader reader;
+ try
{
- var parts = line.Split('=');
- if (parts.Length == 2)
+ reader = new AsnReader(extension.RawData, AsnEncodingRules.DER);
+ }
+ catch (AsnContentException)
+ {
+ yield break;
+ }
+
+ var sequence = reader.ReadSequence();
+ while (sequence.HasData)
+ {
+ var tag = sequence.PeekTag();
+ if (tag.TagClass != TagClass.ContextSpecific)
{
- yield return parts[1].Trim();
+ sequence.ReadEncodedValue();
+ continue;
+ }
+
+ switch (tag.TagValue)
+ {
+ case 1:
+ yield return sequence.ReadCharacterString(UniversalTagNumber.IA5String, new Asn1Tag(TagClass.ContextSpecific, 1));
+ break;
+ case 2:
+ yield return sequence.ReadCharacterString(UniversalTagNumber.IA5String, new Asn1Tag(TagClass.ContextSpecific, 2));
+ break;
+ case 6:
+ yield return sequence.ReadCharacterString(UniversalTagNumber.IA5String, new Asn1Tag(TagClass.ContextSpecific, 6));
+ break;
+ case 7:
+ var ipBytes = sequence.ReadOctetString(new Asn1Tag(TagClass.ContextSpecific, 7));
+ yield return new IPAddress(ipBytes).ToString();
+ break;
+ default:
+ sequence.ReadEncodedValue();
+ break;
}
}
}
@@ -791,21 +827,32 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
private static byte[] ComputePreAuthEncoding(string payloadType, byte[] payload)
{
- var headerBytes = Encoding.UTF8.GetBytes(payloadType ?? string.Empty);
- var buffer = new byte[6 + 8 + headerBytes.Length + 8 + payload.Length];
+ var payloadTypeValue = payloadType ?? string.Empty;
+ var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadTypeValue);
+ var payloadTypeLength = Encoding.ASCII.GetBytes(payloadTypeBytes.Length.ToString(System.Globalization.CultureInfo.InvariantCulture));
+ var payloadLength = Encoding.ASCII.GetBytes(payload.Length.ToString(System.Globalization.CultureInfo.InvariantCulture));
+ var space = new byte[] { (byte)' ' };
+
+ var totalLength = 6 + space.Length + payloadTypeLength.Length + space.Length + payloadTypeBytes.Length
+ + space.Length + payloadLength.Length + space.Length + payload.Length;
+ var buffer = new byte[totalLength];
var offset = 0;
- Encoding.ASCII.GetBytes("DSSEv1", 0, 6, buffer, offset);
- offset += 6;
+ static void CopyBytes(byte[] source, byte[] destination, ref int index)
+ {
+ Buffer.BlockCopy(source, 0, destination, index, source.Length);
+ index += source.Length;
+ }
- BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(offset, 8), (ulong)headerBytes.Length);
- offset += 8;
- Buffer.BlockCopy(headerBytes, 0, buffer, offset, headerBytes.Length);
- offset += headerBytes.Length;
-
- BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(offset, 8), (ulong)payload.Length);
- offset += 8;
- Buffer.BlockCopy(payload, 0, buffer, offset, payload.Length);
+ CopyBytes(Encoding.ASCII.GetBytes("DSSEv1"), buffer, ref offset);
+ CopyBytes(space, buffer, ref offset);
+ CopyBytes(payloadTypeLength, buffer, ref offset);
+ CopyBytes(space, buffer, ref offset);
+ CopyBytes(payloadTypeBytes, buffer, ref offset);
+ CopyBytes(space, buffer, ref offset);
+ CopyBytes(payloadLength, buffer, ref offset);
+ CopyBytes(space, buffer, ref offset);
+ payload.CopyTo(buffer.AsSpan(offset));
return buffer;
}
diff --git a/src/Attestor/StellaOps.Attestor.Verify/Providers/DistributedVerificationProvider.cs b/src/Attestor/StellaOps.Attestor.Verify/Providers/DistributedVerificationProvider.cs
index 682eaa2cd..39e77e534 100644
--- a/src/Attestor/StellaOps.Attestor.Verify/Providers/DistributedVerificationProvider.cs
+++ b/src/Attestor/StellaOps.Attestor.Verify/Providers/DistributedVerificationProvider.cs
@@ -1,21 +1,17 @@
-// ───────────────────────────────────────────────────────────────────────────
-// StellaOps Attestor — Distributed Verification Provider (Resilient, Multi-Node)
+// -----------------------------------------------------------------------------
+// StellaOps Attestor - Distributed Verification Provider (Resilient, Multi-Node)
// SPDX-License-Identifier: AGPL-3.0-or-later
-// ───────────────────────────────────────────────────────────────────────────
+// -----------------------------------------------------------------------------
#if STELLAOPS_EXPERIMENTAL_DISTRIBUTED_VERIFY
+using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
-using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Polly;
-using Polly.CircuitBreaker;
-using Polly.Retry;
-using Polly.Timeout;
using StellaOps.Attestor.Verify.Configuration;
using StellaOps.Attestor.Verify.Models;
@@ -32,7 +28,6 @@ public class DistributedVerificationProvider : IVerificationProvider
private readonly HttpClient _httpClient;
private readonly ConcurrentDictionary _circuitStates = new();
private readonly ConsistentHashRing _hashRing;
- private readonly ResiliencePipeline _resiliencePipeline;
public DistributedVerificationProvider(
ILogger logger,
@@ -49,7 +44,6 @@ public class DistributedVerificationProvider : IVerificationProvider
}
_hashRing = new ConsistentHashRing(_options.Nodes, _options.VirtualNodeMultiplier);
- _resiliencePipeline = BuildResiliencePipeline();
_logger.LogInformation("Initialized distributed verification provider with {NodeCount} nodes", _options.Nodes.Count);
}
@@ -83,9 +77,7 @@ public class DistributedVerificationProvider : IVerificationProvider
try
{
- var result = await _resiliencePipeline.ExecuteAsync(
- async ct => await ExecuteVerificationAsync(node, request, ct),
- cancellationToken);
+ var result = await ExecuteWithRetriesAsync(node, request, cancellationToken);
_logger.LogInformation(
"Verification request {RequestId} completed on node {NodeId} with result {Status}",
@@ -196,37 +188,36 @@ public class DistributedVerificationProvider : IVerificationProvider
return result ?? throw new InvalidOperationException("Received null response from verification node");
}
- private ResiliencePipeline BuildResiliencePipeline()
+ private async Task ExecuteWithRetriesAsync(
+ VerificationNode node,
+ VerificationRequest request,
+ CancellationToken cancellationToken)
{
- return new ResiliencePipelineBuilder()
- .AddTimeout(new TimeoutStrategyOptions
+ Exception? lastError = null;
+
+ for (var attempt = 0; attempt <= _options.MaxRetries; attempt++)
+ {
+ using var attemptCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ attemptCts.CancelAfter(_options.RequestTimeout);
+
+ try
{
- Timeout = _options.RequestTimeout,
- OnTimeout = args =>
- {
- _logger.LogWarning("Request timed out after {Timeout}", args.Timeout);
- return default;
- },
- })
- .AddRetry(new RetryStrategyOptions
+ return await ExecuteVerificationAsync(node, request, attemptCts.Token);
+ }
+ catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
- MaxRetryAttempts = _options.MaxRetries,
- Delay = _options.RetryDelay,
- BackoffType = DelayBackoffType.Exponential,
- ShouldHandle = new PredicateBuilder()
- .Handle()
- .Handle(),
- OnRetry = args =>
+ lastError = ex;
+ if (attempt >= _options.MaxRetries)
{
- _logger.LogWarning(
- args.Outcome.Exception,
- "Retry attempt {AttemptNumber} after delay {Delay}",
- args.AttemptNumber,
- args.RetryDelay);
- return default;
- },
- })
- .Build();
+ break;
+ }
+
+ _logger.LogWarning(ex, "Retry attempt {AttemptNumber} after delay {Delay}", attempt + 1, _options.RetryDelay);
+ await Task.Delay(_options.RetryDelay, cancellationToken);
+ }
+ }
+
+ throw lastError ?? new InvalidOperationException("Verification retry failed.");
}
private static string ComputeRoutingKey(VerificationRequest request)
@@ -342,7 +333,7 @@ internal sealed class ConsistentHashRing
private static int ComputeHash(string key)
{
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(key));
- return BitConverter.ToInt32(hashBytes, 0);
+ return BinaryPrimitives.ReadInt32BigEndian(hashBytes);
}
}
diff --git a/src/Attestor/StellaOps.Attestor.Verify/StellaOps.Attestor.Verify.csproj b/src/Attestor/StellaOps.Attestor.Verify/StellaOps.Attestor.Verify.csproj
index 3233f0d12..e1825338c 100644
--- a/src/Attestor/StellaOps.Attestor.Verify/StellaOps.Attestor.Verify.csproj
+++ b/src/Attestor/StellaOps.Attestor.Verify/StellaOps.Attestor.Verify.csproj
@@ -4,7 +4,7 @@
preview
enable
enable
- false
+ true
diff --git a/src/Attestor/StellaOps.Attestor.Verify/TASKS.md b/src/Attestor/StellaOps.Attestor.Verify/TASKS.md
index cc6fd8639..89ebd7a5f 100644
--- a/src/Attestor/StellaOps.Attestor.Verify/TASKS.md
+++ b/src/Attestor/StellaOps.Attestor.Verify/TASKS.md
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0071-M | DONE | Maintainability audit for StellaOps.Attestor.Verify. |
| AUDIT-0071-T | DONE | Test coverage audit for StellaOps.Attestor.Verify. |
-| AUDIT-0071-A | TODO | Pending approval for changes. |
+| AUDIT-0071-A | DONE | Applied DSSE PAE spec, SAN parsing, keyless chain store fix, KMS count fix, distributed provider cleanup, and tests. |
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Signing/DssePreAuthenticationEncoding.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Signing/DssePreAuthenticationEncoding.cs
index cf15163ed..c2d457d12 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Signing/DssePreAuthenticationEncoding.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Signing/DssePreAuthenticationEncoding.cs
@@ -1,5 +1,6 @@
using System;
-using System.Buffers.Binary;
+using System.Buffers;
+using System.Globalization;
using System.Text;
namespace StellaOps.Attestor.Core.Signing;
@@ -10,27 +11,33 @@ namespace StellaOps.Attestor.Core.Signing;
public static class DssePreAuthenticationEncoding
{
private static readonly byte[] Prefix = Encoding.ASCII.GetBytes("DSSEv1");
+ private static readonly byte[] Space = new byte[] { (byte)' ' };
public static byte[] Compute(string payloadType, ReadOnlySpan payload)
{
- var header = Encoding.UTF8.GetBytes(payloadType ?? string.Empty);
- var buffer = new byte[Prefix.Length + sizeof(long) + header.Length + sizeof(long) + payload.Length];
- var offset = 0;
+ var payloadTypeValue = payloadType ?? string.Empty;
+ var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadTypeValue);
+ var payloadTypeLength = Encoding.ASCII.GetBytes(payloadTypeBytes.Length.ToString(CultureInfo.InvariantCulture));
+ var payloadLength = Encoding.ASCII.GetBytes(payload.Length.ToString(CultureInfo.InvariantCulture));
- Prefix.CopyTo(buffer, offset);
- offset += Prefix.Length;
+ var buffer = new ArrayBufferWriter();
+ Write(buffer, Prefix);
+ Write(buffer, Space);
+ Write(buffer, payloadTypeLength);
+ Write(buffer, Space);
+ Write(buffer, payloadTypeBytes);
+ Write(buffer, Space);
+ Write(buffer, payloadLength);
+ Write(buffer, Space);
+ Write(buffer, payload);
- BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(offset, sizeof(long)), (ulong)header.Length);
- offset += sizeof(long);
+ return buffer.WrittenSpan.ToArray();
+ }
- header.CopyTo(buffer, offset);
- offset += header.Length;
-
- BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(offset, sizeof(long)), (ulong)payload.Length);
- offset += sizeof(long);
-
- payload.CopyTo(buffer.AsSpan(offset));
-
- return buffer;
+ private static void Write(ArrayBufferWriter writer, ReadOnlySpan bytes)
+ {
+ var span = writer.GetSpan(bytes.Length);
+ bytes.CopyTo(span);
+ writer.Advance(bytes.Length);
}
}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/TASKS.md b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/TASKS.md
index f4c52c216..2905cd3a0 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/TASKS.md
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/TASKS.md
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0049-M | DONE | Maintainability audit for StellaOps.Attestor.Core. |
| AUDIT-0049-T | DONE | Test coverage audit for StellaOps.Attestor.Core. |
-| AUDIT-0049-A | TODO | Pending approval for changes. |
+| AUDIT-0049-A | DOING | Pending approval for changes. |
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Bulk/InMemoryBulkVerificationJobStore.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Bulk/InMemoryBulkVerificationJobStore.cs
index 91f8eba57..f2fbd6714 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Bulk/InMemoryBulkVerificationJobStore.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Bulk/InMemoryBulkVerificationJobStore.cs
@@ -11,6 +11,17 @@ internal sealed class InMemoryBulkVerificationJobStore : IBulkVerificationJobSto
{
private readonly ConcurrentQueue _queue = new();
private readonly ConcurrentDictionary _jobs = new(StringComparer.OrdinalIgnoreCase);
+ private readonly TimeProvider _timeProvider;
+
+ public InMemoryBulkVerificationJobStore()
+ : this(TimeProvider.System)
+ {
+ }
+
+ public InMemoryBulkVerificationJobStore(TimeProvider timeProvider)
+ {
+ _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
+ }
public Task CreateAsync(BulkVerificationJob job, CancellationToken cancellationToken = default)
{
@@ -36,7 +47,7 @@ internal sealed class InMemoryBulkVerificationJobStore : IBulkVerificationJobSto
}
job.Status = BulkVerificationJobStatus.Running;
- job.StartedAt ??= DateTimeOffset.UtcNow;
+ job.StartedAt ??= _timeProvider.GetUtcNow();
return Task.FromResult(job);
}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Properties/AssemblyInfo.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Properties/AssemblyInfo.cs
index 98cbea929..12a85d91e 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Properties/AssemblyInfo.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Properties/AssemblyInfo.cs
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Attestor.Tests")]
+[assembly: InternalsVisibleTo("StellaOps.Attestor.Infrastructure.Tests")]
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Queue/PostgresRekorSubmissionQueue.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Queue/PostgresRekorSubmissionQueue.cs
index c16db844f..49dcae94f 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Queue/PostgresRekorSubmissionQueue.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Queue/PostgresRekorSubmissionQueue.cs
@@ -499,6 +499,10 @@ public sealed class PostgresRekorSubmissionQueue : IRekorSubmissionQueue
private static RekorQueueItem ReadQueueItem(NpgsqlDataReader reader)
{
+ var nextRetryAtOrdinal = reader.GetOrdinal("next_retry_at");
+ var createdAtOrdinal = reader.GetOrdinal("created_at");
+ var updatedAtOrdinal = reader.GetOrdinal("updated_at");
+
return new RekorQueueItem
{
Id = reader.GetGuid(reader.GetOrdinal("id")),
@@ -509,9 +513,11 @@ public sealed class PostgresRekorSubmissionQueue : IRekorSubmissionQueue
Status = Enum.Parse(reader.GetString(reader.GetOrdinal("status")), ignoreCase: true),
AttemptCount = reader.GetInt32(reader.GetOrdinal("attempt_count")),
MaxAttempts = reader.GetInt32(reader.GetOrdinal("max_attempts")),
- NextRetryAt = reader.GetDateTime(reader.GetOrdinal("next_retry_at")),
- CreatedAt = reader.GetDateTime(reader.GetOrdinal("created_at")),
- UpdatedAt = reader.GetDateTime(reader.GetOrdinal("updated_at")),
+ NextRetryAt = reader.IsDBNull(nextRetryAtOrdinal)
+ ? null
+ : reader.GetFieldValue(nextRetryAtOrdinal),
+ CreatedAt = reader.GetFieldValue(createdAtOrdinal),
+ UpdatedAt = reader.GetFieldValue(updatedAtOrdinal),
LastError = reader.IsDBNull(reader.GetOrdinal("last_error"))
? null
: reader.GetString(reader.GetOrdinal("last_error")),
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/HttpRekorClient.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/HttpRekorClient.cs
index 9e6dd3d6c..87a5979a1 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/HttpRekorClient.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/HttpRekorClient.cs
@@ -205,6 +205,13 @@ internal sealed class HttpRekorClient : IRekorClient
try
{
+ var logIndex = await GetLogIndexAsync(rekorUuid, backend, cancellationToken).ConfigureAwait(false);
+ if (!logIndex.HasValue)
+ {
+ return RekorInclusionVerificationResult.Failure(
+ "Failed to resolve Rekor log index for inclusion proof");
+ }
+
// Compute expected leaf hash from payload
var expectedLeafHash = MerkleProofVerifier.HashLeaf(payloadDigest);
var actualLeafHash = MerkleProofVerifier.HexToBytes(proof.Inclusion.LeafHash);
@@ -225,13 +232,10 @@ internal sealed class HttpRekorClient : IRekorClient
var expectedRootHash = MerkleProofVerifier.HexToBytes(proof.Checkpoint.RootHash);
- // Extract leaf index from UUID (last 8 bytes are the index in hex)
- var leafIndex = ExtractLeafIndex(rekorUuid);
-
// Compute root from path
var computedRoot = MerkleProofVerifier.ComputeRootFromPath(
actualLeafHash,
- leafIndex,
+ logIndex.Value,
proof.Checkpoint.Size,
proofPath);
@@ -248,7 +252,7 @@ internal sealed class HttpRekorClient : IRekorClient
// Verify root hash matches checkpoint
var verified = MerkleProofVerifier.VerifyInclusion(
actualLeafHash,
- leafIndex,
+ logIndex.Value,
proof.Checkpoint.Size,
proofPath,
expectedRootHash);
@@ -263,13 +267,13 @@ internal sealed class HttpRekorClient : IRekorClient
_logger.LogInformation(
"Successfully verified Rekor inclusion for UUID {Uuid} at index {Index}",
- rekorUuid, leafIndex);
+ rekorUuid, logIndex);
return RekorInclusionVerificationResult.Success(
- leafIndex,
+ logIndex.Value,
computedRootHex,
proof.Checkpoint.RootHash,
- checkpointSignatureValid: true); // TODO: Implement checkpoint signature verification
+ checkpointSignatureValid: false);
}
catch (Exception ex) when (ex is FormatException or ArgumentException)
{
@@ -279,36 +283,47 @@ internal sealed class HttpRekorClient : IRekorClient
}
}
- ///
- /// Extracts the leaf index from a Rekor UUID.
- /// Rekor UUIDs are formatted as: <entry-hash>-<tree-id>-<log-index-hex>
- ///
- private static long ExtractLeafIndex(string rekorUuid)
+ private async Task GetLogIndexAsync(string rekorUuid, RekorBackend backend, CancellationToken cancellationToken)
{
- // Try to parse as hex number from the end of the UUID
- // Rekor v1 format: 64 hex chars for entry hash + log index suffix
- if (rekorUuid.Length >= 16)
+ var entryUri = BuildUri(backend.Url, $"api/v2/log/entries/{rekorUuid}");
+
+ using var request = new HttpRequestMessage(HttpMethod.Get, entryUri);
+ using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+
+ if (response.StatusCode == HttpStatusCode.NotFound)
{
- // Take last 16 chars as potential hex index
- var indexPart = rekorUuid[^16..];
- if (long.TryParse(indexPart, System.Globalization.NumberStyles.HexNumber, null, out var index))
+ _logger.LogDebug("Rekor entry {Uuid} not found when resolving log index", rekorUuid);
+ return null;
+ }
+
+ response.EnsureSuccessStatusCode();
+
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ return TryGetLogIndex(document.RootElement, out var logIndex) ? logIndex : null;
+ }
+
+ private static bool TryGetLogIndex(JsonElement element, out long logIndex)
+ {
+ if (element.ValueKind == JsonValueKind.Object)
+ {
+ if (element.TryGetProperty("logIndex", out var logIndexElement)
+ && logIndexElement.TryGetInt64(out logIndex))
{
- return index;
+ return true;
+ }
+
+ foreach (var property in element.EnumerateObject())
+ {
+ if (TryGetLogIndex(property.Value, out logIndex))
+ {
+ return true;
+ }
}
}
- // Fallback: try parsing UUID parts separated by dashes
- var parts = rekorUuid.Split('-');
- if (parts.Length >= 1)
- {
- var lastPart = parts[^1];
- if (long.TryParse(lastPart, System.Globalization.NumberStyles.HexNumber, null, out var index))
- {
- return index;
- }
- }
-
- // Default to 0 if we can't parse
- return 0;
+ logIndex = 0;
+ return false;
}
}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/StubRekorClient.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/StubRekorClient.cs
index fd46db06e..adf66b0fa 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/StubRekorClient.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/StubRekorClient.cs
@@ -1,4 +1,7 @@
using System;
+using System.Buffers.Binary;
+using System.Security.Cryptography;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -10,15 +13,18 @@ namespace StellaOps.Attestor.Infrastructure.Rekor;
internal sealed class StubRekorClient : IRekorClient
{
private readonly ILogger _logger;
+ private readonly TimeProvider _timeProvider;
- public StubRekorClient(ILogger logger)
+ public StubRekorClient(ILogger logger, TimeProvider timeProvider)
{
_logger = logger;
+ _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}
public Task SubmitAsync(AttestorSubmissionRequest request, RekorBackend backend, CancellationToken cancellationToken = default)
{
- var uuid = Guid.NewGuid().ToString();
+ var hash = SHA256.HashData(Encoding.UTF8.GetBytes(request.Meta.BundleSha256 ?? string.Empty));
+ var uuid = new Guid(hash.AsSpan(0, 16)).ToString();
_logger.LogInformation("Stub Rekor submission for bundle {BundleSha} -> {Uuid}", request.Meta.BundleSha256, uuid);
var proof = new RekorProofResponse
@@ -28,7 +34,7 @@ internal sealed class StubRekorClient : IRekorClient
Origin = backend.Url.Host,
Size = 1,
RootHash = request.Meta.BundleSha256,
- Timestamp = DateTimeOffset.UtcNow
+ Timestamp = _timeProvider.GetUtcNow()
},
Inclusion = new RekorProofResponse.RekorInclusionProof
{
@@ -40,7 +46,7 @@ internal sealed class StubRekorClient : IRekorClient
var response = new RekorSubmissionResponse
{
Uuid = uuid,
- Index = Random.Shared.NextInt64(1, long.MaxValue),
+ Index = ComputeDeterministicIndex(hash),
LogUrl = new Uri(backend.Url, $"/api/v2/log/entries/{uuid}").ToString(),
Status = "included",
Proof = proof
@@ -59,7 +65,7 @@ internal sealed class StubRekorClient : IRekorClient
Origin = backend.Url.Host,
Size = 1,
RootHash = string.Empty,
- Timestamp = DateTimeOffset.UtcNow
+ Timestamp = _timeProvider.GetUtcNow()
},
Inclusion = new RekorProofResponse.RekorInclusionProof
{
@@ -85,4 +91,20 @@ internal sealed class StubRekorClient : IRekorClient
expectedRootHash: "stub-root-hash",
checkpointSignatureValid: true));
}
+
+ private static long ComputeDeterministicIndex(byte[] hash)
+ {
+ if (hash.Length < sizeof(long))
+ {
+ return 1;
+ }
+
+ var value = BinaryPrimitives.ReadInt64BigEndian(hash.AsSpan(0, sizeof(long)));
+ if (value == long.MinValue)
+ {
+ return long.MaxValue;
+ }
+
+ return Math.Abs(value);
+ }
}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs
index 61f5f9562..56f08cc18 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs
@@ -35,6 +35,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddAttestorInfrastructure(this IServiceCollection services)
{
services.AddMemoryCache();
+ services.AddSingleton(TimeProvider.System);
services.AddSingleton();
services.AddSingleton(sp =>
@@ -66,9 +67,21 @@ public static class ServiceCollectionExtensions
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddHttpClient(client =>
+ services.AddHttpClient((sp, client) =>
{
- client.Timeout = TimeSpan.FromSeconds(30);
+ var options = sp.GetRequiredService>().Value;
+ var timeoutMs = options.Rekor.Primary.ProofTimeoutMs;
+ if (options.Rekor.Mirror.Enabled)
+ {
+ timeoutMs = Math.Max(timeoutMs, options.Rekor.Mirror.ProofTimeoutMs);
+ }
+
+ if (timeoutMs <= 0)
+ {
+ timeoutMs = 15_000;
+ }
+
+ client.Timeout = TimeSpan.FromMilliseconds(timeoutMs);
});
services.AddSingleton(sp => sp.GetRequiredService());
@@ -104,7 +117,7 @@ public static class ServiceCollectionExtensions
var options = sp.GetRequiredService>().Value;
if (string.IsNullOrWhiteSpace(options.Redis.Url))
{
- return new InMemoryAttestorDedupeStore();
+ return ActivatorUtilities.CreateInstance(sp);
}
var multiplexer = sp.GetRequiredService();
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Signing/AttestorSigningKeyRegistry.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Signing/AttestorSigningKeyRegistry.cs
index d320783ae..63645ded0 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Signing/AttestorSigningKeyRegistry.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Signing/AttestorSigningKeyRegistry.cs
@@ -185,27 +185,22 @@ internal sealed class AttestorSigningKeyRegistry : IDisposable
throw new InvalidOperationException($"Signing key '{key.KeyId}' must specify kmsVersionId when using mode 'kms'.");
}
- var material = kmsClient.ExportAsync(providerKeyId, versionId, default).GetAwaiter().GetResult();
- var parameters = new ECParameters
- {
- Curve = ECCurve.NamedCurves.nistP256,
- D = material.D,
- Q = new ECPoint
- {
- X = material.Qx,
- Y = material.Qy
- }
- };
-
var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
- ["kms.version"] = material.VersionId
+ ["kms.version"] = versionId
};
+ var privateHandle = System.Text.Encoding.UTF8.GetBytes(
+ string.IsNullOrWhiteSpace(versionId) ? providerKeyId : versionId);
+ if (privateHandle.Length == 0)
+ {
+ throw new InvalidOperationException($"Signing key '{key.KeyId}' must supply a non-empty KMS reference.");
+ }
+
var signingKey = new CryptoSigningKey(
new CryptoKeyReference(providerKeyId, providerName),
normalizedAlgorithm,
- in parameters,
+ privateHandle,
now,
expiresAt: null,
metadata: metadata);
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj
index feca5d879..98df5ac43 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj
@@ -4,7 +4,7 @@
preview
enable
enable
- false
+ true
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj.Backup.tmp b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj.Backup.tmp
deleted file mode 100644
index 149548264..000000000
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/StellaOps.Attestor.Infrastructure.csproj.Backup.tmp
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
- net10.0
- preview
- enable
- enable
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorAuditSink.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorAuditSink.cs
index 9fdad54b7..2d0ab6491 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorAuditSink.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorAuditSink.cs
@@ -8,11 +8,15 @@ namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class InMemoryAttestorAuditSink : IAttestorAuditSink
{
+ private readonly object _sync = new();
public List Records { get; } = new();
public Task WriteAsync(AttestorAuditRecord record, CancellationToken cancellationToken = default)
{
- Records.Add(record);
+ lock (_sync)
+ {
+ Records.Add(record);
+ }
return Task.CompletedTask;
}
}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorDedupeStore.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorDedupeStore.cs
index 4ef28708c..50de05065 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorDedupeStore.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorDedupeStore.cs
@@ -9,12 +9,23 @@ namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class InMemoryAttestorDedupeStore : IAttestorDedupeStore
{
private readonly ConcurrentDictionary _store = new();
+ private readonly TimeProvider _timeProvider;
+
+ public InMemoryAttestorDedupeStore()
+ : this(TimeProvider.System)
+ {
+ }
+
+ public InMemoryAttestorDedupeStore(TimeProvider timeProvider)
+ {
+ _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
+ }
public Task TryGetExistingAsync(string bundleSha256, CancellationToken cancellationToken = default)
{
if (_store.TryGetValue(bundleSha256, out var entry))
{
- if (entry.ExpiresAt > DateTimeOffset.UtcNow)
+ if (entry.ExpiresAt > _timeProvider.GetUtcNow())
{
return Task.FromResult(entry.Uuid);
}
@@ -27,7 +38,7 @@ internal sealed class InMemoryAttestorDedupeStore : IAttestorDedupeStore
public Task SetAsync(string bundleSha256, string rekorUuid, TimeSpan ttl, CancellationToken cancellationToken = default)
{
- _store[bundleSha256] = (rekorUuid, DateTimeOffset.UtcNow.Add(ttl));
+ _store[bundleSha256] = (rekorUuid, _timeProvider.GetUtcNow().Add(ttl));
return Task.CompletedTask;
}
}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorEntryRepository.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorEntryRepository.cs
index 0cfc5c318..a3a664848 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorEntryRepository.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorEntryRepository.cs
@@ -141,7 +141,7 @@ internal sealed class InMemoryAttestorEntryRepository : IAttestorEntryRepository
return false;
}
- return string.CompareOrdinal(e.RekorUuid, continuation.RekorUuid) >= 0;
+ return string.CompareOrdinal(e.RekorUuid, continuation.RekorUuid) > 0;
});
}
@@ -150,19 +150,19 @@ internal sealed class InMemoryAttestorEntryRepository : IAttestorEntryRepository
.ThenBy(e => e.RekorUuid, StringComparer.Ordinal);
var page = ordered.Take(pageSize + 1).ToList();
- AttestorEntry? next = null;
+ AttestorEntry? continuationSource = null;
if (page.Count > pageSize)
{
- next = page[^1];
page.RemoveAt(page.Count - 1);
+ continuationSource = page[^1];
}
var result = new AttestorEntryQueryResult
{
Items = page,
- ContinuationToken = next is null
+ ContinuationToken = continuationSource is null
? null
- : AttestorEntryContinuationToken.Encode(next.CreatedAt, next.RekorUuid)
+ : AttestorEntryContinuationToken.Encode(continuationSource.CreatedAt, continuationSource.RekorUuid)
};
return Task.FromResult(result);
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/S3AttestorArchiveStore.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/S3AttestorArchiveStore.cs
index a66939f38..4a1770378 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/S3AttestorArchiveStore.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/S3AttestorArchiveStore.cs
@@ -54,7 +54,8 @@ internal sealed class S3AttestorArchiveStore : IAttestorArchiveStore, IDisposabl
metadata["bundle.sha256"] = bundle.BundleSha256;
metadata["rekor.uuid"] = bundle.RekorUuid;
- var metadataObject = JsonSerializer.SerializeToUtf8Bytes(metadata);
+ var orderedMetadata = new SortedDictionary(metadata, StringComparer.Ordinal);
+ var metadataObject = JsonSerializer.SerializeToUtf8Bytes(orderedMetadata);
await PutObjectAsync(prefix + "meta/" + bundle.RekorUuid + ".json", metadataObject, cancellationToken).ConfigureAwait(false);
await PutObjectAsync(prefix + "meta/" + bundle.BundleSha256 + ".json", metadataObject, cancellationToken).ConfigureAwait(false);
}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Submission/DefaultDsseCanonicalizer.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Submission/DefaultDsseCanonicalizer.cs
index 2f78bcf61..65aa10aa8 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Submission/DefaultDsseCanonicalizer.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Submission/DefaultDsseCanonicalizer.cs
@@ -1,3 +1,4 @@
+using System;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
@@ -16,6 +17,8 @@ public sealed class DefaultDsseCanonicalizer : IDsseCanonicalizer
public Task CanonicalizeAsync(AttestorSubmissionRequest request, CancellationToken cancellationToken = default)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
var node = new JsonObject
{
["payloadType"] = request.Bundle.Dsse.PayloadType,
@@ -23,14 +26,16 @@ public sealed class DefaultDsseCanonicalizer : IDsseCanonicalizer
["signatures"] = CreateSignaturesArray(request)
};
- var json = node.ToJsonString(SerializerOptions);
- return Task.FromResult(JsonSerializer.SerializeToUtf8Bytes(JsonNode.Parse(json)!, SerializerOptions));
+ var bytes = JsonSerializer.SerializeToUtf8Bytes(node, SerializerOptions);
+ return Task.FromResult(bytes);
}
private static JsonArray CreateSignaturesArray(AttestorSubmissionRequest request)
{
var array = new JsonArray();
- foreach (var signature in request.Bundle.Dsse.Signatures)
+ foreach (var signature in request.Bundle.Dsse.Signatures
+ .OrderBy(s => s.KeyId ?? string.Empty, StringComparer.Ordinal)
+ .ThenBy(s => s.Signature, StringComparer.Ordinal))
{
var obj = new JsonObject
{
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/TASKS.md b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/TASKS.md
index bde961232..0008c8423 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/TASKS.md
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/TASKS.md
@@ -7,4 +7,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0055-M | DONE | Maintainability audit for StellaOps.Attestor.Infrastructure. |
| AUDIT-0055-T | DONE | Test coverage audit for StellaOps.Attestor.Infrastructure. |
-| AUDIT-0055-A | TODO | Pending approval for changes. |
+| AUDIT-0055-A | DONE | Applied audit remediation and added infrastructure tests. |
+| VAL-SMOKE-001 | DONE | Fixed continuation token behavior; unit tests pass. |
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Verification/AttestorVerificationService.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Verification/AttestorVerificationService.cs
index 28db70b52..00c9a3614 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Verification/AttestorVerificationService.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Verification/AttestorVerificationService.cs
@@ -214,7 +214,10 @@ internal sealed class AttestorVerificationService : IAttestorVerificationService
private async Task ResolveEntryByArtifactAsync(string artifactSha256, bool refreshProof, CancellationToken cancellationToken)
{
var entries = await _repository.GetByArtifactShaAsync(artifactSha256, cancellationToken).ConfigureAwait(false);
- var entry = entries.OrderByDescending(e => e.CreatedAt).FirstOrDefault();
+ var entry = entries
+ .OrderByDescending(e => e.CreatedAt)
+ .ThenBy(e => e.RekorUuid, StringComparer.Ordinal)
+ .FirstOrDefault();
if (entry is null)
{
return null;
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Workers/RekorRetryWorker.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Workers/RekorRetryWorker.cs
index 58d7463c7..86a4f5858 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Workers/RekorRetryWorker.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Workers/RekorRetryWorker.cs
@@ -7,6 +7,7 @@
#if STELLAOPS_EXPERIMENTAL_REKOR_QUEUE
+using System;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -15,6 +16,7 @@ using StellaOps.Attestor.Core.Options;
using StellaOps.Attestor.Core.Queue;
using StellaOps.Attestor.Core.Rekor;
using StellaOps.Attestor.Core.Submission;
+using System.Text.Json;
namespace StellaOps.Attestor.Infrastructure.Workers;
@@ -190,41 +192,90 @@ public sealed class RekorRetryWorker : BackgroundService
{
return backend.ToLowerInvariant() switch
{
- "primary" => new RekorBackend(
- _attestorOptions.Rekor.Primary.Url ?? throw new InvalidOperationException("Primary Rekor URL not configured"),
- "primary"),
- "mirror" => new RekorBackend(
- _attestorOptions.Rekor.Mirror.Url ?? throw new InvalidOperationException("Mirror Rekor URL not configured"),
- "mirror"),
+ "primary" => BuildBackend("primary", _attestorOptions.Rekor.Primary),
+ "mirror" => BuildBackend("mirror", _attestorOptions.Rekor.Mirror),
_ => throw new InvalidOperationException($"Unknown Rekor backend: {backend}")
};
}
private static AttestorSubmissionRequest BuildSubmissionRequest(RekorQueueItem item)
{
- // Reconstruct the submission request from the stored payload
+ var dsseEnvelope = ParseDsseEnvelope(item.DssePayload);
return new AttestorSubmissionRequest
{
- TenantId = item.TenantId,
- BundleSha256 = item.BundleSha256,
- DssePayload = item.DssePayload
+ Bundle = new AttestorSubmissionRequest.SubmissionBundle
+ {
+ Dsse = dsseEnvelope
+ },
+ Meta = new AttestorSubmissionRequest.SubmissionMeta
+ {
+ BundleSha256 = item.BundleSha256,
+ Artifact = new AttestorSubmissionRequest.ArtifactInfo()
+ }
+ };
+ }
+
+ private static AttestorSubmissionRequest.DsseEnvelope ParseDsseEnvelope(byte[] payload)
+ {
+ if (payload.Length == 0)
+ {
+ throw new InvalidOperationException("Queue item DSSE payload is empty.");
+ }
+
+ using var document = JsonDocument.Parse(payload);
+ var root = document.RootElement;
+
+ var payloadType = root.GetProperty("payloadType").GetString()
+ ?? throw new InvalidOperationException("Queue item DSSE payload missing payloadType.");
+ var payloadBase64 = root.GetProperty("payload").GetString()
+ ?? throw new InvalidOperationException("Queue item DSSE payload missing payload.");
+
+ var signatures = new List();
+ if (root.TryGetProperty("signatures", out var signaturesElement) && signaturesElement.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var signatureElement in signaturesElement.EnumerateArray())
+ {
+ var signatureValue = signatureElement.GetProperty("sig").GetString()
+ ?? throw new InvalidOperationException("Queue item DSSE signature missing sig.");
+ signatureElement.TryGetProperty("keyid", out var keyIdElement);
+
+ signatures.Add(new AttestorSubmissionRequest.DsseSignature
+ {
+ Signature = signatureValue,
+ KeyId = keyIdElement.ValueKind == JsonValueKind.String ? keyIdElement.GetString() : null
+ });
+ }
+ }
+
+ if (signatures.Count == 0)
+ {
+ throw new InvalidOperationException("Queue item DSSE payload missing signatures.");
+ }
+
+ return new AttestorSubmissionRequest.DsseEnvelope
+ {
+ PayloadType = payloadType,
+ PayloadBase64 = payloadBase64,
+ Signatures = signatures
+ };
+ }
+
+ private static RekorBackend BuildBackend(string name, AttestorOptions.RekorBackendOptions options)
+ {
+ if (string.IsNullOrWhiteSpace(options.Url))
+ {
+ throw new InvalidOperationException($"Rekor backend '{name}' is not configured.");
+ }
+
+ return new RekorBackend
+ {
+ Name = name,
+ Url = new Uri(options.Url, UriKind.Absolute),
+ ProofTimeout = TimeSpan.FromMilliseconds(options.ProofTimeoutMs),
+ PollInterval = TimeSpan.FromMilliseconds(options.PollIntervalMs),
+ MaxAttempts = options.MaxAttempts
};
}
}
-///
-/// Simple Rekor backend configuration.
-///
-public sealed record RekorBackend(string Url, string Name);
-
-///
-/// Submission request for the retry worker.
-///
-public sealed class AttestorSubmissionRequest
-{
- public string TenantId { get; init; } = string.Empty;
- public string BundleSha256 { get; init; } = string.Empty;
- public byte[] DssePayload { get; init; } = Array.Empty();
-}
-
#endif
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/AttestationBundleEndpointsTests.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/AttestationBundleEndpointsTests.cs
index 5200725db..2dad6ab39 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/AttestationBundleEndpointsTests.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/AttestationBundleEndpointsTests.cs
@@ -194,7 +194,8 @@ internal sealed class AttestorWebApplicationFactory : WebApplicationFactory());
+ var rekorClient = new StubRekorClient(new NullLogger(), TimeProvider.System);
var archiveStore = new NullAttestorArchiveStore(new NullLogger());
var auditSink = new InMemoryAttestorAuditSink();
var witnessClient = new TestTransparencyWitnessClient();
@@ -131,7 +131,7 @@ public sealed class AttestorSubmissionServiceTests
var validator = new AttestorSubmissionValidator(canonicalizer);
var repository = new InMemoryAttestorEntryRepository();
var dedupeStore = new InMemoryAttestorDedupeStore();
- var rekorClient = new StubRekorClient(new NullLogger());
+ var rekorClient = new StubRekorClient(new NullLogger(), TimeProvider.System);
var archiveStore = new NullAttestorArchiveStore(new NullLogger());
var auditSink = new InMemoryAttestorAuditSink();
var witnessClient = new TestTransparencyWitnessClient();
@@ -199,7 +199,7 @@ public sealed class AttestorSubmissionServiceTests
var validator = new AttestorSubmissionValidator(canonicalizer);
var repository = new InMemoryAttestorEntryRepository();
var dedupeStore = new InMemoryAttestorDedupeStore();
- var rekorClient = new StubRekorClient(new NullLogger());
+ var rekorClient = new StubRekorClient(new NullLogger(), TimeProvider.System);
var archiveStore = new NullAttestorArchiveStore(new NullLogger());
var auditSink = new InMemoryAttestorAuditSink();
var witnessClient = new TestTransparencyWitnessClient();
@@ -270,7 +270,7 @@ public sealed class AttestorSubmissionServiceTests
var validator = new AttestorSubmissionValidator(canonicalizer);
var repository = new InMemoryAttestorEntryRepository();
var dedupeStore = new InMemoryAttestorDedupeStore();
- var rekorClient = new StubRekorClient(new NullLogger());
+ var rekorClient = new StubRekorClient(new NullLogger(), TimeProvider.System);
var archiveStore = new NullAttestorArchiveStore(new NullLogger());
var auditSink = new InMemoryAttestorAuditSink();
var witnessClient = new TestTransparencyWitnessClient();
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/AttestorVerificationServiceTests.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/AttestorVerificationServiceTests.cs
index c01abfb49..735c7415e 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/AttestorVerificationServiceTests.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/AttestorVerificationServiceTests.cs
@@ -1,4 +1,3 @@
-using System.Buffers.Binary;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
@@ -67,7 +66,7 @@ public sealed class AttestorVerificationServiceTests
var engine = new AttestorVerificationEngine(canonicalizer, new TestCryptoHash(), options, NullLogger.Instance);
var repository = new InMemoryAttestorEntryRepository();
var dedupeStore = new InMemoryAttestorDedupeStore();
- var rekorClient = new StubRekorClient(new NullLogger());
+ var rekorClient = new StubRekorClient(new NullLogger(), TimeProvider.System);
var archiveStore = new NullAttestorArchiveStore(new NullLogger());
var auditSink = new InMemoryAttestorAuditSink();
var submissionService = new AttestorSubmissionService(
@@ -163,7 +162,7 @@ public sealed class AttestorVerificationServiceTests
var engine = new AttestorVerificationEngine(canonicalizer, new TestCryptoHash(), options, NullLogger.Instance);
var repository = new InMemoryAttestorEntryRepository();
var dedupeStore = new InMemoryAttestorDedupeStore();
- var rekorClient = new StubRekorClient(new NullLogger());
+ var rekorClient = new StubRekorClient(new NullLogger(), TimeProvider.System);
var archiveStore = new NullAttestorArchiveStore(new NullLogger());
var auditSink = new InMemoryAttestorAuditSink();
var submissionService = new AttestorSubmissionService(
@@ -250,7 +249,7 @@ public sealed class AttestorVerificationServiceTests
var engine = new AttestorVerificationEngine(canonicalizer, new TestCryptoHash(), options, NullLogger.Instance);
var repository = new InMemoryAttestorEntryRepository();
var dedupeStore = new InMemoryAttestorDedupeStore();
- var rekorClient = new StubRekorClient(new NullLogger());
+ var rekorClient = new StubRekorClient(new NullLogger(), TimeProvider.System);
var archiveStore = new NullAttestorArchiveStore(new NullLogger());
var auditSink = new InMemoryAttestorAuditSink();
var submissionService = new AttestorSubmissionService(
@@ -416,19 +415,7 @@ public sealed class AttestorVerificationServiceTests
private static byte[] ComputePreAuthEncodingForTests(string payloadType, byte[] payload)
{
- var headerBytes = Encoding.UTF8.GetBytes(payloadType ?? string.Empty);
- var buffer = new byte[6 + 8 + headerBytes.Length + 8 + payload.Length];
- var offset = 0;
- Encoding.ASCII.GetBytes("DSSEv1", 0, 6, buffer, offset);
- offset += 6;
- BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(offset, 8), (ulong)headerBytes.Length);
- offset += 8;
- Buffer.BlockCopy(headerBytes, 0, buffer, offset, headerBytes.Length);
- offset += headerBytes.Length;
- BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(offset, 8), (ulong)payload.Length);
- offset += 8;
- Buffer.BlockCopy(payload, 0, buffer, offset, payload.Length);
- return buffer;
+ return StellaOps.Attestor.Core.Signing.DssePreAuthenticationEncoding.Compute(payloadType, payload);
}
[Trait("Category", TestCategories.Unit)]
@@ -629,7 +616,7 @@ public sealed class AttestorVerificationServiceTests
var engine = new AttestorVerificationEngine(canonicalizer, new TestCryptoHash(), options, NullLogger.Instance);
var repository = new InMemoryAttestorEntryRepository();
var dedupeStore = new InMemoryAttestorDedupeStore();
- var rekorClient = new StubRekorClient(new NullLogger());
+ var rekorClient = new StubRekorClient(new NullLogger(), TimeProvider.System);
var archiveStore = new NullAttestorArchiveStore(new NullLogger());
var auditSink = new InMemoryAttestorAuditSink();
var witnessClient = new TestTransparencyWitnessClient
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/AttestorWebServiceEndpoints.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/AttestorWebServiceEndpoints.cs
new file mode 100644
index 000000000..ef38e8d79
--- /dev/null
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/AttestorWebServiceEndpoints.cs
@@ -0,0 +1,454 @@
+using System.Security.Claims;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Http;
+using StellaOps.Attestor.Core.Bulk;
+using StellaOps.Attestor.Core.Offline;
+using StellaOps.Attestor.Core.Options;
+using StellaOps.Attestor.Core.Signing;
+using StellaOps.Attestor.Core.Storage;
+using StellaOps.Attestor.Core.Submission;
+using StellaOps.Attestor.Core.Verification;
+using StellaOps.Attestor.WebService.Contracts;
+
+namespace StellaOps.Attestor.WebService;
+
+internal static class AttestorWebServiceEndpoints
+{
+ public static void MapAttestorEndpoints(this WebApplication app, AttestorOptions attestorOptions)
+ {
+ app.MapGet("/api/v1/attestations", async (HttpRequest request, IAttestorEntryRepository repository, CancellationToken cancellationToken) =>
+ {
+ if (!AttestationListContracts.TryBuildQuery(request, out var query, out var error))
+ {
+ return error!;
+ }
+
+ var result = await repository.QueryAsync(query, cancellationToken).ConfigureAwait(false);
+ var response = new AttestationListResponseDto
+ {
+ Items = result.Items.Select(MapToListItem).ToList(),
+ ContinuationToken = result.ContinuationToken
+ };
+
+ return Results.Ok(response);
+ })
+ .RequireAuthorization("attestor:read")
+ .RequireRateLimiting("attestor-reads");
+
+ app.MapPost("/api/v1/attestations:export", async (HttpContext httpContext, AttestationExportRequestDto? requestDto, IAttestorBundleService bundleService, CancellationToken cancellationToken) =>
+ {
+ if (httpContext.Request.ContentLength > 0 && !IsJsonContentType(httpContext.Request.ContentType))
+ {
+ return UnsupportedMediaTypeResult();
+ }
+
+ AttestorBundleExportRequest request;
+ if (requestDto is null)
+ {
+ request = new AttestorBundleExportRequest();
+ }
+ else if (!requestDto.TryToDomain(out request, out var error))
+ {
+ return error!;
+ }
+
+ var package = await bundleService.ExportAsync(request, cancellationToken).ConfigureAwait(false);
+ return Results.Ok(package);
+ })
+ .RequireAuthorization("attestor:read")
+ .RequireRateLimiting("attestor-reads")
+ .Produces(StatusCodes.Status200OK);
+
+ app.MapPost("/api/v1/attestations:import", async (HttpContext httpContext, AttestorBundlePackage package, IAttestorBundleService bundleService, CancellationToken cancellationToken) =>
+ {
+ if (!IsJsonContentType(httpContext.Request.ContentType))
+ {
+ return UnsupportedMediaTypeResult();
+ }
+
+ var result = await bundleService.ImportAsync(package, cancellationToken).ConfigureAwait(false);
+ return Results.Ok(result);
+ })
+ .RequireAuthorization("attestor:write")
+ .RequireRateLimiting("attestor-submissions")
+ .Produces(StatusCodes.Status200OK);
+
+ app.MapPost("/api/v1/attestations:sign", async (AttestationSignRequestDto? requestDto, HttpContext httpContext, IAttestationSigningService signingService, CancellationToken cancellationToken) =>
+ {
+ if (requestDto is null)
+ {
+ return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: "Request body is required.");
+ }
+
+ if (!IsJsonContentType(httpContext.Request.ContentType))
+ {
+ return UnsupportedMediaTypeResult();
+ }
+
+ var certificate = httpContext.Connection.ClientCertificate;
+ if (certificate is null)
+ {
+ return Results.Problem(statusCode: StatusCodes.Status403Forbidden, title: "Client certificate required");
+ }
+
+ var user = httpContext.User;
+ if (user?.Identity is not { IsAuthenticated: true })
+ {
+ return Results.Problem(statusCode: StatusCodes.Status401Unauthorized, title: "Authentication required");
+ }
+
+ var signingRequest = new AttestationSignRequest
+ {
+ KeyId = requestDto.KeyId ?? string.Empty,
+ PayloadType = requestDto.PayloadType ?? string.Empty,
+ PayloadBase64 = requestDto.Payload ?? string.Empty,
+ Mode = requestDto.Mode,
+ CertificateChain = requestDto.CertificateChain ?? new List(),
+ Artifact = new AttestorSubmissionRequest.ArtifactInfo
+ {
+ Sha256 = requestDto.Artifact?.Sha256 ?? string.Empty,
+ Kind = requestDto.Artifact?.Kind ?? string.Empty,
+ ImageDigest = requestDto.Artifact?.ImageDigest,
+ SubjectUri = requestDto.Artifact?.SubjectUri
+ },
+ LogPreference = requestDto.LogPreference ?? "primary",
+ Archive = requestDto.Archive ?? true
+ };
+
+ try
+ {
+ var submissionContext = BuildSubmissionContext(user, certificate);
+ var result = await signingService.SignAsync(signingRequest, submissionContext, cancellationToken).ConfigureAwait(false);
+ var response = new AttestationSignResponseDto
+ {
+ Bundle = result.Bundle,
+ Meta = result.Meta,
+ Key = new AttestationSignKeyDto
+ {
+ KeyId = result.KeyId,
+ Algorithm = result.Algorithm,
+ Mode = result.Mode,
+ Provider = result.Provider,
+ SignedAt = result.SignedAt.ToString("O")
+ }
+ };
+ return Results.Ok(response);
+ }
+ catch (AttestorSigningException signingEx)
+ {
+ return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: signingEx.Message, extensions: new Dictionary
+ {
+ ["code"] = signingEx.Code
+ });
+ }
+ }).RequireAuthorization("attestor:write")
+ .RequireRateLimiting("attestor-submissions");
+
+ app.MapPost("/api/v1/rekor/entries", async (AttestorSubmissionRequest request, HttpContext httpContext, IAttestorSubmissionService submissionService, CancellationToken cancellationToken) =>
+ {
+ if (!IsJsonContentType(httpContext.Request.ContentType))
+ {
+ return UnsupportedMediaTypeResult();
+ }
+
+ var certificate = httpContext.Connection.ClientCertificate;
+ if (certificate is null)
+ {
+ return Results.Problem(statusCode: StatusCodes.Status403Forbidden, title: "Client certificate required");
+ }
+
+ var user = httpContext.User;
+ if (user?.Identity is not { IsAuthenticated: true })
+ {
+ return Results.Problem(statusCode: StatusCodes.Status401Unauthorized, title: "Authentication required");
+ }
+
+ var submissionContext = BuildSubmissionContext(user, certificate);
+
+ try
+ {
+ var result = await submissionService.SubmitAsync(request, submissionContext, cancellationToken).ConfigureAwait(false);
+ return Results.Ok(result);
+ }
+ catch (AttestorValidationException validationEx)
+ {
+ return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: validationEx.Message, extensions: new Dictionary
+ {
+ ["code"] = validationEx.Code
+ });
+ }
+ })
+ .RequireAuthorization("attestor:write")
+ .RequireRateLimiting("attestor-submissions");
+
+ app.MapGet("/api/v1/rekor/entries/{uuid}", async (string uuid, bool? refresh, IAttestorVerificationService verificationService, CancellationToken cancellationToken) =>
+ await GetAttestationDetailResultAsync(uuid, refresh is true, verificationService, cancellationToken))
+ .RequireAuthorization("attestor:read")
+ .RequireRateLimiting("attestor-reads");
+
+ app.MapGet("/api/v1/attestations/{uuid}", async (string uuid, bool? refresh, IAttestorVerificationService verificationService, CancellationToken cancellationToken) =>
+ await GetAttestationDetailResultAsync(uuid, refresh is true, verificationService, cancellationToken))
+ .RequireAuthorization("attestor:read")
+ .RequireRateLimiting("attestor-reads");
+
+ app.MapPost("/api/v1/rekor/verify", async (HttpContext httpContext, AttestorVerificationRequest verifyRequest, IAttestorVerificationService verificationService, CancellationToken cancellationToken) =>
+ {
+ if (!IsJsonContentType(httpContext.Request.ContentType))
+ {
+ return UnsupportedMediaTypeResult();
+ }
+
+ try
+ {
+ var result = await verificationService.VerifyAsync(verifyRequest, cancellationToken).ConfigureAwait(false);
+ return Results.Ok(result);
+ }
+ catch (AttestorVerificationException ex)
+ {
+ return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, extensions: new Dictionary
+ {
+ ["code"] = ex.Code
+ });
+ }
+ })
+ .RequireAuthorization("attestor:verify")
+ .RequireRateLimiting("attestor-verifications");
+
+ app.MapPost("/api/v1/rekor/verify:bulk", async (
+ BulkVerificationRequestDto? requestDto,
+ HttpContext httpContext,
+ IBulkVerificationJobStore jobStore,
+ CancellationToken cancellationToken) =>
+ {
+ var context = BuildBulkJobContext(httpContext.User);
+
+ if (!BulkVerificationContracts.TryBuildJob(requestDto, attestorOptions, context, out var job, out var error))
+ {
+ return error!;
+ }
+
+ var queued = await jobStore.CountQueuedAsync(cancellationToken).ConfigureAwait(false);
+ if (queued >= Math.Max(1, attestorOptions.Quotas.Bulk.MaxQueuedJobs))
+ {
+ return Results.Problem(statusCode: StatusCodes.Status429TooManyRequests, title: "Too many bulk verification jobs queued. Try again later.");
+ }
+
+ job = await jobStore.CreateAsync(job!, cancellationToken).ConfigureAwait(false);
+ var response = BulkVerificationContracts.MapJob(job);
+ return Results.Accepted($"/api/v1/rekor/verify:bulk/{job.Id}", response);
+ }).RequireAuthorization("attestor:write")
+ .RequireRateLimiting("attestor-bulk");
+
+ app.MapGet("/api/v1/rekor/verify:bulk/{jobId}", async (
+ string jobId,
+ HttpContext httpContext,
+ IBulkVerificationJobStore jobStore,
+ CancellationToken cancellationToken) =>
+ {
+ if (string.IsNullOrWhiteSpace(jobId))
+ {
+ return Results.NotFound();
+ }
+
+ var job = await jobStore.GetAsync(jobId, cancellationToken).ConfigureAwait(false);
+ if (job is null || !IsAuthorizedForJob(job, httpContext.User))
+ {
+ return Results.NotFound();
+ }
+
+ return Results.Ok(BulkVerificationContracts.MapJob(job));
+ }).RequireAuthorization("attestor:write");
+ }
+
+ private static async Task GetAttestationDetailResultAsync(
+ string uuid,
+ bool refresh,
+ IAttestorVerificationService verificationService,
+ CancellationToken cancellationToken)
+ {
+ var entry = await verificationService.GetEntryAsync(uuid, refresh, cancellationToken).ConfigureAwait(false);
+ if (entry is null)
+ {
+ return Results.NotFound();
+ }
+
+ return Results.Ok(MapAttestationDetail(entry));
+ }
+
+ private static AttestationDetailResponseDto MapAttestationDetail(AttestorEntry entry)
+ {
+ return new AttestationDetailResponseDto
+ {
+ Uuid = entry.RekorUuid,
+ Index = entry.Index,
+ Backend = entry.Log.Backend,
+ Proof = entry.Proof is null ? null : new AttestationProofDto
+ {
+ Checkpoint = entry.Proof.Checkpoint is null ? null : new AttestationCheckpointDto
+ {
+ Origin = entry.Proof.Checkpoint.Origin,
+ Size = entry.Proof.Checkpoint.Size,
+ RootHash = entry.Proof.Checkpoint.RootHash,
+ Timestamp = entry.Proof.Checkpoint.Timestamp?.ToString("O")
+ },
+ Inclusion = entry.Proof.Inclusion is null ? null : new AttestationInclusionDto
+ {
+ LeafHash = entry.Proof.Inclusion.LeafHash,
+ Path = entry.Proof.Inclusion.Path
+ }
+ },
+ LogUrl = entry.Log.Url,
+ Status = entry.Status,
+ Mirror = entry.Mirror is null ? null : new AttestationMirrorDto
+ {
+ Backend = entry.Mirror.Backend,
+ Uuid = entry.Mirror.Uuid,
+ Index = entry.Mirror.Index,
+ LogUrl = entry.Mirror.Url,
+ Status = entry.Mirror.Status,
+ Proof = entry.Mirror.Proof is null ? null : new AttestationProofDto
+ {
+ Checkpoint = entry.Mirror.Proof.Checkpoint is null ? null : new AttestationCheckpointDto
+ {
+ Origin = entry.Mirror.Proof.Checkpoint.Origin,
+ Size = entry.Mirror.Proof.Checkpoint.Size,
+ RootHash = entry.Mirror.Proof.Checkpoint.RootHash,
+ Timestamp = entry.Mirror.Proof.Checkpoint.Timestamp?.ToString("O")
+ },
+ Inclusion = entry.Mirror.Proof.Inclusion is null ? null : new AttestationInclusionDto
+ {
+ LeafHash = entry.Mirror.Proof.Inclusion.LeafHash,
+ Path = entry.Mirror.Proof.Inclusion.Path
+ }
+ },
+ Error = entry.Mirror.Error
+ },
+ Artifact = new AttestationArtifactDto
+ {
+ Sha256 = entry.Artifact.Sha256,
+ Kind = entry.Artifact.Kind,
+ ImageDigest = entry.Artifact.ImageDigest,
+ SubjectUri = entry.Artifact.SubjectUri
+ }
+ };
+ }
+
+ private static AttestationListItemDto MapToListItem(AttestorEntry entry)
+ {
+ return new AttestationListItemDto
+ {
+ Uuid = entry.RekorUuid,
+ Status = entry.Status,
+ CreatedAt = entry.CreatedAt.ToString("O"),
+ Artifact = new AttestationArtifactDto
+ {
+ Sha256 = entry.Artifact.Sha256,
+ Kind = entry.Artifact.Kind,
+ ImageDigest = entry.Artifact.ImageDigest,
+ SubjectUri = entry.Artifact.SubjectUri
+ },
+ Signer = new AttestationSignerDto
+ {
+ Mode = entry.SignerIdentity.Mode,
+ Issuer = entry.SignerIdentity.Issuer,
+ Subject = entry.SignerIdentity.SubjectAlternativeName,
+ KeyId = entry.SignerIdentity.KeyId
+ },
+ Log = new AttestationLogDto
+ {
+ Backend = entry.Log.Backend,
+ Url = entry.Log.Url,
+ Index = entry.Index,
+ Status = entry.Status
+ },
+ Mirror = entry.Mirror is null ? null : new AttestationLogDto
+ {
+ Backend = entry.Mirror.Backend,
+ Url = entry.Mirror.Url,
+ Index = entry.Mirror.Index,
+ Status = entry.Mirror.Status
+ }
+ };
+ }
+
+ private static SubmissionContext BuildSubmissionContext(ClaimsPrincipal user, X509Certificate2 certificate)
+ {
+ var subject = user.FindFirst("sub")?.Value ?? certificate.Subject;
+ var audience = user.FindFirst("aud")?.Value ?? string.Empty;
+ var clientId = user.FindFirst("client_id")?.Value;
+ var tenant = user.FindFirst("tenant")?.Value;
+
+ return new SubmissionContext
+ {
+ CallerSubject = subject,
+ CallerAudience = audience,
+ CallerClientId = clientId,
+ CallerTenant = tenant,
+ ClientCertificate = certificate,
+ MtlsThumbprint = certificate.Thumbprint
+ };
+ }
+
+ private static BulkVerificationJobContext BuildBulkJobContext(ClaimsPrincipal user)
+ {
+ var scopes = user.FindAll("scope")
+ .Select(claim => claim.Value)
+ .Where(value => !string.IsNullOrWhiteSpace(value))
+ .ToList();
+
+ return new BulkVerificationJobContext
+ {
+ Tenant = user.FindFirst("tenant")?.Value,
+ RequestedBy = user.FindFirst("sub")?.Value,
+ ClientId = user.FindFirst("client_id")?.Value,
+ Scopes = scopes
+ };
+ }
+
+ private static bool IsAuthorizedForJob(BulkVerificationJob job, ClaimsPrincipal user)
+ {
+ var tenant = user.FindFirst("tenant")?.Value;
+ if (!string.IsNullOrEmpty(job.Context.Tenant) &&
+ !string.Equals(job.Context.Tenant, tenant, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ var subject = user.FindFirst("sub")?.Value;
+ if (!string.IsNullOrEmpty(job.Context.RequestedBy) &&
+ !string.Equals(job.Context.RequestedBy, subject, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static bool IsJsonContentType(string? contentType)
+ {
+ if (string.IsNullOrWhiteSpace(contentType))
+ {
+ return false;
+ }
+
+ var mediaType = contentType.Split(';', 2)[0].Trim();
+ if (mediaType.Length == 0)
+ {
+ return false;
+ }
+
+ return mediaType.EndsWith("/json", StringComparison.OrdinalIgnoreCase)
+ || mediaType.Contains("+json", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static IResult UnsupportedMediaTypeResult()
+ {
+ return Results.Problem(
+ statusCode: StatusCodes.Status415UnsupportedMediaType,
+ title: "Unsupported content type. Submit application/json payloads.",
+ extensions: new Dictionary
+ {
+ ["code"] = "unsupported_media_type"
+ });
+ }
+}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Contracts/AttestationDetailContracts.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Contracts/AttestationDetailContracts.cs
new file mode 100644
index 000000000..08eb3e4ba
--- /dev/null
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Contracts/AttestationDetailContracts.cs
@@ -0,0 +1,87 @@
+using System.Text.Json.Serialization;
+
+namespace StellaOps.Attestor.WebService.Contracts;
+
+public sealed class AttestationDetailResponseDto
+{
+ [JsonPropertyName("uuid")]
+ public required string Uuid { get; init; }
+
+ [JsonPropertyName("index")]
+ public long? Index { get; init; }
+
+ [JsonPropertyName("backend")]
+ public required string Backend { get; init; }
+
+ [JsonPropertyName("proof")]
+ public AttestationProofDto? Proof { get; init; }
+
+ [JsonPropertyName("logURL")]
+ public required string LogUrl { get; init; }
+
+ [JsonPropertyName("status")]
+ public required string Status { get; init; }
+
+ [JsonPropertyName("mirror")]
+ public AttestationMirrorDto? Mirror { get; init; }
+
+ [JsonPropertyName("artifact")]
+ public required AttestationArtifactDto Artifact { get; init; }
+}
+
+public sealed class AttestationProofDto
+{
+ [JsonPropertyName("checkpoint")]
+ public AttestationCheckpointDto? Checkpoint { get; init; }
+
+ [JsonPropertyName("inclusion")]
+ public AttestationInclusionDto? Inclusion { get; init; }
+}
+
+public sealed class AttestationCheckpointDto
+{
+ [JsonPropertyName("origin")]
+ public string? Origin { get; init; }
+
+ [JsonPropertyName("size")]
+ public long Size { get; init; }
+
+ [JsonPropertyName("rootHash")]
+ public string? RootHash { get; init; }
+
+ [JsonPropertyName("timestamp")]
+ public string? Timestamp { get; init; }
+}
+
+public sealed class AttestationInclusionDto
+{
+ [JsonPropertyName("leafHash")]
+ public string? LeafHash { get; init; }
+
+ [JsonPropertyName("path")]
+ public IReadOnlyList Path { get; init; } = Array.Empty();
+}
+
+public sealed class AttestationMirrorDto
+{
+ [JsonPropertyName("backend")]
+ public required string Backend { get; init; }
+
+ [JsonPropertyName("uuid")]
+ public string? Uuid { get; init; }
+
+ [JsonPropertyName("index")]
+ public long? Index { get; init; }
+
+ [JsonPropertyName("logURL")]
+ public required string LogUrl { get; init; }
+
+ [JsonPropertyName("status")]
+ public required string Status { get; init; }
+
+ [JsonPropertyName("proof")]
+ public AttestationProofDto? Proof { get; init; }
+
+ [JsonPropertyName("error")]
+ public string? Error { get; init; }
+}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/AnchorsController.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/AnchorsController.cs
index 328b6c2f3..d47c9716a 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/AnchorsController.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/AnchorsController.cs
@@ -1,4 +1,6 @@
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.RateLimiting;
using StellaOps.Attestor.WebService.Contracts.Anchors;
namespace StellaOps.Attestor.WebService.Controllers;
@@ -25,14 +27,13 @@ public class AnchorsController : ControllerBase
/// Cancellation token.
/// List of trust anchors.
[HttpGet]
+ [Authorize("attestor:read")]
+ [EnableRateLimiting("attestor-reads")]
[ProducesResponseType(typeof(TrustAnchorDto[]), StatusCodes.Status200OK)]
public async Task> GetAnchorsAsync(CancellationToken ct = default)
{
_logger.LogInformation("Getting all trust anchors");
-
- // TODO: Implement using IProofChainRepository.GetActiveTrustAnchorsAsync
-
- return Ok(Array.Empty());
+ return NotImplementedResult();
}
///
@@ -42,6 +43,8 @@ public class AnchorsController : ControllerBase
/// Cancellation token.
/// The trust anchor.
[HttpGet("{anchorId}")]
+ [Authorize("attestor:read")]
+ [EnableRateLimiting("attestor-reads")]
[ProducesResponseType(typeof(TrustAnchorDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -49,26 +52,8 @@ public class AnchorsController : ControllerBase
[FromRoute] string anchorId,
CancellationToken ct = default)
{
- if (!Guid.TryParse(anchorId, out var parsedAnchorId))
- {
- return BadRequest(new ProblemDetails
- {
- Title = "Invalid anchor ID",
- Detail = "Anchor ID must be a valid GUID.",
- Status = StatusCodes.Status400BadRequest
- });
- }
-
- _logger.LogInformation("Getting trust anchor {AnchorId}", parsedAnchorId);
-
- // TODO: Implement using IProofChainRepository.GetTrustAnchorAsync
-
- return NotFound(new ProblemDetails
- {
- Title = "Trust Anchor Not Found",
- Detail = $"No trust anchor found with ID {parsedAnchorId}",
- Status = StatusCodes.Status404NotFound
- });
+ _logger.LogInformation("Getting trust anchor {AnchorId}", anchorId);
+ return NotImplementedResult();
}
///
@@ -78,6 +63,8 @@ public class AnchorsController : ControllerBase
/// Cancellation token.
/// The created trust anchor.
[HttpPost]
+ [Authorize("attestor:write")]
+ [EnableRateLimiting("attestor-submissions")]
[ProducesResponseType(typeof(TrustAnchorDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -86,26 +73,7 @@ public class AnchorsController : ControllerBase
CancellationToken ct = default)
{
_logger.LogInformation("Creating trust anchor for pattern {Pattern}", request.PurlPattern);
-
- // TODO: Implement using IProofChainRepository.SaveTrustAnchorAsync
- // 1. Check for existing anchor with same pattern
- // 2. Create new anchor entity
- // 3. Save to repository
- // 4. Log audit entry
-
- var anchor = new TrustAnchorDto
- {
- AnchorId = Guid.NewGuid(),
- PurlPattern = request.PurlPattern,
- AllowedKeyIds = request.AllowedKeyIds,
- AllowedPredicateTypes = request.AllowedPredicateTypes,
- PolicyRef = request.PolicyRef,
- PolicyVersion = request.PolicyVersion,
- CreatedAt = DateTimeOffset.UtcNow,
- UpdatedAt = DateTimeOffset.UtcNow
- };
-
- return CreatedAtAction(nameof(GetAnchorAsync), new { anchorId = anchor.AnchorId }, anchor);
+ return NotImplementedResult();
}
///
@@ -116,6 +84,8 @@ public class AnchorsController : ControllerBase
/// Cancellation token.
/// The updated trust anchor.
[HttpPatch("{anchorId:guid}")]
+ [Authorize("attestor:write")]
+ [EnableRateLimiting("attestor-submissions")]
[ProducesResponseType(typeof(TrustAnchorDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task> UpdateAnchorAsync(
@@ -124,19 +94,7 @@ public class AnchorsController : ControllerBase
CancellationToken ct = default)
{
_logger.LogInformation("Updating trust anchor {AnchorId}", anchorId);
-
- // TODO: Implement using IProofChainRepository
- // 1. Get existing anchor
- // 2. Apply updates
- // 3. Save to repository
- // 4. Log audit entry
-
- return NotFound(new ProblemDetails
- {
- Title = "Trust Anchor Not Found",
- Detail = $"No trust anchor found with ID {anchorId}",
- Status = StatusCodes.Status404NotFound
- });
+ return NotImplementedResult();
}
///
@@ -147,6 +105,8 @@ public class AnchorsController : ControllerBase
/// Cancellation token.
/// No content on success.
[HttpPost("{anchorId:guid}/revoke-key")]
+ [Authorize("attestor:write")]
+ [EnableRateLimiting("attestor-submissions")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
@@ -156,20 +116,7 @@ public class AnchorsController : ControllerBase
CancellationToken ct = default)
{
_logger.LogInformation("Revoking key {KeyId} in anchor {AnchorId}", request.KeyId, anchorId);
-
- // TODO: Implement using IProofChainRepository.RevokeKeyAsync
- // 1. Get existing anchor
- // 2. Add key to revoked_keys
- // 3. Remove from allowed_keyids
- // 4. Save to repository
- // 5. Log audit entry
-
- return NotFound(new ProblemDetails
- {
- Title = "Trust Anchor Not Found",
- Detail = $"No trust anchor found with ID {anchorId}",
- Status = StatusCodes.Status404NotFound
- });
+ return NotImplementedResult();
}
///
@@ -179,6 +126,8 @@ public class AnchorsController : ControllerBase
/// Cancellation token.
/// No content on success.
[HttpDelete("{anchorId:guid}")]
+ [Authorize("attestor:write")]
+ [EnableRateLimiting("attestor-submissions")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task DeleteAnchorAsync(
@@ -186,14 +135,19 @@ public class AnchorsController : ControllerBase
CancellationToken ct = default)
{
_logger.LogInformation("Deactivating trust anchor {AnchorId}", anchorId);
+ return NotImplementedResult();
+ }
- // TODO: Implement - set is_active = false (soft delete)
-
- return NotFound(new ProblemDetails
+ private static ObjectResult NotImplementedResult()
+ {
+ return new ObjectResult(new ProblemDetails
{
- Title = "Trust Anchor Not Found",
- Detail = $"No trust anchor found with ID {anchorId}",
- Status = StatusCodes.Status404NotFound
- });
+ Title = "Trust anchor management is not implemented.",
+ Status = StatusCodes.Status501NotImplemented,
+ Extensions = { ["code"] = "feature_not_implemented" }
+ })
+ {
+ StatusCode = StatusCodes.Status501NotImplemented
+ };
}
}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/ProofsController.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/ProofsController.cs
index aac6f2a58..21ba7e5b1 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/ProofsController.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/ProofsController.cs
@@ -1,6 +1,6 @@
-using System.Security.Cryptography;
-using System.Text;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.RateLimiting;
using StellaOps.Attestor.WebService.Contracts.Proofs;
namespace StellaOps.Attestor.WebService.Controllers;
@@ -29,6 +29,8 @@ public class ProofsController : ControllerBase
/// Cancellation token.
/// The created proof bundle ID.
[HttpPost("{entry}/spine")]
+ [Authorize("attestor:write")]
+ [EnableRateLimiting("attestor-submissions")]
[ProducesResponseType(typeof(CreateSpineResponse), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -39,49 +41,7 @@ public class ProofsController : ControllerBase
CancellationToken ct = default)
{
_logger.LogInformation("Creating proof spine for entry {Entry}", entry);
-
- // Validate entry format
- if (!IsValidSbomEntryId(entry))
- {
- return BadRequest(new ProblemDetails
- {
- Title = "Invalid SBOM Entry ID",
- Detail = "Entry ID must be in format sha256::pkg:",
- Status = StatusCodes.Status400BadRequest
- });
- }
-
- // TODO: Implement spine creation using IProofSpineAssembler
- // 1. Validate all evidence IDs exist
- // 2. Validate reasoning ID exists
- // 3. Validate VEX verdict ID exists
- // 4. Assemble spine using merkle tree
- // 5. Sign and store spine
- // 6. Return proof bundle ID
-
- foreach (var evidenceId in request.EvidenceIds)
- {
- if (!IsValidSha256Id(evidenceId))
- {
- return UnprocessableEntity(new ProblemDetails
- {
- Title = "Invalid evidence ID",
- Detail = "Evidence IDs must be in format sha256:<64-hex>",
- Status = StatusCodes.Status422UnprocessableEntity
- });
- }
- }
-
- var proofBundleId = ComputeProofBundleId(entry, request);
-
- var receiptUrl = $"/proofs/{Uri.EscapeDataString(entry)}/receipt";
- var response = new CreateSpineResponse
- {
- ProofBundleId = proofBundleId,
- ReceiptUrl = receiptUrl
- };
-
- return Created(receiptUrl, response);
+ return NotImplementedResult();
}
///
@@ -91,6 +51,8 @@ public class ProofsController : ControllerBase
/// Cancellation token.
/// The verification receipt.
[HttpGet("{entry}/receipt")]
+ [Authorize("attestor:read")]
+ [EnableRateLimiting("attestor-reads")]
[ProducesResponseType(typeof(VerificationReceiptDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task> GetReceiptAsync(
@@ -98,18 +60,7 @@ public class ProofsController : ControllerBase
CancellationToken ct = default)
{
_logger.LogInformation("Getting receipt for entry {Entry}", entry);
-
- // TODO: Implement receipt retrieval using IReceiptGenerator
- // 1. Get spine for entry
- // 2. Generate/retrieve verification receipt
- // 3. Return receipt
-
- return NotFound(new ProblemDetails
- {
- Title = "Receipt Not Found",
- Detail = $"No verification receipt found for entry {entry}",
- Status = StatusCodes.Status404NotFound
- });
+ return NotImplementedResult();
}
///
@@ -119,6 +70,8 @@ public class ProofsController : ControllerBase
/// Cancellation token.
/// The proof spine details.
[HttpGet("{entry}/spine")]
+ [Authorize("attestor:read")]
+ [EnableRateLimiting("attestor-reads")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task GetSpineAsync(
@@ -126,15 +79,7 @@ public class ProofsController : ControllerBase
CancellationToken ct = default)
{
_logger.LogInformation("Getting spine for entry {Entry}", entry);
-
- // TODO: Implement spine retrieval
-
- return NotFound(new ProblemDetails
- {
- Title = "Spine Not Found",
- Detail = $"No proof spine found for entry {entry}",
- Status = StatusCodes.Status404NotFound
- });
+ return NotImplementedResult();
}
///
@@ -144,6 +89,8 @@ public class ProofsController : ControllerBase
/// Cancellation token.
/// The VEX statement.
[HttpGet("{entry}/vex")]
+ [Authorize("attestor:read")]
+ [EnableRateLimiting("attestor-reads")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task GetVexAsync(
@@ -151,88 +98,19 @@ public class ProofsController : ControllerBase
CancellationToken ct = default)
{
_logger.LogInformation("Getting VEX for entry {Entry}", entry);
-
- // TODO: Implement VEX retrieval
-
- return NotFound(new ProblemDetails
- {
- Title = "VEX Not Found",
- Detail = $"No VEX statement found for entry {entry}",
- Status = StatusCodes.Status404NotFound
- });
+ return NotImplementedResult();
}
- private static bool IsValidSbomEntryId(string entry)
+ private static ObjectResult NotImplementedResult()
{
- // Format: sha256:<64-hex>:pkg:
- if (string.IsNullOrWhiteSpace(entry))
- return false;
-
- var parts = entry.Split(':', 4);
- if (parts.Length < 4)
- return false;
-
- return parts[0] == "sha256"
- && parts[1].Length == 64
- && parts[1].All(c => "0123456789abcdef".Contains(c))
- && parts[2] == "pkg";
- }
-
- private static string ComputeProofBundleId(string entry, CreateSpineRequest request)
- {
- var evidenceIds = request.EvidenceIds
- .Select(static value => (value ?? string.Empty).Trim())
- .Where(static value => value.Length > 0)
- .Distinct(StringComparer.Ordinal)
- .OrderBy(static value => value, StringComparer.Ordinal);
-
- var material = string.Join(
- "\n",
- new[]
- {
- entry.Trim(),
- request.PolicyVersion.Trim(),
- request.ReasoningId.Trim(),
- request.VexVerdictId.Trim()
- }.Concat(evidenceIds));
-
- var digest = SHA256.HashData(Encoding.UTF8.GetBytes(material));
- return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
- }
-
- private static bool IsValidSha256Id(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
+ return new ObjectResult(new ProblemDetails
{
- return false;
- }
-
- if (!value.StartsWith("sha256:", StringComparison.Ordinal))
+ Title = "Proof chain endpoints are not implemented.",
+ Status = StatusCodes.Status501NotImplemented,
+ Extensions = { ["code"] = "feature_not_implemented" }
+ })
{
- return false;
- }
-
- var hex = value.AsSpan()["sha256:".Length..];
- if (hex.Length != 64)
- {
- return false;
- }
-
- foreach (var c in hex)
- {
- if (c is >= '0' and <= '9')
- {
- continue;
- }
-
- if (c is >= 'a' and <= 'f')
- {
- continue;
- }
-
- return false;
- }
-
- return true;
+ StatusCode = StatusCodes.Status501NotImplemented
+ };
}
}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/VerifyController.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/VerifyController.cs
index 5d0b68429..91fc58f8c 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/VerifyController.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Controllers/VerifyController.cs
@@ -1,4 +1,6 @@
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.RateLimiting;
using StellaOps.Attestor.WebService.Contracts.Proofs;
namespace StellaOps.Attestor.WebService.Controllers;
@@ -27,6 +29,8 @@ public class VerifyController : ControllerBase
/// Cancellation token.
/// The verification receipt.
[HttpPost("{proofBundleId}")]
+ [Authorize("attestor:verify")]
+ [EnableRateLimiting("attestor-verifications")]
[ProducesResponseType(typeof(VerificationReceiptDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -35,88 +39,13 @@ public class VerifyController : ControllerBase
[FromBody] VerifyProofRequest? request,
CancellationToken ct = default)
{
- if (!IsValidSha256Id(proofBundleId))
- {
- return BadRequest(new ProblemDetails
- {
- Title = "Invalid proof bundle ID",
- Detail = "Proof bundle ID must be in format sha256:<64-hex>",
- Status = StatusCodes.Status400BadRequest
- });
- }
-
- request ??= new VerifyProofRequest
- {
- ProofBundleId = proofBundleId
- };
-
_logger.LogInformation("Verifying proof bundle {BundleId}", proofBundleId);
-
- // TODO: Implement using IVerificationPipeline per advisory §9.1
- // Pipeline steps:
- // 1. DSSE signature verification (for each envelope in chain)
- // 2. ID recomputation (verify content-addressed IDs match)
- // 3. Merkle root verification (recompute ProofBundleID)
- // 4. Trust anchor matching (verify signer key is allowed)
- // 5. Rekor inclusion proof verification (if enabled)
- // 6. Policy version compatibility check
- // 7. Key revocation check
-
- var checks = new List
- {
- new()
- {
- Check = "dsse_signature",
- Status = "pass",
- KeyId = "example-key-id"
- },
- new()
- {
- Check = "id_recomputation",
- Status = "pass"
- },
- new()
- {
- Check = "merkle_root",
- Status = "pass"
- },
- new()
- {
- Check = "trust_anchor",
- Status = "pass"
- }
- };
-
- if (request.VerifyRekor)
- {
- checks.Add(new VerificationCheckDto
- {
- Check = "rekor_inclusion",
- Status = "pass",
- LogIndex = 12345678
- });
- }
-
- var receipt = new VerificationReceiptDto
- {
- ProofBundleId = proofBundleId,
- VerifiedAt = DateTimeOffset.UtcNow,
- VerifierVersion = "1.0.0",
- AnchorId = request.AnchorId,
- Result = "pass",
- Checks = checks.ToArray()
- };
-
- return Ok(receipt);
+ return NotImplementedResult();
}
- ///
- /// Verify a DSSE envelope signature.
- ///
- /// The envelope body hash.
- /// Cancellation token.
- /// Signature verification result.
[HttpGet("envelope/{envelopeHash}")]
+ [Authorize("attestor:read")]
+ [EnableRateLimiting("attestor-reads")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task VerifyEnvelopeAsync(
@@ -124,24 +53,12 @@ public class VerifyController : ControllerBase
CancellationToken ct = default)
{
_logger.LogInformation("Verifying envelope {Hash}", envelopeHash);
-
- // TODO: Implement DSSE envelope verification
-
- return NotFound(new ProblemDetails
- {
- Title = "Envelope Not Found",
- Detail = $"No envelope found with hash {envelopeHash}",
- Status = StatusCodes.Status404NotFound
- });
+ return NotImplementedResult();
}
- ///
- /// Verify Rekor inclusion for an envelope.
- ///
- /// The envelope body hash.
- /// Cancellation token.
- /// Rekor verification result.
[HttpGet("rekor/{envelopeHash}")]
+ [Authorize("attestor:read")]
+ [EnableRateLimiting("attestor-reads")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task VerifyRekorAsync(
@@ -149,50 +66,19 @@ public class VerifyController : ControllerBase
CancellationToken ct = default)
{
_logger.LogInformation("Verifying Rekor inclusion for {Hash}", envelopeHash);
-
- // TODO: Implement Rekor inclusion proof verification
-
- return NotFound(new ProblemDetails
- {
- Title = "Rekor Entry Not Found",
- Detail = $"No Rekor entry found for envelope {envelopeHash}",
- Status = StatusCodes.Status404NotFound
- });
+ return NotImplementedResult();
}
- private static bool IsValidSha256Id(string value)
+ private static ObjectResult NotImplementedResult()
{
- if (string.IsNullOrWhiteSpace(value))
+ return new ObjectResult(new ProblemDetails
{
- return false;
- }
-
- if (!value.StartsWith("sha256:", StringComparison.Ordinal))
+ Title = "Verification endpoints are not implemented.",
+ Status = StatusCodes.Status501NotImplemented,
+ Extensions = { ["code"] = "feature_not_implemented" }
+ })
{
- return false;
- }
-
- var hex = value.AsSpan()["sha256:".Length..];
- if (hex.Length != 64)
- {
- return false;
- }
-
- foreach (var c in hex)
- {
- if (c is >= '0' and <= '9')
- {
- continue;
- }
-
- if (c is >= 'a' and <= 'f')
- {
- continue;
- }
-
- return false;
- }
-
- return true;
+ StatusCode = StatusCodes.Status501NotImplemented
+ };
}
}
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Program.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Program.cs
index 3088189de..33fd3e7a3 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Program.cs
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Program.cs
@@ -24,6 +24,7 @@ using OpenTelemetry.Trace;
using StellaOps.Attestor.Core.Observability;
using StellaOps.Attestor.Core.Storage;
using StellaOps.Attestor.Core.Verification;
+using StellaOps.Attestor.WebService;
using StellaOps.Attestor.WebService.Contracts;
using StellaOps.Attestor.Core.Bulk;
using Microsoft.AspNetCore.Server.Kestrel.Https;
@@ -161,13 +162,16 @@ builder.Services.AddScoped("EvidenceLocker:BaseUrl")
+ ?? builder.Configuration.GetValue("EvidenceLockerUrl");
+if (string.IsNullOrWhiteSpace(evidenceLockerUrl))
+{
+ throw new InvalidOperationException("EvidenceLocker base URL must be configured (EvidenceLocker:BaseUrl or EvidenceLockerUrl).");
+}
+
builder.Services.AddHttpClient("EvidenceLocker", client =>
{
- // TODO: Configure base address from configuration
- // For now, use localhost default (will be overridden by actual configuration)
- var evidenceLockerUrl = builder.Configuration.GetValue("EvidenceLockerUrl")
- ?? "http://localhost:9090";
- client.BaseAddress = new Uri(evidenceLockerUrl);
+ client.BaseAddress = new Uri(evidenceLockerUrl, UriKind.Absolute);
client.Timeout = TimeSpan.FromSeconds(30);
});
@@ -374,419 +378,13 @@ app.MapHealthChecks("/health/live");
app.MapControllers();
-app.MapGet("/api/v1/attestations", async (HttpRequest request, IAttestorEntryRepository repository, CancellationToken cancellationToken) =>
-{
- if (!AttestationListContracts.TryBuildQuery(request, out var query, out var error))
- {
- return error!;
- }
-
- var result = await repository.QueryAsync(query, cancellationToken).ConfigureAwait(false);
- var response = new AttestationListResponseDto
- {
- Items = result.Items.Select(MapToListItem).ToList(),
- ContinuationToken = result.ContinuationToken
- };
-
- return Results.Ok(response);
-})
-.RequireAuthorization("attestor:read")
-.RequireRateLimiting("attestor-reads");
-
-app.MapPost("/api/v1/attestations:export", async (HttpContext httpContext, AttestationExportRequestDto? requestDto, IAttestorBundleService bundleService, CancellationToken cancellationToken) =>
-{
- if (httpContext.Request.ContentLength > 0 && !IsJsonContentType(httpContext.Request.ContentType))
- {
- return UnsupportedMediaTypeResult();
- }
-
- AttestorBundleExportRequest request;
- if (requestDto is null)
- {
- request = new AttestorBundleExportRequest();
- }
- else if (!requestDto.TryToDomain(out request, out var error))
- {
- return error!;
- }
-
- var package = await bundleService.ExportAsync(request, cancellationToken).ConfigureAwait(false);
- return Results.Ok(package);
-})
-.RequireAuthorization("attestor:read")
-.RequireRateLimiting("attestor-reads")
-.Produces(StatusCodes.Status200OK);
-
-app.MapPost("/api/v1/attestations:import", async (HttpContext httpContext, AttestorBundlePackage package, IAttestorBundleService bundleService, CancellationToken cancellationToken) =>
-{
- if (!IsJsonContentType(httpContext.Request.ContentType))
- {
- return UnsupportedMediaTypeResult();
- }
-
- var result = await bundleService.ImportAsync(package, cancellationToken).ConfigureAwait(false);
- return Results.Ok(result);
-})
-.RequireAuthorization("attestor:write")
-.RequireRateLimiting("attestor-submissions")
-.Produces(StatusCodes.Status200OK);
-
-app.MapPost("/api/v1/attestations:sign", async (AttestationSignRequestDto? requestDto, HttpContext httpContext, IAttestationSigningService signingService, CancellationToken cancellationToken) =>
-{
- if (requestDto is null)
- {
- return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: "Request body is required.");
- }
-
- if (!IsJsonContentType(httpContext.Request.ContentType))
- {
- return UnsupportedMediaTypeResult();
- }
-
- var certificate = httpContext.Connection.ClientCertificate;
- if (certificate is null)
- {
- return Results.Problem(statusCode: StatusCodes.Status403Forbidden, title: "Client certificate required");
- }
-
- var user = httpContext.User;
- if (user?.Identity is not { IsAuthenticated: true })
- {
- return Results.Problem(statusCode: StatusCodes.Status401Unauthorized, title: "Authentication required");
- }
-
- var signingRequest = new AttestationSignRequest
- {
- KeyId = requestDto.KeyId ?? string.Empty,
- PayloadType = requestDto.PayloadType ?? string.Empty,
- PayloadBase64 = requestDto.Payload ?? string.Empty,
- Mode = requestDto.Mode,
- CertificateChain = requestDto.CertificateChain ?? new List(),
- Artifact = new AttestorSubmissionRequest.ArtifactInfo
- {
- Sha256 = requestDto.Artifact?.Sha256 ?? string.Empty,
- Kind = requestDto.Artifact?.Kind ?? string.Empty,
- ImageDigest = requestDto.Artifact?.ImageDigest,
- SubjectUri = requestDto.Artifact?.SubjectUri
- },
- LogPreference = requestDto.LogPreference ?? "primary",
- Archive = requestDto.Archive ?? true
- };
-
- try
- {
- var submissionContext = BuildSubmissionContext(user, certificate);
- var result = await signingService.SignAsync(signingRequest, submissionContext, cancellationToken).ConfigureAwait(false);
- var response = new AttestationSignResponseDto
- {
- Bundle = result.Bundle,
- Meta = result.Meta,
- Key = new AttestationSignKeyDto
- {
- KeyId = result.KeyId,
- Algorithm = result.Algorithm,
- Mode = result.Mode,
- Provider = result.Provider,
- SignedAt = result.SignedAt.ToString("O")
- }
- };
- return Results.Ok(response);
- }
- catch (AttestorSigningException signingEx)
- {
- return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: signingEx.Message, extensions: new Dictionary
- {
- ["code"] = signingEx.Code
- });
- }
-}).RequireAuthorization("attestor:write")
- .RequireRateLimiting("attestor-submissions");
-
-app.MapPost("/api/v1/rekor/entries", async (AttestorSubmissionRequest request, HttpContext httpContext, IAttestorSubmissionService submissionService, CancellationToken cancellationToken) =>
-{
- if (!IsJsonContentType(httpContext.Request.ContentType))
- {
- return UnsupportedMediaTypeResult();
- }
-
- var certificate = httpContext.Connection.ClientCertificate;
- if (certificate is null)
- {
- return Results.Problem(statusCode: StatusCodes.Status403Forbidden, title: "Client certificate required");
- }
-
- var user = httpContext.User;
- if (user?.Identity is not { IsAuthenticated: true })
- {
- return Results.Problem(statusCode: StatusCodes.Status401Unauthorized, title: "Authentication required");
- }
-
- var submissionContext = BuildSubmissionContext(user, certificate);
-
- try
- {
- var result = await submissionService.SubmitAsync(request, submissionContext, cancellationToken).ConfigureAwait(false);
- return Results.Ok(result);
- }
- catch (AttestorValidationException validationEx)
- {
- return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: validationEx.Message, extensions: new Dictionary
- {
- ["code"] = validationEx.Code
- });
- }
-})
-.RequireAuthorization("attestor:write")
-.RequireRateLimiting("attestor-submissions");
-
-app.MapGet("/api/v1/rekor/entries/{uuid}", async (string uuid, bool? refresh, IAttestorVerificationService verificationService, CancellationToken cancellationToken) =>
- await GetAttestationDetailResultAsync(uuid, refresh is true, verificationService, cancellationToken))
- .RequireAuthorization("attestor:read")
- .RequireRateLimiting("attestor-reads");
-
-app.MapGet("/api/v1/attestations/{uuid}", async (string uuid, bool? refresh, IAttestorVerificationService verificationService, CancellationToken cancellationToken) =>
- await GetAttestationDetailResultAsync(uuid, refresh is true, verificationService, cancellationToken))
- .RequireAuthorization("attestor:read")
- .RequireRateLimiting("attestor-reads");
-
-app.MapPost("/api/v1/rekor/verify", async (HttpContext httpContext, AttestorVerificationRequest verifyRequest, IAttestorVerificationService verificationService, CancellationToken cancellationToken) =>
-{
- if (!IsJsonContentType(httpContext.Request.ContentType))
- {
- return UnsupportedMediaTypeResult();
- }
-
- try
- {
- var result = await verificationService.VerifyAsync(verifyRequest, cancellationToken).ConfigureAwait(false);
- return Results.Ok(result);
- }
- catch (AttestorVerificationException ex)
- {
- return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, extensions: new Dictionary
- {
- ["code"] = ex.Code
- });
- }
-})
-.RequireAuthorization("attestor:verify")
-.RequireRateLimiting("attestor-verifications");
-
-app.MapPost("/api/v1/rekor/verify:bulk", async (
- BulkVerificationRequestDto? requestDto,
- HttpContext httpContext,
- IBulkVerificationJobStore jobStore,
- CancellationToken cancellationToken) =>
-{
- var context = BuildBulkJobContext(httpContext.User);
-
- if (!BulkVerificationContracts.TryBuildJob(requestDto, attestorOptions, context, out var job, out var error))
- {
- return error!;
- }
-
- var queued = await jobStore.CountQueuedAsync(cancellationToken).ConfigureAwait(false);
- if (queued >= Math.Max(1, attestorOptions.Quotas.Bulk.MaxQueuedJobs))
- {
- return Results.Problem(statusCode: StatusCodes.Status429TooManyRequests, title: "Too many bulk verification jobs queued. Try again later.");
- }
-
- job = await jobStore.CreateAsync(job!, cancellationToken).ConfigureAwait(false);
- var response = BulkVerificationContracts.MapJob(job);
- return Results.Accepted($"/api/v1/rekor/verify:bulk/{job.Id}", response);
-}).RequireAuthorization("attestor:write")
- .RequireRateLimiting("attestor-bulk");
-
-app.MapGet("/api/v1/rekor/verify:bulk/{jobId}", async (
- string jobId,
- HttpContext httpContext,
- IBulkVerificationJobStore jobStore,
- CancellationToken cancellationToken) =>
-{
- if (string.IsNullOrWhiteSpace(jobId))
- {
- return Results.NotFound();
- }
-
- var job = await jobStore.GetAsync(jobId, cancellationToken).ConfigureAwait(false);
- if (job is null || !IsAuthorizedForJob(job, httpContext.User))
- {
- return Results.NotFound();
- }
-
- return Results.Ok(BulkVerificationContracts.MapJob(job));
-}).RequireAuthorization("attestor:write");
+app.MapAttestorEndpoints(attestorOptions);
// Refresh Router endpoint cache
app.TryRefreshStellaRouterEndpoints(routerOptions);
app.Run();
-static async Task GetAttestationDetailResultAsync(
- string uuid,
- bool refresh,
- IAttestorVerificationService verificationService,
- CancellationToken cancellationToken)
-{
- var entry = await verificationService.GetEntryAsync(uuid, refresh, cancellationToken).ConfigureAwait(false);
- if (entry is null)
- {
- return Results.NotFound();
- }
-
- return Results.Ok(MapAttestationDetail(entry));
-}
-
-static object MapAttestationDetail(AttestorEntry entry)
-{
- return new
- {
- uuid = entry.RekorUuid,
- index = entry.Index,
- backend = entry.Log.Backend,
- proof = entry.Proof is null ? null : new
- {
- checkpoint = entry.Proof.Checkpoint is null ? null : new
- {
- origin = entry.Proof.Checkpoint.Origin,
- size = entry.Proof.Checkpoint.Size,
- rootHash = entry.Proof.Checkpoint.RootHash,
- timestamp = entry.Proof.Checkpoint.Timestamp?.ToString("O")
- },
- inclusion = entry.Proof.Inclusion is null ? null : new
- {
- leafHash = entry.Proof.Inclusion.LeafHash,
- path = entry.Proof.Inclusion.Path
- }
- },
- logURL = entry.Log.Url,
- status = entry.Status,
- mirror = entry.Mirror is null ? null : new
- {
- backend = entry.Mirror.Backend,
- uuid = entry.Mirror.Uuid,
- index = entry.Mirror.Index,
- logURL = entry.Mirror.Url,
- status = entry.Mirror.Status,
- proof = entry.Mirror.Proof is null ? null : new
- {
- checkpoint = entry.Mirror.Proof.Checkpoint is null ? null : new
- {
- origin = entry.Mirror.Proof.Checkpoint.Origin,
- size = entry.Mirror.Proof.Checkpoint.Size,
- rootHash = entry.Mirror.Proof.Checkpoint.RootHash,
- timestamp = entry.Mirror.Proof.Checkpoint.Timestamp?.ToString("O")
- },
- inclusion = entry.Mirror.Proof.Inclusion is null ? null : new
- {
- leafHash = entry.Mirror.Proof.Inclusion.LeafHash,
- path = entry.Mirror.Proof.Inclusion.Path
- }
- },
- error = entry.Mirror.Error
- },
- artifact = new
- {
- sha256 = entry.Artifact.Sha256,
- kind = entry.Artifact.Kind,
- imageDigest = entry.Artifact.ImageDigest,
- subjectUri = entry.Artifact.SubjectUri
- }
- };
-}
-
-static AttestationListItemDto MapToListItem(AttestorEntry entry)
-{
- return new AttestationListItemDto
- {
- Uuid = entry.RekorUuid,
- Status = entry.Status,
- CreatedAt = entry.CreatedAt.ToString("O"),
- Artifact = new AttestationArtifactDto
- {
- Sha256 = entry.Artifact.Sha256,
- Kind = entry.Artifact.Kind,
- ImageDigest = entry.Artifact.ImageDigest,
- SubjectUri = entry.Artifact.SubjectUri
- },
- Signer = new AttestationSignerDto
- {
- Mode = entry.SignerIdentity.Mode,
- Issuer = entry.SignerIdentity.Issuer,
- Subject = entry.SignerIdentity.SubjectAlternativeName,
- KeyId = entry.SignerIdentity.KeyId
- },
- Log = new AttestationLogDto
- {
- Backend = entry.Log.Backend,
- Url = entry.Log.Url,
- Index = entry.Index,
- Status = entry.Status
- },
- Mirror = entry.Mirror is null ? null : new AttestationLogDto
- {
- Backend = entry.Mirror.Backend,
- Url = entry.Mirror.Url,
- Index = entry.Mirror.Index,
- Status = entry.Mirror.Status
- }
- };
-}
-
-static SubmissionContext BuildSubmissionContext(ClaimsPrincipal user, X509Certificate2 certificate)
-{
- var subject = user.FindFirst("sub")?.Value ?? certificate.Subject;
- var audience = user.FindFirst("aud")?.Value ?? string.Empty;
- var clientId = user.FindFirst("client_id")?.Value;
- var tenant = user.FindFirst("tenant")?.Value;
-
- return new SubmissionContext
- {
- CallerSubject = subject,
- CallerAudience = audience,
- CallerClientId = clientId,
- CallerTenant = tenant,
- ClientCertificate = certificate,
- MtlsThumbprint = certificate.Thumbprint
- };
-}
-
-static BulkVerificationJobContext BuildBulkJobContext(ClaimsPrincipal user)
-{
- var scopes = user.FindAll("scope")
- .Select(claim => claim.Value)
- .Where(value => !string.IsNullOrWhiteSpace(value))
- .ToList();
-
- return new BulkVerificationJobContext
- {
- Tenant = user.FindFirst("tenant")?.Value,
- RequestedBy = user.FindFirst("sub")?.Value,
- ClientId = user.FindFirst("client_id")?.Value,
- Scopes = scopes
- };
-}
-
-static bool IsAuthorizedForJob(BulkVerificationJob job, ClaimsPrincipal user)
-{
- var tenant = user.FindFirst("tenant")?.Value;
- if (!string.IsNullOrEmpty(job.Context.Tenant) &&
- !string.Equals(job.Context.Tenant, tenant, StringComparison.Ordinal))
- {
- return false;
- }
-
- var subject = user.FindFirst("sub")?.Value;
- if (!string.IsNullOrEmpty(job.Context.RequestedBy) &&
- !string.Equals(job.Context.RequestedBy, subject, StringComparison.Ordinal))
- {
- return false;
- }
-
- return true;
-}
-
-
static List LoadClientCertificateAuthorities(string? path)
{
var certificates = new List();
@@ -857,34 +455,6 @@ static IEnumerable ExtractScopes(ClaimsPrincipal user)
}
}
-static bool IsJsonContentType(string? contentType)
-{
- if (string.IsNullOrWhiteSpace(contentType))
- {
- return false;
- }
-
- var mediaType = contentType.Split(';', 2)[0].Trim();
- if (mediaType.Length == 0)
- {
- return false;
- }
-
- return mediaType.EndsWith("/json", StringComparison.OrdinalIgnoreCase)
- || mediaType.Contains("+json", StringComparison.OrdinalIgnoreCase);
-}
-
-static IResult UnsupportedMediaTypeResult()
-{
- return Results.Problem(
- statusCode: StatusCodes.Status415UnsupportedMediaType,
- title: "Unsupported content type. Submit application/json payloads.",
- extensions: new Dictionary
- {
- ["code"] = "unsupported_media_type"
- });
-}
-
internal sealed class NoAuthHandler : AuthenticationHandler
{
public const string SchemeName = "NoAuth";
@@ -909,3 +479,7 @@ internal sealed class NoAuthHandler : AuthenticationHandlerpreview
enable
enable
- false
+ true
diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/TASKS.md b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/TASKS.md
index 6f70a723c..aec3bdc69 100644
--- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/TASKS.md
+++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/TASKS.md
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0072-M | DONE | Maintainability audit for StellaOps.Attestor.WebService. |
| AUDIT-0072-T | DONE | Test coverage audit for StellaOps.Attestor.WebService. |
-| AUDIT-0072-A | TODO | Pending approval for changes. |
+| AUDIT-0072-A | DOING | Addressing WebService audit findings. |
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/AttestationBundler.cs b/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/AttestationBundler.cs
index e99b56c8b..c1148408f 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/AttestationBundler.cs
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/AttestationBundler.cs
@@ -28,6 +28,7 @@ public sealed class AttestationBundler : IAttestationBundler
private readonly IMerkleTreeBuilder _merkleBuilder;
private readonly ILogger _logger;
private readonly BundlingOptions _options;
+ private readonly TimeProvider _timeProvider;
///
/// Create a new attestation bundler.
@@ -38,7 +39,8 @@ public sealed class AttestationBundler : IAttestationBundler
IMerkleTreeBuilder merkleBuilder,
ILogger logger,
IOptions options,
- IOrgKeySigner? orgSigner = null)
+ IOrgKeySigner? orgSigner = null,
+ TimeProvider? timeProvider = null)
{
_aggregator = aggregator ?? throw new ArgumentNullException(nameof(aggregator));
_store = store ?? throw new ArgumentNullException(nameof(store));
@@ -46,6 +48,7 @@ public sealed class AttestationBundler : IAttestationBundler
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options?.Value ?? new BundlingOptions();
_orgSigner = orgSigner;
+ _timeProvider = timeProvider ?? TimeProvider.System;
}
///
@@ -60,13 +63,42 @@ public sealed class AttestationBundler : IAttestationBundler
request.PeriodStart,
request.PeriodEnd);
- // Collect attestations in deterministic order
- var attestations = await CollectAttestationsAsync(request, cancellationToken);
-
- if (attestations.Count == 0)
+ if (request.PeriodStart > request.PeriodEnd)
{
- _logger.LogWarning("No attestations found for the specified period");
- throw new InvalidOperationException("No attestations found for the specified period.");
+ throw new ArgumentException(
+ "PeriodStart must be less than or equal to PeriodEnd.",
+ nameof(request));
+ }
+
+ var effectivePeriodStart = request.PeriodStart;
+ var lookbackDays = _options.Aggregation.LookbackDays;
+ if (lookbackDays > 0)
+ {
+ var lookbackStart = request.PeriodEnd.AddDays(-lookbackDays);
+ if (effectivePeriodStart < lookbackStart)
+ {
+ _logger.LogDebug(
+ "Clamping period start from {RequestedStart} to {EffectiveStart} to honor lookback window.",
+ request.PeriodStart,
+ lookbackStart);
+ effectivePeriodStart = lookbackStart;
+ }
+ }
+
+ // Collect attestations in deterministic order
+ var attestations = await CollectAttestationsAsync(
+ request with { PeriodStart = effectivePeriodStart },
+ cancellationToken);
+
+ var minimumAttestations = Math.Max(1, _options.Aggregation.MinAttestationsForBundle);
+ if (attestations.Count < minimumAttestations)
+ {
+ _logger.LogWarning(
+ "Insufficient attestations for bundling. Required {Required}, found {Found}.",
+ minimumAttestations,
+ attestations.Count);
+ throw new InvalidOperationException(
+ $"Insufficient attestations for bundling. Required {minimumAttestations}, found {attestations.Count}.");
}
_logger.LogInformation("Collected {Count} attestations for bundling", attestations.Count);
@@ -83,8 +115,8 @@ public sealed class AttestationBundler : IAttestationBundler
{
BundleId = bundleId,
Version = "1.0",
- CreatedAt = DateTimeOffset.UtcNow,
- PeriodStart = request.PeriodStart,
+ CreatedAt = _timeProvider.GetUtcNow(),
+ PeriodStart = effectivePeriodStart,
PeriodEnd = request.PeriodEnd,
AttestationCount = attestations.Count,
TenantId = request.TenantId
@@ -104,6 +136,11 @@ public sealed class AttestationBundler : IAttestationBundler
};
// Sign with organization key if requested
+ if (request.SignWithOrgKey && _orgSigner == null)
+ {
+ throw new InvalidOperationException("Organization signer is not configured.");
+ }
+
if (request.SignWithOrgKey && _orgSigner != null)
{
bundle = await SignBundleAsync(bundle, request.OrgKeyId, cancellationToken);
@@ -146,14 +183,22 @@ public sealed class AttestationBundler : IAttestationBundler
ArgumentNullException.ThrowIfNull(bundle);
var issues = new List();
- var verifiedAt = DateTimeOffset.UtcNow;
+ var verifiedAt = _timeProvider.GetUtcNow();
// Verify Merkle root
var merkleValid = VerifyMerkleRoot(bundle, issues);
// Verify org signature if present
bool? orgSigValid = null;
- if (bundle.OrgSignature != null && _orgSigner != null)
+ if (bundle.OrgSignature != null && _orgSigner == null)
+ {
+ issues.Add(new BundleVerificationIssue(
+ VerificationIssueSeverity.Critical,
+ "ORG_SIG_VERIFIER_UNAVAILABLE",
+ "Organization signature present but no signer is configured for verification."));
+ orgSigValid = false;
+ }
+ else if (bundle.OrgSignature != null && _orgSigner != null)
{
orgSigValid = await VerifyOrgSignatureAsync(bundle, issues, cancellationToken);
}
@@ -236,11 +281,19 @@ public sealed class AttestationBundler : IAttestationBundler
keyId);
// Return bundle with signature and updated metadata
+ var fingerprint = await GetKeyFingerprintAsync(keyId, cancellationToken);
+ if (fingerprint == null)
+ {
+ _logger.LogWarning(
+ "Organization key fingerprint not found for key {KeyId}; leaving fingerprint unset.",
+ keyId);
+ }
+
return bundle with
{
Metadata = bundle.Metadata with
{
- OrgKeyFingerprint = $"sha256:{ComputeKeyFingerprint(keyId)}"
+ OrgKeyFingerprint = fingerprint
},
OrgSignature = signature
};
@@ -328,10 +381,17 @@ public sealed class AttestationBundler : IAttestationBundler
return SHA256.HashData(Encoding.UTF8.GetBytes(sb.ToString()));
}
- private static string ComputeKeyFingerprint(string keyId)
+ private async Task GetKeyFingerprintAsync(
+ string keyId,
+ CancellationToken cancellationToken)
{
- // Simple fingerprint - in production this would use the actual public key
- var hash = SHA256.HashData(Encoding.UTF8.GetBytes(keyId));
- return Convert.ToHexString(hash[..16]).ToLowerInvariant();
+ if (_orgSigner == null)
+ {
+ return null;
+ }
+
+ var keys = await _orgSigner.ListKeysAsync(cancellationToken) ?? Array.Empty();
+ var match = keys.FirstOrDefault(key => string.Equals(key.KeyId, keyId, StringComparison.Ordinal));
+ return match?.Fingerprint;
}
}
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/OfflineKitBundleProvider.cs b/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/OfflineKitBundleProvider.cs
index 5179f09e3..ce21de620 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/OfflineKitBundleProvider.cs
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/OfflineKitBundleProvider.cs
@@ -120,15 +120,18 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
private readonly IBundleStore _bundleStore;
private readonly BundlingOptions _options;
private readonly ILogger _logger;
+ private readonly TimeProvider _timeProvider;
public OfflineKitBundleProvider(
IBundleStore bundleStore,
IOptions options,
- ILogger logger)
+ ILogger logger,
+ TimeProvider? timeProvider = null)
{
_bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore));
_options = options?.Value ?? new BundlingOptions();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _timeProvider = timeProvider ?? TimeProvider.System;
}
///
@@ -137,7 +140,7 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
OfflineKitExportOptions? options = null,
CancellationToken cancellationToken = default)
{
- options ??= new OfflineKitExportOptions();
+ options = ResolveExportOptions(options);
if (!_options.Export.IncludeInOfflineKit)
{
@@ -147,7 +150,7 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
Bundles = [],
TotalAttestations = 0,
TotalSizeBytes = 0,
- ExportedAt = DateTimeOffset.UtcNow
+ ExportedAt = _timeProvider.GetUtcNow()
};
}
@@ -203,7 +206,7 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
Bundles = exportedBundles,
TotalAttestations = totalAttestations,
TotalSizeBytes = totalSize,
- ExportedAt = DateTimeOffset.UtcNow
+ ExportedAt = _timeProvider.GetUtcNow()
};
}
@@ -212,9 +215,9 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
OfflineKitExportOptions? options = null,
CancellationToken cancellationToken = default)
{
- options ??= new OfflineKitExportOptions();
+ options = ResolveExportOptions(options);
- var cutoffDate = DateTimeOffset.UtcNow.AddMonths(-options.MaxAgeMonths);
+ var cutoffDate = _timeProvider.GetUtcNow().AddMonths(-options.MaxAgeMonths);
var result = new List();
string? cursor = null;
@@ -303,4 +306,58 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
return $"bundle-{hash}{extension}{compression}";
}
+
+ private OfflineKitExportOptions ResolveExportOptions(OfflineKitExportOptions? options)
+ {
+ if (options != null)
+ {
+ return options;
+ }
+
+ return new OfflineKitExportOptions
+ {
+ MaxAgeMonths = _options.Export.MaxAgeMonths,
+ Format = ParseFormat(_options.Export.SupportedFormats ?? new List()),
+ Compression = ParseCompression(_options.Export.Compression),
+ RequireOrgSignature = false,
+ TenantId = null
+ };
+ }
+
+ private static BundleFormat ParseFormat(IList supportedFormats)
+ {
+ if (supportedFormats.Count == 0)
+ {
+ return BundleFormat.Json;
+ }
+
+ var format = supportedFormats
+ .FirstOrDefault(value => value.Equals("json", StringComparison.OrdinalIgnoreCase))
+ ?? supportedFormats.FirstOrDefault()
+ ?? "json";
+
+ return format.Equals("cbor", StringComparison.OrdinalIgnoreCase)
+ ? BundleFormat.Cbor
+ : BundleFormat.Json;
+ }
+
+ private static BundleCompression ParseCompression(string? compression)
+ {
+ if (string.IsNullOrWhiteSpace(compression))
+ {
+ return BundleCompression.None;
+ }
+
+ if (compression.Equals("gzip", StringComparison.OrdinalIgnoreCase))
+ {
+ return BundleCompression.Gzip;
+ }
+
+ if (compression.Equals("zstd", StringComparison.OrdinalIgnoreCase))
+ {
+ return BundleCompression.Zstd;
+ }
+
+ return BundleCompression.None;
+ }
}
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/RetentionPolicyEnforcer.cs b/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/RetentionPolicyEnforcer.cs
index c579d0a06..f80787ef7 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/RetentionPolicyEnforcer.cs
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/Services/RetentionPolicyEnforcer.cs
@@ -164,25 +164,28 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
private readonly IBundleExpiryNotifier? _notifier;
private readonly BundleRetentionOptions _options;
private readonly ILogger _logger;
+ private readonly TimeProvider _timeProvider;
public RetentionPolicyEnforcer(
IBundleStore bundleStore,
IOptions options,
ILogger logger,
IBundleArchiver? archiver = null,
- IBundleExpiryNotifier? notifier = null)
+ IBundleExpiryNotifier? notifier = null,
+ TimeProvider? timeProvider = null)
{
_bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore));
_options = options?.Value?.Retention ?? new BundleRetentionOptions();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_archiver = archiver;
_notifier = notifier;
+ _timeProvider = timeProvider ?? TimeProvider.System;
}
///
public async Task EnforceAsync(CancellationToken cancellationToken = default)
{
- var startedAt = DateTimeOffset.UtcNow;
+ var startedAt = _timeProvider.GetUtcNow();
var failures = new List();
int evaluated = 0;
int deleted = 0;
@@ -196,7 +199,7 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
return new RetentionEnforcementResult
{
StartedAt = startedAt,
- CompletedAt = DateTimeOffset.UtcNow,
+ CompletedAt = _timeProvider.GetUtcNow(),
BundlesEvaluated = 0,
BundlesDeleted = 0,
BundlesArchived = 0,
@@ -213,10 +216,11 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
// Process bundles in batches
string? cursor = null;
- var now = DateTimeOffset.UtcNow;
+ var now = _timeProvider.GetUtcNow();
var notificationCutoff = now.AddDays(_options.NotifyDaysBeforeExpiry);
var gracePeriodCutoff = now.AddDays(-_options.GracePeriodDays);
var expiredNotifications = new List();
+ var applyOverrides = _options.TenantOverrides.Count > 0 || _options.PredicateTypeOverrides.Count > 0;
do
{
@@ -227,7 +231,29 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
foreach (var bundle in listResult.Bundles)
{
evaluated++;
- var expiryDate = CalculateExpiryDate(bundle);
+ string? tenantId = null;
+ IReadOnlyList? predicateTypes = null;
+
+ if (applyOverrides)
+ {
+ var fullBundle = await _bundleStore.GetBundleAsync(bundle.BundleId, cancellationToken);
+ if (fullBundle == null)
+ {
+ failures.Add(new BundleEnforcementFailure(
+ bundle.BundleId,
+ "Bundle not found",
+ "Failed to load bundle metadata for retention overrides."));
+ continue;
+ }
+
+ tenantId = fullBundle.Metadata.TenantId;
+ predicateTypes = fullBundle.Attestations
+ .Select(attestation => attestation.PredicateType)
+ .Distinct(StringComparer.Ordinal)
+ .ToList();
+ }
+
+ var expiryDate = CalculateExpiryDate(tenantId, predicateTypes, bundle.CreatedAt);
// Check if bundle has expired
if (expiryDate <= now)
@@ -300,7 +326,7 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
}
}
- var completedAt = DateTimeOffset.UtcNow;
+ var completedAt = _timeProvider.GetUtcNow();
_logger.LogInformation(
"Retention enforcement completed. Evaluated={Evaluated}, Deleted={Deleted}, Archived={Archived}, Marked={Marked}, Approaching={Approaching}, Failed={Failed}",
evaluated, deleted, archived, markedExpired, approachingExpiry, failures.Count);
@@ -324,9 +350,10 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
CancellationToken cancellationToken = default)
{
var notifications = new List();
- var now = DateTimeOffset.UtcNow;
+ var now = _timeProvider.GetUtcNow();
var cutoff = now.AddDays(daysBeforeExpiry);
string? cursor = null;
+ var applyOverrides = _options.TenantOverrides.Count > 0 || _options.PredicateTypeOverrides.Count > 0;
do
{
@@ -336,7 +363,25 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
foreach (var bundle in listResult.Bundles)
{
- var expiryDate = CalculateExpiryDate(bundle);
+ string? tenantId = null;
+ IReadOnlyList? predicateTypes = null;
+
+ if (applyOverrides)
+ {
+ var fullBundle = await _bundleStore.GetBundleAsync(bundle.BundleId, cancellationToken);
+ if (fullBundle == null)
+ {
+ continue;
+ }
+
+ tenantId = fullBundle.Metadata.TenantId;
+ predicateTypes = fullBundle.Attestations
+ .Select(attestation => attestation.PredicateType)
+ .Distinct(StringComparer.Ordinal)
+ .ToList();
+ }
+
+ var expiryDate = CalculateExpiryDate(tenantId, predicateTypes, bundle.CreatedAt);
if (expiryDate > now && expiryDate <= cutoff)
{
notifications.Add(new BundleExpiryNotification(
@@ -364,17 +409,51 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
///
public DateTimeOffset CalculateExpiryDate(string? tenantId, DateTimeOffset createdAt)
{
- int retentionMonths = _options.DefaultMonths;
+ var retentionMonths = ResolveRetentionMonths(tenantId, null);
+
+ return createdAt.AddMonths(retentionMonths);
+ }
+
+ private DateTimeOffset CalculateExpiryDate(
+ string? tenantId,
+ IReadOnlyList? predicateTypes,
+ DateTimeOffset createdAt)
+ {
+ var retentionMonths = ResolveRetentionMonths(tenantId, predicateTypes);
+ return createdAt.AddMonths(retentionMonths);
+ }
+
+ private int ResolveRetentionMonths(
+ string? tenantId,
+ IReadOnlyList? predicateTypes)
+ {
+ var retentionMonths = ClampRetentionMonths(_options.DefaultMonths);
// Check for tenant-specific override
if (!string.IsNullOrEmpty(tenantId) &&
_options.TenantOverrides.TryGetValue(tenantId, out var tenantMonths))
{
- retentionMonths = Math.Max(tenantMonths, _options.MinimumMonths);
- retentionMonths = Math.Min(retentionMonths, _options.MaximumMonths);
+ retentionMonths = ClampRetentionMonths(tenantMonths);
}
- return createdAt.AddMonths(retentionMonths);
+ if (predicateTypes != null && _options.PredicateTypeOverrides.Count > 0)
+ {
+ foreach (var predicateType in predicateTypes)
+ {
+ if (_options.PredicateTypeOverrides.TryGetValue(predicateType, out var predicateMonths))
+ {
+ retentionMonths = Math.Max(retentionMonths, ClampRetentionMonths(predicateMonths));
+ }
+ }
+ }
+
+ return retentionMonths;
+ }
+
+ private int ClampRetentionMonths(int months)
+ {
+ var clamped = Math.Max(months, _options.MinimumMonths);
+ return Math.Min(clamped, _options.MaximumMonths);
}
private async Task<(bool Success, BundleEnforcementFailure? Failure)> HandleExpiredBundleAsync(
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/StellaOps.Attestor.Bundling.csproj b/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/StellaOps.Attestor.Bundling.csproj
index 5681e5a66..4d86c6096 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/StellaOps.Attestor.Bundling.csproj
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/StellaOps.Attestor.Bundling.csproj
@@ -6,10 +6,10 @@
enable
StellaOps.Attestor.Bundling
Attestation bundle aggregation and rotation for long-term verification in air-gapped environments.
+ true
-
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/StellaOps.Attestor.Bundling.csproj.Backup.tmp b/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/StellaOps.Attestor.Bundling.csproj.Backup.tmp
deleted file mode 100644
index 08bdc3cb1..000000000
--- a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/StellaOps.Attestor.Bundling.csproj.Backup.tmp
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- net10.0
- enable
- enable
- StellaOps.Attestor.Bundling
- Attestation bundle aggregation and rotation for long-term verification in air-gapped environments.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/TASKS.md b/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/TASKS.md
index 3d7570a9a..2134ceceb 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/TASKS.md
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.Bundling/TASKS.md
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0047-M | DONE | Maintainability audit for StellaOps.Attestor.Bundling. |
| AUDIT-0047-T | DONE | Test coverage audit for StellaOps.Attestor.Bundling. |
-| AUDIT-0047-A | TODO | Pending approval for changes. |
+| AUDIT-0047-A | DONE | Applied bundling validation, defaults, and test coverage updates. |
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/GraphRootAttestor.cs b/src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/GraphRootAttestor.cs
index 22a91f924..671638578 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/GraphRootAttestor.cs
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/GraphRootAttestor.cs
@@ -39,6 +39,7 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
private readonly Func _keyResolver;
private readonly IRekorClient? _rekorClient;
private readonly GraphRootAttestorOptions _options;
+ private readonly TimeProvider _timeProvider;
private readonly ILogger _logger;
///
@@ -56,7 +57,8 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
Func keyResolver,
ILogger logger,
IRekorClient? rekorClient = null,
- IOptions? options = null)
+ IOptions? options = null,
+ TimeProvider? timeProvider = null)
{
_merkleComputer = merkleComputer ?? throw new ArgumentNullException(nameof(merkleComputer));
_signatureService = signatureService ?? throw new ArgumentNullException(nameof(signatureService));
@@ -64,6 +66,7 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_rekorClient = rekorClient;
_options = options?.Value ?? new GraphRootAttestorOptions();
+ _timeProvider = timeProvider ?? TimeProvider.System;
}
///
@@ -91,14 +94,20 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
.OrderBy(x => x, StringComparer.Ordinal)
.ToList();
+ var normalizedPolicyDigest = NormalizeDigest(request.PolicyDigest);
+ var normalizedFeedsDigest = NormalizeDigest(request.FeedsDigest);
+ var normalizedToolchainDigest = NormalizeDigest(request.ToolchainDigest);
+ var normalizedParamsDigest = NormalizeDigest(request.ParamsDigest);
+
// 2. Build leaf data for Merkle tree
var leaves = BuildLeaves(
sortedNodeIds,
sortedEdgeIds,
- request.PolicyDigest,
- request.FeedsDigest,
- request.ToolchainDigest,
- request.ParamsDigest);
+ sortedEvidenceIds,
+ normalizedPolicyDigest,
+ normalizedFeedsDigest,
+ normalizedToolchainDigest,
+ normalizedParamsDigest);
// 3. Compute Merkle root
var rootBytes = _merkleComputer.ComputeRoot(leaves);
@@ -108,7 +117,7 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
_logger.LogDebug("Computed Merkle root: {RootHash}", rootHash);
// 4. Build in-toto statement
- var computedAt = DateTimeOffset.UtcNow;
+ var computedAt = _timeProvider.GetUtcNow();
var attestation = BuildAttestation(
request,
sortedNodeIds,
@@ -116,6 +125,10 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
sortedEvidenceIds,
rootHash,
rootHex,
+ normalizedPolicyDigest,
+ normalizedFeedsDigest,
+ normalizedToolchainDigest,
+ normalizedParamsDigest,
computedAt);
// 5. Canonicalize the attestation
@@ -129,7 +142,7 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
$"Unable to resolve signing key: {request.SigningKeyId ?? "(default)"}");
}
- var signResult = _signatureService.Sign(payload, key, ct);
+ var signResult = _signatureService.SignDsse(PayloadType, payload, key, ct);
if (!signResult.IsSuccess)
{
throw new InvalidOperationException(
@@ -260,8 +273,8 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
};
// Compute bundle hash
- var bundleJson = JsonSerializer.Serialize(EnvDsseEnvelope);
- var bundleHash = SHA256.HashData(Encoding.UTF8.GetBytes(bundleJson));
+ var bundleJson = CanonJson.Canonicalize(EnvDsseEnvelope);
+ var bundleHash = SHA256.HashData(bundleJson);
return new AttestorSubmissionRequest
{
@@ -303,6 +316,24 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
nodes.Count,
edges.Count);
+ if (!string.Equals(envelope.PayloadType, PayloadType, StringComparison.Ordinal))
+ {
+ return new GraphRootVerificationResult
+ {
+ IsValid = false,
+ FailureReason = $"Unexpected payloadType '{envelope.PayloadType}'."
+ };
+ }
+
+ if (!TryVerifyEnvelopeSignatures(envelope, ct, out var signatureFailure))
+ {
+ return new GraphRootVerificationResult
+ {
+ IsValid = false,
+ FailureReason = signatureFailure ?? "No valid DSSE signatures found."
+ };
+ }
+
// 1. Deserialize attestation from envelope payload
GraphRootAttestation? attestation;
try
@@ -336,15 +367,69 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
.Select(e => e.EdgeId)
.OrderBy(x => x, StringComparer.Ordinal)
.ToList();
+ var predicateNodeIds = attestation.Predicate.NodeIds?.ToList() ?? [];
+ var predicateEdgeIds = attestation.Predicate.EdgeIds?.ToList() ?? [];
+ var predicateEvidenceIds = attestation.Predicate.EvidenceIds?.ToList() ?? [];
+
+ if (!SequenceEqual(predicateNodeIds, recomputedNodeIds))
+ {
+ return new GraphRootVerificationResult
+ {
+ IsValid = false,
+ FailureReason = "Predicate node IDs do not match provided graph data."
+ };
+ }
+
+ if (!SequenceEqual(predicateEdgeIds, recomputedEdgeIds))
+ {
+ return new GraphRootVerificationResult
+ {
+ IsValid = false,
+ FailureReason = "Predicate edge IDs do not match provided graph data."
+ };
+ }
+
+ var sortedPredicateEvidenceIds = predicateEvidenceIds
+ .OrderBy(x => x, StringComparer.Ordinal)
+ .ToList();
+ if (!SequenceEqual(predicateEvidenceIds, sortedPredicateEvidenceIds))
+ {
+ return new GraphRootVerificationResult
+ {
+ IsValid = false,
+ FailureReason = "Predicate evidence IDs are not in deterministic order."
+ };
+ }
+
+ string normalizedPolicyDigest;
+ string normalizedFeedsDigest;
+ string normalizedToolchainDigest;
+ string normalizedParamsDigest;
+ try
+ {
+ normalizedPolicyDigest = NormalizeDigest(attestation.Predicate.Inputs.PolicyDigest);
+ normalizedFeedsDigest = NormalizeDigest(attestation.Predicate.Inputs.FeedsDigest);
+ normalizedToolchainDigest = NormalizeDigest(attestation.Predicate.Inputs.ToolchainDigest);
+ normalizedParamsDigest = NormalizeDigest(attestation.Predicate.Inputs.ParamsDigest);
+ }
+ catch (ArgumentException ex)
+ {
+ return new GraphRootVerificationResult
+ {
+ IsValid = false,
+ FailureReason = $"Invalid predicate digest: {ex.Message}"
+ };
+ }
// 3. Build leaves using the same inputs from the attestation
var leaves = BuildLeaves(
recomputedNodeIds,
recomputedEdgeIds,
- attestation.Predicate.Inputs.PolicyDigest,
- attestation.Predicate.Inputs.FeedsDigest,
- attestation.Predicate.Inputs.ToolchainDigest,
- attestation.Predicate.Inputs.ParamsDigest);
+ sortedPredicateEvidenceIds,
+ normalizedPolicyDigest,
+ normalizedFeedsDigest,
+ normalizedToolchainDigest,
+ normalizedParamsDigest);
// 4. Compute Merkle root
var recomputedRootBytes = _merkleComputer.ComputeRoot(leaves);
@@ -385,13 +470,14 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
private static List> BuildLeaves(
IReadOnlyList sortedNodeIds,
IReadOnlyList sortedEdgeIds,
+ IReadOnlyList sortedEvidenceIds,
string policyDigest,
string feedsDigest,
string toolchainDigest,
string paramsDigest)
{
var leaves = new List>(
- sortedNodeIds.Count + sortedEdgeIds.Count + 4);
+ sortedNodeIds.Count + sortedEdgeIds.Count + sortedEvidenceIds.Count + 4);
// Add node IDs
foreach (var nodeId in sortedNodeIds)
@@ -405,6 +491,12 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
leaves.Add(Encoding.UTF8.GetBytes(edgeId));
}
+ // Add evidence IDs
+ foreach (var evidenceId in sortedEvidenceIds)
+ {
+ leaves.Add(Encoding.UTF8.GetBytes(evidenceId));
+ }
+
// Add input digests (deterministic order)
leaves.Add(Encoding.UTF8.GetBytes(policyDigest));
leaves.Add(Encoding.UTF8.GetBytes(feedsDigest));
@@ -421,6 +513,10 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
IReadOnlyList sortedEvidenceIds,
string rootHash,
string rootHex,
+ string policyDigest,
+ string feedsDigest,
+ string toolchainDigest,
+ string paramsDigest,
DateTimeOffset computedAt)
{
var subjects = new List
@@ -457,10 +553,10 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
EdgeIds = sortedEdgeIds,
Inputs = new GraphInputDigests
{
- PolicyDigest = request.PolicyDigest,
- FeedsDigest = request.FeedsDigest,
- ToolchainDigest = request.ToolchainDigest,
- ParamsDigest = request.ParamsDigest
+ PolicyDigest = policyDigest,
+ FeedsDigest = feedsDigest,
+ ToolchainDigest = toolchainDigest,
+ ParamsDigest = paramsDigest
},
EvidenceIds = sortedEvidenceIds,
CanonVersion = CanonVersion.Current,
@@ -476,13 +572,13 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
var colonIndex = digest.IndexOf(':');
if (colonIndex > 0 && colonIndex < digest.Length - 1)
{
- var algorithm = digest[..colonIndex];
- var value = digest[(colonIndex + 1)..];
+ var algorithm = digest[..colonIndex].ToLowerInvariant();
+ var value = digest[(colonIndex + 1)..].ToLowerInvariant();
return new Dictionary { [algorithm] = value };
}
// Assume sha256 if no algorithm prefix
- return new Dictionary { ["sha256"] = digest };
+ return new Dictionary { ["sha256"] = digest.ToLowerInvariant() };
}
private static string GetToolVersion()
@@ -493,4 +589,104 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
?? "1.0.0";
return version;
}
+
+ private bool TryVerifyEnvelopeSignatures(
+ EnvDsseEnvelope envelope,
+ CancellationToken ct,
+ out string? failureReason)
+ {
+ if (envelope.Signatures.Count == 0)
+ {
+ failureReason = "Envelope does not contain signatures.";
+ return false;
+ }
+
+ foreach (var signature in envelope.Signatures)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (string.IsNullOrWhiteSpace(signature.KeyId))
+ {
+ continue;
+ }
+
+ var key = _keyResolver(signature.KeyId);
+ if (key is null)
+ {
+ continue;
+ }
+
+ if (!string.Equals(signature.KeyId, key.KeyId, StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ if (!TryDecodeSignature(signature.Signature, out var signatureBytes))
+ {
+ continue;
+ }
+
+ var envelopeSignature = new EnvelopeSignature(signature.KeyId, key.AlgorithmId, signatureBytes);
+ var verified = _signatureService.VerifyDsse(envelope.PayloadType, envelope.Payload.Span, envelopeSignature, key, ct);
+ if (verified.IsSuccess)
+ {
+ failureReason = null;
+ return true;
+ }
+ }
+
+ failureReason = "DSSE signature verification failed.";
+ return false;
+ }
+
+ private static bool TryDecodeSignature(string signature, out byte[] signatureBytes)
+ {
+ try
+ {
+ signatureBytes = Convert.FromBase64String(signature);
+ return signatureBytes.Length > 0;
+ }
+ catch (FormatException)
+ {
+ signatureBytes = [];
+ return false;
+ }
+ }
+
+ private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList right)
+ {
+ if (left.Count != right.Count)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < left.Count; i++)
+ {
+ if (!string.Equals(left[i], right[i], StringComparison.Ordinal))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static string NormalizeDigest(string digest)
+ {
+ if (string.IsNullOrWhiteSpace(digest))
+ {
+ throw new ArgumentException("Digest must be provided.", nameof(digest));
+ }
+
+ var trimmed = digest.Trim();
+ var colonIndex = trimmed.IndexOf(':');
+ if (colonIndex > 0 && colonIndex < trimmed.Length - 1)
+ {
+ var algorithm = trimmed[..colonIndex].ToLowerInvariant();
+ var value = trimmed[(colonIndex + 1)..].ToLowerInvariant();
+ return $"{algorithm}:{value}";
+ }
+
+ return $"sha256:{trimmed.ToLowerInvariant()}";
+ }
}
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/GraphRootServiceCollectionExtensions.cs b/src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/GraphRootServiceCollectionExtensions.cs
index 3086a2d68..73590888f 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/GraphRootServiceCollectionExtensions.cs
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.GraphRoot/GraphRootServiceCollectionExtensions.cs
@@ -1,3 +1,4 @@
+using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Attestor.Envelope;
@@ -18,6 +19,7 @@ public static class GraphRootServiceCollectionExtensions
{
services.TryAddSingleton();
services.TryAddSingleton();
+ services.TryAddSingleton(TimeProvider.System);
services.TryAddSingleton();
return services;
@@ -37,14 +39,16 @@ public static class GraphRootServiceCollectionExtensions
services.TryAddSingleton();
services.TryAddSingleton();
+ services.TryAddSingleton(TimeProvider.System);
services.AddSingleton(sp =>
{
var merkleComputer = sp.GetRequiredService();
var signatureService = sp.GetRequiredService();
var logger = sp.GetRequiredService>();
var resolver = keyResolver(sp);
+ var timeProvider = sp.GetService