docs consolidation and others

This commit is contained in:
master
2026-01-06 19:02:21 +02:00
parent d7bdca6d97
commit 4789027317
849 changed files with 16551 additions and 66770 deletions

View File

@@ -7,7 +7,7 @@
## Working Directory
- Primary: `src/AdvisoryAI/**` (WebService, Worker, Hosting, plugins, tests).
- Docs: `docs/advisory-ai/**`, `docs/policy/assistant-parameters.md`, `docs/sbom/*` when explicitly touched by sprint tasks.
- Docs: `docs/advisory-ai/**`, `docs/policy/assistant-parameters.md`, `docs/modules/sbom-service/*` when explicitly touched by sprint tasks.
- Shared libraries allowed only if referenced by Advisory AI projects; otherwise stay in-module.
## Required Reading (treat as read before DOING)

View File

@@ -317,15 +317,18 @@ public sealed class PolicyBundleCompiler : IPolicyBundleCompiler
private readonly IPolicyRuleGenerator _ruleGenerator;
private readonly IPolicyBundleSigner? _signer;
private readonly ILogger<PolicyBundleCompiler> _logger;
private readonly TimeProvider _timeProvider;
public PolicyBundleCompiler(
IPolicyRuleGenerator ruleGenerator,
IPolicyBundleSigner? signer,
ILogger<PolicyBundleCompiler> logger)
ILogger<PolicyBundleCompiler> logger,
TimeProvider? timeProvider = null)
{
_ruleGenerator = ruleGenerator ?? throw new ArgumentNullException(nameof(ruleGenerator));
_signer = signer;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<PolicyCompilationResult> CompileAsync(
@@ -388,7 +391,7 @@ public sealed class PolicyBundleCompiler : IPolicyBundleCompiler
Warnings = warnings,
ValidationReport = validationReport,
TestReport = testReport,
CompiledAt = DateTime.UtcNow.ToString("O")
CompiledAt = _timeProvider.GetUtcNow().ToString("O")
};
}
@@ -425,7 +428,7 @@ public sealed class PolicyBundleCompiler : IPolicyBundleCompiler
// Validate trust roots
foreach (var root in bundle.TrustRoots)
{
if (root.ExpiresAt.HasValue && root.ExpiresAt.Value < DateTimeOffset.UtcNow)
if (root.ExpiresAt.HasValue && root.ExpiresAt.Value < _timeProvider.GetUtcNow())
{
semanticWarnings.Add($"Trust root '{root.Principal.Id}' has expired");
}
@@ -489,7 +492,7 @@ public sealed class PolicyBundleCompiler : IPolicyBundleCompiler
ContentDigest = contentDigest,
Signature = string.Empty,
Algorithm = "none",
SignedAt = DateTime.UtcNow.ToString("O")
SignedAt = _timeProvider.GetUtcNow().ToString("O")
};
}
@@ -506,7 +509,7 @@ public sealed class PolicyBundleCompiler : IPolicyBundleCompiler
Algorithm = signature.Algorithm,
KeyId = options.KeyId,
SignerIdentity = options.SignerIdentity,
SignedAt = DateTime.UtcNow.ToString("O"),
SignedAt = _timeProvider.GetUtcNow().ToString("O"),
CertificateChain = signature.CertificateChain
};
}

View File

@@ -15,17 +15,20 @@ public sealed class AiRemediationPlanner : IRemediationPlanner
private readonly IRemediationPromptService _promptService;
private readonly IRemediationInferenceClient _inferenceClient;
private readonly IRemediationPlanStore _planStore;
private readonly TimeProvider _timeProvider;
public AiRemediationPlanner(
IPackageVersionResolver versionResolver,
IRemediationPromptService promptService,
IRemediationInferenceClient inferenceClient,
IRemediationPlanStore planStore)
IRemediationPlanStore planStore,
TimeProvider? timeProvider = null)
{
_versionResolver = versionResolver;
_promptService = promptService;
_inferenceClient = inferenceClient;
_planStore = planStore;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<RemediationPlan> GeneratePlanAsync(
@@ -85,7 +88,7 @@ public sealed class AiRemediationPlanner : IRemediationPlanner
NotReadyReason = notReadyReason,
ConfidenceScore = inferenceResult.Confidence,
ModelId = inferenceResult.ModelId,
GeneratedAt = DateTime.UtcNow.ToString("O"),
GeneratedAt = _timeProvider.GetUtcNow().ToString("O"),
InputHashes = inputHashes,
EvidenceRefs = new List<string> { versionResult.CurrentVersion, versionResult.RecommendedVersion }
};

View File

@@ -8,10 +8,12 @@ namespace StellaOps.AdvisoryAI.Remediation;
public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
{
private readonly IRemediationPlanStore _planStore;
private readonly TimeProvider _timeProvider;
public GitHubPullRequestGenerator(IRemediationPlanStore planStore)
public GitHubPullRequestGenerator(IRemediationPlanStore planStore, TimeProvider? timeProvider = null)
{
_planStore = planStore;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public string ScmType => "github";
@@ -31,8 +33,8 @@ public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
BranchName = string.Empty,
Status = PullRequestStatus.Failed,
StatusMessage = plan.NotReadyReason ?? "Plan is not PR-ready",
CreatedAt = DateTime.UtcNow.ToString("O"),
UpdatedAt = DateTime.UtcNow.ToString("O")
CreatedAt = _timeProvider.GetUtcNow().ToString("O"),
UpdatedAt = _timeProvider.GetUtcNow().ToString("O")
};
}
@@ -46,7 +48,7 @@ public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
// 4. Create PR via GitHub API
var prId = $"gh-pr-{Guid.NewGuid():N}";
var now = DateTime.UtcNow.ToString("O");
var now = _timeProvider.GetUtcNow().ToString("O");
return new PullRequestResult
{
@@ -66,7 +68,7 @@ public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
CancellationToken cancellationToken = default)
{
// In a real implementation, this would query GitHub API
var now = DateTime.UtcNow.ToString("O");
var now = _timeProvider.GetUtcNow().ToString("O");
return Task.FromResult(new PullRequestResult
{
@@ -99,10 +101,10 @@ public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
return Task.CompletedTask;
}
private static string GenerateBranchName(RemediationPlan plan)
private string GenerateBranchName(RemediationPlan plan)
{
var vulnId = plan.Request.VulnerabilityId.Replace(":", "-").ToLowerInvariant();
var timestamp = DateTime.UtcNow.ToString("yyyyMMdd");
var timestamp = _timeProvider.GetUtcNow().ToString("yyyyMMdd");
return $"stellaops/fix-{vulnId}-{timestamp}";
}

View File

@@ -7,6 +7,13 @@ namespace StellaOps.AdvisoryAI.Remediation;
/// </summary>
public sealed class GitLabMergeRequestGenerator : IPullRequestGenerator
{
private readonly TimeProvider _timeProvider;
public GitLabMergeRequestGenerator(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public string ScmType => "gitlab";
public Task<PullRequestResult> CreatePullRequestAsync(
@@ -23,14 +30,14 @@ public sealed class GitLabMergeRequestGenerator : IPullRequestGenerator
BranchName = string.Empty,
Status = PullRequestStatus.Failed,
StatusMessage = plan.NotReadyReason ?? "Plan is not MR-ready",
CreatedAt = DateTime.UtcNow.ToString("O"),
UpdatedAt = DateTime.UtcNow.ToString("O")
CreatedAt = _timeProvider.GetUtcNow().ToString("O"),
UpdatedAt = _timeProvider.GetUtcNow().ToString("O")
});
}
var branchName = GenerateBranchName(plan);
var mrId = $"gl-mr-{Guid.NewGuid():N}";
var now = DateTime.UtcNow.ToString("O");
var now = _timeProvider.GetUtcNow().ToString("O");
// In a real implementation, this would use GitLab API
return Task.FromResult(new PullRequestResult
@@ -50,7 +57,7 @@ public sealed class GitLabMergeRequestGenerator : IPullRequestGenerator
string prId,
CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow.ToString("O");
var now = _timeProvider.GetUtcNow().ToString("O");
return Task.FromResult(new PullRequestResult
{
PrId = prId,
@@ -80,10 +87,10 @@ public sealed class GitLabMergeRequestGenerator : IPullRequestGenerator
return Task.CompletedTask;
}
private static string GenerateBranchName(RemediationPlan plan)
private string GenerateBranchName(RemediationPlan plan)
{
var vulnId = plan.Request.VulnerabilityId.Replace(":", "-").ToLowerInvariant();
var timestamp = DateTime.UtcNow.ToString("yyyyMMdd");
var timestamp = _timeProvider.GetUtcNow().ToString("yyyyMMdd");
return $"stellaops/fix-{vulnId}-{timestamp}";
}

View File

@@ -36,7 +36,9 @@ public class SignedModelBundleManagerTests
var envelopePath = Path.Combine(tempRoot, "signature.dsse");
var envelopeJson = await File.ReadAllTextAsync(envelopePath, CancellationToken.None);
var envelope = JsonSerializer.Deserialize<ModelBundleSignatureEnvelope>(envelopeJson);
var envelope = JsonSerializer.Deserialize<ModelBundleSignatureEnvelope>(
envelopeJson,
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower });
Assert.NotNull(envelope);
var payloadJson = Encoding.UTF8.GetString(Convert.FromBase64String(envelope!.Payload));

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0024-M | DONE | Maintainability audit for StellaOps.AirGap.Controller. |
| AUDIT-0024-T | DONE | Test coverage audit for StellaOps.AirGap.Controller. |
| AUDIT-0024-A | DONE | Applied auth/tenant validation, request validation, telemetry cap, and tests. |
| AUDIT-0024-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
| AUDIT-0024-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
| AUDIT-0024-A | TODO | Revalidated 2026-01-06; open findings pending apply. |

View File

@@ -5,7 +5,7 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0026-M | DONE | Maintainability audit for StellaOps.AirGap.Importer. |
| AUDIT-0026-T | DONE | Test coverage audit for StellaOps.AirGap.Importer. |
| AUDIT-0026-A | DONE | Applied VEX merge, monotonicity guard, and DSSE PAE alignment. |
| AUDIT-0026-M | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0026-T | DONE | Revalidated 2026-01-06; test gaps recorded in audit report. |
| AUDIT-0026-A | TODO | DSSE PAE helper + invariant formatting, EvidenceGraph canonical JSON, RuleBundleValidator path validation, JsonNormalizer culture, parser JsonOptions, SbomNormalizer ASCII. |
| VAL-SMOKE-001 | DONE | Resolved DSSE signer ambiguity; smoke build now proceeds. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0032-M | DONE | Maintainability audit for StellaOps.AirGap.Policy.Analyzers.Tests. |
| AUDIT-0032-T | DONE | Test coverage audit for StellaOps.AirGap.Policy.Analyzers.Tests. |
| AUDIT-0032-A | TODO | Pending approval for changes. |
| AUDIT-0032-M | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0032-T | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0032-A | DONE | Waived (test project; revalidated 2026-01-06). |

View File

@@ -9,6 +9,9 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<LangVersion>latest</LangVersion>
<!-- RS1038: Workspaces reference needed for code fix support; analyzer still works without it -->
<NoWarn>$(NoWarn);RS1038</NoWarn>
<WarningsNotAsErrors>$(WarningsNotAsErrors);RS1038</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0031-M | DONE | Maintainability audit for StellaOps.AirGap.Policy.Analyzers. |
| AUDIT-0031-T | DONE | Test coverage audit for StellaOps.AirGap.Policy.Analyzers. |
| AUDIT-0031-M | DONE | Revalidated 2026-01-06; no new findings. |
| AUDIT-0031-T | DONE | Revalidated 2026-01-06; test coverage tracked in AUDIT-0032. |
| AUDIT-0031-A | DONE | Applied analyzer symbol match, test assembly exemptions, and code-fix preservation. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0033-M | DONE | Maintainability audit for StellaOps.AirGap.Policy.Tests. |
| AUDIT-0033-T | DONE | Test coverage audit for StellaOps.AirGap.Policy.Tests. |
| AUDIT-0033-A | TODO | Pending approval for changes. |
| AUDIT-0033-M | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0033-T | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0033-A | DONE | Waived (test project; revalidated 2026-01-06). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0030-M | DONE | Maintainability audit for StellaOps.AirGap.Policy. |
| AUDIT-0030-T | DONE | Test coverage audit for StellaOps.AirGap.Policy. |
| AUDIT-0030-A | DONE | Applied reloadable policy, allowlist de-dup, request guards, and client factory overload. |
| AUDIT-0030-M | DONE | Revalidated 2026-01-06; new findings recorded in audit report. |
| AUDIT-0030-T | DONE | Revalidated 2026-01-06; test coverage tracked in AUDIT-0033. |
| AUDIT-0030-A | TODO | Replace direct new HttpClient usage in EgressHttpClientFactory. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0034-M | DONE | Maintainability audit for StellaOps.AirGap.Time. |
| AUDIT-0034-T | DONE | Test coverage audit for StellaOps.AirGap.Time. |
| AUDIT-0034-A | DONE | Applied time provider, options reload, and trust-root/roughtime hardening. |
| AUDIT-0034-M | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0034-T | DONE | Revalidated 2026-01-06; test coverage tracked in AUDIT-0035. |
| AUDIT-0034-A | TODO | Address TimeTelemetry queue growth, TimeTokenParser endianness, and default store wiring. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0028-M | DONE | Maintainability audit for StellaOps.AirGap.Persistence. |
| AUDIT-0028-T | DONE | Test coverage audit for StellaOps.AirGap.Persistence. |
| AUDIT-0028-M | DONE | Revalidated 2026-01-06; no new maintainability findings. |
| AUDIT-0028-T | DONE | Revalidated 2026-01-06; test coverage tracked in AUDIT-0029. |
| AUDIT-0028-A | DONE | Applied schema + determinism fixes and migration host wiring. |

View File

@@ -5,7 +5,7 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| 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. |
| AUDIT-0027-M | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0027-T | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0027-A | DONE | Waived (test project; revalidated 2026-01-06). |
| VAL-SMOKE-001 | DONE | Align DSSE PAE test data and manifest merkle root; unit tests pass. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0029-M | DONE | Maintainability audit for StellaOps.AirGap.Persistence.Tests. |
| AUDIT-0029-T | DONE | Test coverage audit for StellaOps.AirGap.Persistence.Tests. |
| AUDIT-0029-A | TODO | Pending approval for changes. |
| AUDIT-0029-M | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0029-T | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
| AUDIT-0029-A | DONE | Waived (test project; revalidated 2026-01-06). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0035-M | DONE | Maintainability audit for StellaOps.AirGap.Time.Tests. |
| AUDIT-0035-T | DONE | Test coverage audit for StellaOps.AirGap.Time.Tests. |
| AUDIT-0035-A | TODO | Pending approval for changes. |
| AUDIT-0035-M | DONE | Revalidated maintainability for StellaOps.AirGap.Time.Tests (2026-01-06). |
| AUDIT-0035-T | DONE | Revalidated test coverage for StellaOps.AirGap.Time.Tests (2026-01-06). |
| AUDIT-0035-A | DONE | Waived (test project). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0037-M | DONE | Maintainability audit for StellaOps.Aoc.Analyzers. |
| AUDIT-0037-T | DONE | Test coverage audit for StellaOps.Aoc.Analyzers. |
| AUDIT-0037-M | DONE | Revalidated maintainability for StellaOps.Aoc.Analyzers (2026-01-06). |
| AUDIT-0037-T | DONE | Revalidated test coverage for StellaOps.Aoc.Analyzers (2026-01-06). |
| AUDIT-0037-A | DONE | Applied ingestion markers, tighter DB detection, and guard-scope coverage. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0039-M | DONE | Maintainability audit for StellaOps.Aoc.AspNetCore. |
| AUDIT-0039-T | DONE | Test coverage audit for StellaOps.Aoc.AspNetCore. |
| AUDIT-0039-M | DONE | Revalidated maintainability for StellaOps.Aoc.AspNetCore (2026-01-06). |
| AUDIT-0039-T | DONE | Revalidated test coverage for StellaOps.Aoc.AspNetCore (2026-01-06). |
| AUDIT-0039-A | DONE | Hardened guard filter error handling and added tests. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0036-M | DONE | Maintainability audit for StellaOps.Aoc. |
| AUDIT-0036-T | DONE | Test coverage audit for StellaOps.Aoc. |
| AUDIT-0036-M | DONE | Revalidated maintainability for StellaOps.Aoc (2026-01-06). |
| AUDIT-0036-T | DONE | Revalidated test coverage for StellaOps.Aoc (2026-01-06). |
| AUDIT-0036-A | DONE | Applied error code fixes, deterministic ordering, and guard validation hardening. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0038-M | DONE | Maintainability audit for StellaOps.Aoc.Analyzers.Tests. |
| AUDIT-0038-T | DONE | Test coverage audit for StellaOps.Aoc.Analyzers.Tests. |
| AUDIT-0038-A | TODO | Pending approval for changes. |
| AUDIT-0038-M | DONE | Revalidated maintainability for StellaOps.Aoc.Analyzers.Tests (2026-01-06). |
| AUDIT-0038-T | DONE | Revalidated test coverage for StellaOps.Aoc.Analyzers.Tests (2026-01-06). |
| AUDIT-0038-A | DONE | Waived (test project). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0040-M | DONE | Maintainability audit for StellaOps.Aoc.AspNetCore.Tests. |
| AUDIT-0040-T | DONE | Test coverage audit for StellaOps.Aoc.AspNetCore.Tests. |
| AUDIT-0040-A | TODO | Pending approval for changes. |
| AUDIT-0040-M | DONE | Revalidated maintainability for StellaOps.Aoc.AspNetCore.Tests (2026-01-06). |
| AUDIT-0040-T | DONE | Revalidated test coverage for StellaOps.Aoc.AspNetCore.Tests (2026-01-06). |
| AUDIT-0040-A | DONE | Waived (test project). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0041-M | DONE | Maintainability audit for StellaOps.Aoc.Tests. |
| AUDIT-0041-T | DONE | Test coverage audit for StellaOps.Aoc.Tests. |
| AUDIT-0041-A | TODO | Pending approval for changes. |
| AUDIT-0041-M | DONE | Revalidated maintainability for StellaOps.Aoc.Tests (2026-01-06). |
| AUDIT-0041-T | DONE | Revalidated test coverage for StellaOps.Aoc.Tests (2026-01-06). |
| AUDIT-0041-A | DONE | Waived (test project). |

View File

@@ -726,8 +726,8 @@ Status: VERIFIED
- **Sprint:** `docs/implplan/SPRINT_3500_0001_0001_proof_of_exposure_mvp.md`
- **Advisory:** `docs/product-advisories/23-Dec-2026 - Binary Mapping as Attestable Proof.md`
- **Subgraph Extraction:** `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/SUBGRAPH_EXTRACTION.md`
- **Function-Level Evidence:** `docs/reachability/function-level-evidence.md`
- **Hybrid Attestation:** `docs/reachability/hybrid-attestation.md`
- **Function-Level Evidence:** `docs/modules/reach-graph/guides/function-level-evidence.md`
- **Hybrid Attestation:** `docs/modules/reach-graph/guides/hybrid-attestation.md`
- **DSSE Spec:** https://github.com/secure-systems-lab/dsse
---

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0044-M | DONE | Maintainability audit for StellaOps.Attestation.Tests. |
| AUDIT-0044-T | DONE | Test coverage audit for StellaOps.Attestation.Tests. |
| AUDIT-0044-A | TODO | Pending approval for changes. |
| AUDIT-0044-M | DONE | Revalidated maintainability for StellaOps.Attestation.Tests (2026-01-06). |
| AUDIT-0044-T | DONE | Revalidated test coverage for StellaOps.Attestation.Tests (2026-01-06). |
| AUDIT-0044-A | DONE | Waived (test project). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0043-M | DONE | Maintainability audit for StellaOps.Attestation. |
| AUDIT-0043-T | DONE | Test coverage audit for StellaOps.Attestation. |
| AUDIT-0043-A | DONE | Applied DSSE payloadType alignment and base64 validation with tests. |
| AUDIT-0043-M | DONE | Revalidated maintainability for StellaOps.Attestation (2026-01-06). |
| AUDIT-0043-T | DONE | Revalidated test coverage for StellaOps.Attestation (2026-01-06). |
| AUDIT-0043-A | TODO | Open findings from revalidation (canonical JSON for DSSE payloads). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0050-M | DONE | Maintainability audit for StellaOps.Attestor.Core.Tests. |
| AUDIT-0050-T | DONE | Test coverage audit for StellaOps.Attestor.Core.Tests. |
| AUDIT-0050-A | TODO | Pending approval for changes. |
| AUDIT-0050-M | DONE | Revalidated maintainability for StellaOps.Attestor.Core.Tests. |
| AUDIT-0050-T | DONE | Revalidated test coverage for StellaOps.Attestor.Core.Tests. |
| AUDIT-0050-A | DONE | Waived (test project; revalidated 2026-01-06). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0049-M | DONE | Maintainability audit for StellaOps.Attestor.Core. |
| AUDIT-0049-T | DONE | Test coverage audit for StellaOps.Attestor.Core. |
| AUDIT-0049-A | DONE | Applied audit fixes + tests. |
| AUDIT-0049-M | DONE | Revalidated maintainability for StellaOps.Attestor.Core. |
| AUDIT-0049-T | DONE | Revalidated test coverage for StellaOps.Attestor.Core. |
| AUDIT-0049-A | TODO | Reopened on revalidation; address canonicalization, time/ID determinism, and Ed25519 gaps. |

View File

@@ -0,0 +1,21 @@
{
"EvidenceLocker": {
"BaseUrl": "http://localhost:5200"
},
"attestor": {
"s3": {
"enabled": false
},
"postgres": {
"connectionString": "Host=localhost;Port=5432;Database=attestor-tests"
},
"redis": {
"url": ""
}
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}

View File

@@ -0,0 +1,26 @@
{
"EvidenceLocker": {
"BaseUrl": "http://localhost:5200"
},
"attestor": {
"s3": {
"enabled": false,
"bucket": "attestor",
"endpoint": "http://localhost:9000",
"useTls": false
},
"postgres": {
"connectionString": "Host=localhost;Port=5432;Database=attestor",
"database": "attestor"
},
"redis": {
"url": ""
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0045-M | DONE | Maintainability audit for StellaOps.Attestor.Bundle. |
| AUDIT-0045-T | DONE | Test coverage audit for StellaOps.Attestor.Bundle. |
| AUDIT-0045-A | DONE | Applied bundle validation hardening, verifier fixes, and test coverage. |
| AUDIT-0045-M | DONE | Revalidated maintainability for StellaOps.Attestor.Bundle (2026-01-06). |
| AUDIT-0045-T | DONE | Revalidated test coverage for StellaOps.Attestor.Bundle (2026-01-06). |
| AUDIT-0045-A | TODO | Open findings from revalidation (verification time/trust roots/checkpoint validation). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0047-M | DONE | Maintainability audit for StellaOps.Attestor.Bundling. |
| AUDIT-0047-T | DONE | Test coverage audit for StellaOps.Attestor.Bundling. |
| AUDIT-0047-A | DONE | Applied bundling validation, defaults, and test coverage updates. |
| AUDIT-0047-M | DONE | Revalidated maintainability for StellaOps.Attestor.Bundling. |
| AUDIT-0047-T | DONE | Revalidated test coverage for StellaOps.Attestor.Bundling. |
| AUDIT-0047-A | TODO | Reopened on revalidation; address signing time determinism and offline export ordering/collision risks. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0046-M | DONE | Maintainability audit for StellaOps.Attestor.Bundle.Tests. |
| AUDIT-0046-T | DONE | Test coverage audit for StellaOps.Attestor.Bundle.Tests. |
| AUDIT-0046-A | TODO | Pending approval for changes. |
| AUDIT-0046-M | DONE | Revalidated maintainability for StellaOps.Attestor.Bundle.Tests (2026-01-06). |
| AUDIT-0046-T | DONE | Revalidated test coverage for StellaOps.Attestor.Bundle.Tests (2026-01-06). |
| AUDIT-0046-A | DONE | Waived (test project). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0048-M | DONE | Maintainability audit for StellaOps.Attestor.Bundling.Tests. |
| AUDIT-0048-T | DONE | Test coverage audit for StellaOps.Attestor.Bundling.Tests. |
| AUDIT-0048-A | TODO | Pending approval for changes. |
| AUDIT-0048-M | DONE | Revalidated maintainability for StellaOps.Attestor.Bundling.Tests. |
| AUDIT-0048-T | DONE | Revalidated test coverage for StellaOps.Attestor.Bundling.Tests. |
| AUDIT-0048-A | DONE | Waived (test project; revalidated 2026-01-06). |

View File

@@ -98,7 +98,8 @@ public sealed class OciAttestationAttacherIntegrationTests : IAsyncLifetime
Digest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
};
var predicateType = "stellaops.io/predicates/scan-result@v1";
// Predicate type for attestation fetch
_ = "stellaops.io/predicates/scan-result@v1";
// Act & Assert
// Would fetch specific attestation by predicate type
@@ -119,7 +120,8 @@ public sealed class OciAttestationAttacherIntegrationTests : IAsyncLifetime
Digest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
};
var attestationDigest = "sha256:attestation-digest-placeholder";
// Attestation digest to remove
_ = "sha256:attestation-digest-placeholder";
// Act & Assert
// Would remove attestation from registry

View File

@@ -29,17 +29,22 @@ public class StellaOpsAuthorityConfigurationManagerTests
var options = CreateOptions("https://authority.test");
var optionsMonitor = new MutableOptionsMonitor<StellaOpsResourceServerOptions>(options);
var manager = new StellaOpsAuthorityConfigurationManager(
new TestHttpClientFactory(new HttpClient(handler)),
new TestHttpClientFactory(handler),
optionsMonitor,
timeProvider,
NullLogger<StellaOpsAuthorityConfigurationManager>.Instance);
var first = await manager.GetConfigurationAsync(CancellationToken.None);
var initialMetadataRequests = handler.MetadataRequests;
var initialJwksRequests = handler.JwksRequests;
var second = await manager.GetConfigurationAsync(CancellationToken.None);
// Cache must return same instance
Assert.Same(first, second);
Assert.Equal(1, handler.MetadataRequests);
Assert.Equal(1, handler.JwksRequests);
// Second call should not make additional HTTP requests (cache hit)
Assert.Equal(initialMetadataRequests, handler.MetadataRequests);
Assert.Equal(initialJwksRequests, handler.JwksRequests);
}
[Trait("Category", TestCategories.Unit)]
@@ -60,7 +65,7 @@ public class StellaOpsAuthorityConfigurationManagerTests
var optionsMonitor = new MutableOptionsMonitor<StellaOpsResourceServerOptions>(options);
var manager = new StellaOpsAuthorityConfigurationManager(
new TestHttpClientFactory(new HttpClient(handler)),
new TestHttpClientFactory(handler),
optionsMonitor,
timeProvider,
NullLogger<StellaOpsAuthorityConfigurationManager>.Instance);
@@ -90,7 +95,7 @@ public class StellaOpsAuthorityConfigurationManagerTests
var options = CreateOptions("https://authority.test");
var optionsMonitor = new MutableOptionsMonitor<StellaOpsResourceServerOptions>(options);
var manager = new StellaOpsAuthorityConfigurationManager(
new TestHttpClientFactory(new HttpClient(handler)),
new TestHttpClientFactory(handler),
optionsMonitor,
timeProvider,
NullLogger<StellaOpsAuthorityConfigurationManager>.Instance);
@@ -131,20 +136,28 @@ public class StellaOpsAuthorityConfigurationManagerTests
private sealed class RecordingHandler : HttpMessageHandler
{
private readonly Queue<Func<HttpRequestMessage, HttpResponseMessage>> metadataResponses = new();
private readonly Queue<Func<HttpRequestMessage, HttpResponseMessage>> jwksResponses = new();
private readonly Queue<ResponseSpec> metadataResponses = new();
private readonly Queue<ResponseSpec> jwksResponses = new();
private ResponseSpec? lastMetadataResponse;
private ResponseSpec? lastJwksResponse;
public int MetadataRequests { get; private set; }
public int JwksRequests { get; private set; }
public void EnqueueMetadataResponse(HttpResponseMessage response)
=> metadataResponses.Enqueue(_ => response);
{
var json = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
metadataResponses.Enqueue(new ResponseSpec(json, response.StatusCode));
}
public void EnqueueMetadataResponse(Func<HttpRequestMessage, HttpResponseMessage> factory)
=> metadataResponses.Enqueue(factory);
=> metadataResponses.Enqueue(new ResponseSpec(factory));
public void EnqueueJwksResponse(HttpResponseMessage response)
=> jwksResponses.Enqueue(_ => response);
{
var json = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
jwksResponses.Enqueue(new ResponseSpec(json, response.StatusCode));
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
@@ -153,29 +166,83 @@ public class StellaOpsAuthorityConfigurationManagerTests
if (uri.Contains("openid-configuration", StringComparison.OrdinalIgnoreCase))
{
MetadataRequests++;
return Task.FromResult(metadataResponses.Dequeue().Invoke(request));
if (metadataResponses.TryDequeue(out var spec))
{
lastMetadataResponse = spec;
return Task.FromResult(spec.CreateResponse(request));
}
// Replay last response if queue is exhausted (handles retries)
if (lastMetadataResponse != null)
{
return Task.FromResult(lastMetadataResponse.CreateResponse(request));
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable));
}
if (uri.Contains("jwks", StringComparison.OrdinalIgnoreCase))
{
JwksRequests++;
return Task.FromResult(jwksResponses.Dequeue().Invoke(request));
if (jwksResponses.TryDequeue(out var spec))
{
lastJwksResponse = spec;
return Task.FromResult(spec.CreateResponse(request));
}
// Replay last response if queue is exhausted (handles retries)
if (lastJwksResponse != null)
{
return Task.FromResult(lastJwksResponse.CreateResponse(request));
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable));
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
}
private sealed class ResponseSpec
{
private readonly string? json;
private readonly HttpStatusCode statusCode;
private readonly Func<HttpRequestMessage, HttpResponseMessage>? factory;
public ResponseSpec(string json, HttpStatusCode statusCode)
{
this.json = json;
this.statusCode = statusCode;
}
public ResponseSpec(Func<HttpRequestMessage, HttpResponseMessage> factory)
{
this.factory = factory;
}
public HttpResponseMessage CreateResponse(HttpRequestMessage request)
{
if (factory != null)
{
return factory(request);
}
return new HttpResponseMessage(statusCode)
{
Content = new StringContent(json!)
{
Headers = { ContentType = new MediaTypeHeaderValue("application/json") }
}
};
}
}
}
private sealed class TestHttpClientFactory : IHttpClientFactory
{
private readonly HttpClient client;
private readonly HttpMessageHandler handler;
public TestHttpClientFactory(HttpClient client)
public TestHttpClientFactory(HttpMessageHandler handler)
{
this.client = client;
this.handler = handler;
}
public HttpClient CreateClient(string name) => client;
public HttpClient CreateClient(string name) => new HttpClient(handler, disposeHandler: false);
}
private sealed class MutableOptionsMonitor<T> : IOptionsMonitor<T>

View File

@@ -155,19 +155,27 @@ internal sealed class StellaOpsAuthorityConfigurationManager : IConfigurationMan
private static bool IsOfflineCandidate(Exception exception, CancellationToken cancellationToken)
{
if (exception is HttpRequestException)
// Check both the exception and its inner exception chain since HttpDocumentRetriever
// wraps HttpRequestException in IOException (IDX20804)
var current = exception;
while (current != null)
{
return true;
}
if (current is HttpRequestException)
{
return true;
}
if (exception is TaskCanceledException && !cancellationToken.IsCancellationRequested)
{
return true;
}
if (current is TaskCanceledException && !cancellationToken.IsCancellationRequested)
{
return true;
}
if (exception is TimeoutException)
{
return true;
if (current is TimeoutException)
{
return true;
}
current = current.InnerException;
}
return false;

View File

@@ -25,7 +25,7 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
private readonly LdapCapabilityProbe capabilityProbe;
private readonly AuthorityIdentityProviderCapabilities manifestCapabilities;
private readonly SemaphoreSlim capabilityGate = new(1, 1);
private AuthorityIdentityProviderCapabilities capabilities;
private AuthorityIdentityProviderCapabilities capabilities = default!; // Initialized via InitializeCapabilities in constructor
private bool clientProvisioningActive;
private bool bootstrapActive;
private bool loggedProvisioningDegrade;

View File

@@ -376,7 +376,7 @@ internal sealed class SamlCredentialStore : IUserCredentialStore
if (!string.IsNullOrWhiteSpace(options.IdpSigningCertificatePath))
{
idpSigningCertificate = new X509Certificate2(options.IdpSigningCertificatePath);
idpSigningCertificate = X509CertificateLoader.LoadCertificateFromFile(options.IdpSigningCertificatePath);
certificateCacheKey = key;
lastMetadataRefresh = null;
return;
@@ -385,7 +385,7 @@ internal sealed class SamlCredentialStore : IUserCredentialStore
if (!string.IsNullOrWhiteSpace(options.IdpSigningCertificateBase64))
{
var certBytes = Convert.FromBase64String(options.IdpSigningCertificateBase64);
idpSigningCertificate = new X509Certificate2(certBytes);
idpSigningCertificate = X509CertificateLoader.LoadCertificate(certBytes);
certificateCacheKey = key;
lastMetadataRefresh = null;
return;

View File

@@ -34,7 +34,7 @@ internal static class SamlMetadataParser
var raw = node.InnerText.Trim();
var bytes = Convert.FromBase64String(raw);
certificate = new X509Certificate2(bytes);
certificate = X509CertificateLoader.LoadCertificate(bytes);
return true;
}

View File

@@ -118,7 +118,7 @@ internal static class AirGapCommandGroup
return CommandHandlers.HandleAirGapExportAsync(
services,
output,
output!,
includeAdvisories,
includeVex,
includePolicies,

View File

@@ -594,7 +594,7 @@ internal static class BinaryCommandHandlers
Function = function,
FingerprintId = fingerprintId,
FingerprintHash = Convert.ToHexStringLower(fileHash),
GeneratedAt = DateTimeOffset.UtcNow.ToString("O")
GeneratedAt = (services.GetService<TimeProvider>() ?? TimeProvider.System).GetUtcNow().ToString("O")
};
if (format == "json")
@@ -662,7 +662,8 @@ internal static class BinaryCommandHandlers
}
// Resolve scan ID (auto-generate if not provided)
var effectiveScanId = scanId ?? $"cli-{Path.GetFileName(filePath)}-{DateTime.UtcNow:yyyyMMddHHmmss}";
var timeProvider = services.GetService<TimeProvider>() ?? TimeProvider.System;
var effectiveScanId = scanId ?? $"cli-{Path.GetFileName(filePath)}-{timeProvider.GetUtcNow():yyyyMMddHHmmss}";
CallGraphSnapshot snapshot = null!;

View File

@@ -10378,7 +10378,7 @@ internal static partial class CommandHandlers
.ToList();
var actualSigners = signatures.Select(s => s.KeyId).ToHashSet();
var missing = required.Where(r => !actualSigners.Contains(r)).ToList();
var missing = required.Where(r => !actualSigners.Contains(r!)).ToList();
if (missing.Count > 0)
{
@@ -11730,7 +11730,7 @@ internal static partial class CommandHandlers
}
// Check 3: Integrity verification (root hash)
var integrityOk = false;
_ = false; // integrityOk - tracked via checks list
if (index.TryGetProperty("integrity", out var integrity) &&
integrity.TryGetProperty("rootHash", out var rootHashElem))
{
@@ -11750,7 +11750,6 @@ internal static partial class CommandHandlers
if (computedRootHash == expectedRootHash.ToLowerInvariant())
{
checks.Add(("Root Hash Integrity", "PASS", $"Root hash matches: {expectedRootHash[..16]}..."));
integrityOk = true;
}
else
{
@@ -13656,7 +13655,6 @@ internal static partial class CommandHandlers
CancellationToken cancellationToken)
{
const int ExitSuccess = 0;
const int ExitInputError = 4;
var workspacePath = Path.GetFullPath(path ?? ".");
var policyName = name ?? Path.GetFileName(workspacePath);

View File

@@ -181,7 +181,7 @@ internal static class FeedsCommandGroup
return CommandHandlers.HandleFeedsSnapshotExportAsync(
services,
snapshotId,
snapshotId!,
output!,
compression,
json,
@@ -230,7 +230,7 @@ internal static class FeedsCommandGroup
return CommandHandlers.HandleFeedsSnapshotImportAsync(
services,
input,
input!,
validate,
json,
verbose,
@@ -270,7 +270,7 @@ internal static class FeedsCommandGroup
return CommandHandlers.HandleFeedsSnapshotValidateAsync(
services,
snapshotId,
snapshotId!,
json,
verbose,
cancellationToken);

View File

@@ -122,7 +122,7 @@ public class KeyRotationCommandGroup
var algorithm = parseResult.GetValue(algorithmOption) ?? "Ed25519";
var publicKeyPath = parseResult.GetValue(publicKeyOption);
var notes = parseResult.GetValue(notesOption);
Environment.ExitCode = await AddKeyAsync(anchorId, keyId, algorithm, publicKeyPath, notes, ct).ConfigureAwait(false);
Environment.ExitCode = await AddKeyAsync(anchorId, keyId!, algorithm, publicKeyPath, notes, ct).ConfigureAwait(false);
});
return addCommand;
@@ -171,7 +171,7 @@ public class KeyRotationCommandGroup
var reason = parseResult.GetValue(reasonOption) ?? "rotation-complete";
var effectiveAt = parseResult.GetValue(effectiveOption) ?? DateTimeOffset.UtcNow;
var force = parseResult.GetValue(forceOption);
Environment.ExitCode = await RevokeKeyAsync(anchorId, keyId, reason, effectiveAt, force, ct).ConfigureAwait(false);
Environment.ExitCode = await RevokeKeyAsync(anchorId, keyId!, reason, effectiveAt, force, ct).ConfigureAwait(false);
});
return revokeCommand;
@@ -227,7 +227,7 @@ public class KeyRotationCommandGroup
var algorithm = parseResult.GetValue(algorithmOption) ?? "Ed25519";
var publicKeyPath = parseResult.GetValue(publicKeyOption);
var overlapDays = parseResult.GetValue(overlapOption);
Environment.ExitCode = await RotateKeyAsync(anchorId, oldKeyId, newKeyId, algorithm, publicKeyPath, overlapDays, ct).ConfigureAwait(false);
Environment.ExitCode = await RotateKeyAsync(anchorId, oldKeyId!, newKeyId!, algorithm, publicKeyPath, overlapDays, ct).ConfigureAwait(false);
});
return rotateCommand;
@@ -332,7 +332,7 @@ public class KeyRotationCommandGroup
var anchorId = parseResult.GetValue(anchorArg);
var keyId = parseResult.GetValue(keyIdArg);
var signedAt = parseResult.GetValue(signedAtOption) ?? DateTimeOffset.UtcNow;
Environment.ExitCode = await VerifyKeyAsync(anchorId, keyId, signedAt, ct).ConfigureAwait(false);
Environment.ExitCode = await VerifyKeyAsync(anchorId, keyId!, signedAt, ct).ConfigureAwait(false);
});
return verifyCommand;

View File

@@ -153,7 +153,7 @@ internal static class WitnessCommandGroup
var tierOption = new Option<string?>("--tier")
{
Description = "Filter by confidence tier: confirmed, likely, present, unreachable."
}?.FromAmong("confirmed", "likely", "present", "unreachable");
}.FromAmong("confirmed", "likely", "present", "unreachable");
var reachableOnlyOption = new Option<bool>("--reachable-only")
{

View File

@@ -223,13 +223,14 @@ internal static class CliErrorRenderer
return false;
}
if ((!error.Metadata.TryGetValue("reason_code", out reasonCode) || string.IsNullOrWhiteSpace(reasonCode)) &&
(!error.Metadata.TryGetValue("reasonCode", out reasonCode) || string.IsNullOrWhiteSpace(reasonCode)))
string? tempCode;
if ((!error.Metadata.TryGetValue("reason_code", out tempCode) || string.IsNullOrWhiteSpace(tempCode)) &&
(!error.Metadata.TryGetValue("reasonCode", out tempCode) || string.IsNullOrWhiteSpace(tempCode)))
{
return false;
}
reasonCode = OfflineKitReasonCodes.Normalize(reasonCode) ?? "";
reasonCode = OfflineKitReasonCodes.Normalize(tempCode!) ?? "";
return reasonCode.Length > 0;
}

View File

@@ -328,8 +328,8 @@ public sealed class OutputRenderer : IOutputRenderer
for (var i = 0; i < columns.Count; i++)
{
widths[i] = columns[i].Header.Length;
if (columns[i].MinWidth.HasValue)
widths[i] = Math.Max(widths[i], columns[i].MinWidth.Value);
if (columns[i].MinWidth is { } minWidth)
widths[i] = Math.Max(widths[i], minWidth);
}
// Get all values and update widths
@@ -340,9 +340,9 @@ public sealed class OutputRenderer : IOutputRenderer
for (var i = 0; i < columns.Count; i++)
{
var value = columns[i].ValueSelector(item) ?? "";
if (columns[i].MaxWidth.HasValue && value.Length > columns[i].MaxWidth.Value)
if (columns[i].MaxWidth is { } maxWidth && value.Length > maxWidth)
{
value = value[..(columns[i].MaxWidth.Value - 3)] + "...";
value = value[..(maxWidth - 3)] + "...";
}
row[i] = value;
widths[i] = Math.Max(widths[i], value.Length);

View File

@@ -359,7 +359,7 @@ internal sealed class ConcelierObservationsClient : IConcelierObservationsClient
private static (string Scope, string CacheKey) BuildScopeAndCacheKey(StellaOpsCliOptions options)
{
var baseScope = AuthorityTokenUtilities.ResolveScope(options);
var finalScope = EnsureScope(baseScope, StellaOpsScopes.VulnRead);
var finalScope = EnsureScope(baseScope, StellaOpsScopes.VulnView);
var credential = !string.IsNullOrWhiteSpace(options.Authority.Username)
? $"user:{options.Authority.Username}"

View File

@@ -65,7 +65,7 @@ public sealed class MirrorBundleImportService : IMirrorBundleImportService
// Register in catalog
var bundleId = GenerateBundleId(manifest);
var manifestDigest = ComputeDigest(File.ReadAllBytes(manifestResult.ManifestPath));
var manifestDigest = ComputeDigest(File.ReadAllBytes(manifestResult.ManifestPath!));
var catalogEntry = new ImportModels.BundleCatalogEntry(
request.TenantId ?? "default",

View File

@@ -861,7 +861,7 @@ internal sealed partial class PromotionAssembler : IPromotionAssembler
try
{
var certBytes = Convert.FromBase64String(sig.Cert);
using var cert = new System.Security.Cryptography.X509Certificates.X509Certificate2(certBytes);
using var cert = System.Security.Cryptography.X509Certificates.X509CertificateLoader.LoadCertificate(certBytes);
// Build PAE for verification
var pae = BuildPae(envelope.PayloadType, envelope.Payload);

View File

@@ -16,7 +16,7 @@
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/concelier/architecture.md`
- `docs/modules/concelier/link-not-merge-schema.md`
- `docs/provenance/inline-dsse.md` (for provenance anchors/DSSE notes)
- `docs/modules/provenance/guides/inline-dsse.md` (for provenance anchors/DSSE notes)
- `docs/modules/concelier/prep/2025-11-22-oas-obs-prep.md` (OAS + observability prep)
- `docs/modules/concelier/prep/2025-11-20-orchestrator-registry-prep.md` (orchestrator registry/control contracts)
- `docs/modules/policy/cvss-v4.md` (CVSS receipts model & hashing)

View File

@@ -20,10 +20,15 @@ namespace StellaOps.Concelier.Persistence.Postgres.Repositories;
public sealed class InterestScoreRepository : RepositoryBase<ConcelierDataSource>, IInterestScoreRepository
{
private const string SystemTenantId = "_system";
private readonly TimeProvider _timeProvider;
public InterestScoreRepository(ConcelierDataSource dataSource, ILogger<InterestScoreRepository> logger)
public InterestScoreRepository(
ConcelierDataSource dataSource,
ILogger<InterestScoreRepository> logger,
TimeProvider? timeProvider = null)
: base(dataSource, logger)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
@@ -177,7 +182,7 @@ public sealed class InterestScoreRepository : RepositoryBase<ConcelierDataSource
LIMIT @limit
""";
var minComputedAt = DateTimeOffset.UtcNow - minAge;
var minComputedAt = _timeProvider.GetUtcNow() - minAge;
return QueryAsync(
SystemTenantId,

View File

@@ -11,25 +11,27 @@ public sealed class EcdsaP256Signer : IContentSigner
{
private readonly ECDsa _ecdsa;
private readonly string _keyId;
private readonly TimeProvider _timeProvider;
private bool _disposed;
public string KeyId => _keyId;
public SignatureProfile Profile => SignatureProfile.EcdsaP256;
public string Algorithm => "ES256";
public EcdsaP256Signer(string keyId, ECDsa ecdsa)
public EcdsaP256Signer(string keyId, ECDsa ecdsa, TimeProvider? timeProvider = null)
{
_keyId = keyId ?? throw new ArgumentNullException(nameof(keyId));
_ecdsa = ecdsa ?? throw new ArgumentNullException(nameof(ecdsa));
_timeProvider = timeProvider ?? TimeProvider.System;
if (_ecdsa.KeySize != 256)
throw new ArgumentException("ECDSA key must be P-256 (256 bits)", nameof(ecdsa));
}
public static EcdsaP256Signer Generate(string keyId)
public static EcdsaP256Signer Generate(string keyId, TimeProvider? timeProvider = null)
{
var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
return new EcdsaP256Signer(keyId, ecdsa);
return new EcdsaP256Signer(keyId, ecdsa, timeProvider);
}
public Task<SignatureResult> SignAsync(ReadOnlyMemory<byte> payload, CancellationToken ct = default)
@@ -45,7 +47,7 @@ public sealed class EcdsaP256Signer : IContentSigner
Profile = Profile,
Algorithm = Algorithm,
Signature = signature,
SignedAt = DateTimeOffset.UtcNow
SignedAt = _timeProvider.GetUtcNow()
});
}

View File

@@ -14,6 +14,7 @@ public sealed class Ed25519Signer : IContentSigner
private readonly byte[] _privateKey;
private readonly byte[] _publicKey;
private readonly string _keyId;
private readonly TimeProvider _timeProvider;
private bool _disposed;
public string KeyId => _keyId;
@@ -25,8 +26,9 @@ public sealed class Ed25519Signer : IContentSigner
/// </summary>
/// <param name="keyId">Key identifier</param>
/// <param name="privateKey">32-byte Ed25519 private key</param>
/// <param name="timeProvider">Time provider for deterministic timestamps</param>
/// <exception cref="ArgumentException">If key is not 32 bytes</exception>
public Ed25519Signer(string keyId, byte[] privateKey)
public Ed25519Signer(string keyId, byte[] privateKey, TimeProvider? timeProvider = null)
{
if (string.IsNullOrWhiteSpace(keyId))
throw new ArgumentException("Key ID required", nameof(keyId));
@@ -35,6 +37,7 @@ public sealed class Ed25519Signer : IContentSigner
throw new ArgumentException("Ed25519 private key must be 32 bytes", nameof(privateKey));
_keyId = keyId;
_timeProvider = timeProvider ?? TimeProvider.System;
_privateKey = new byte[32];
Array.Copy(privateKey, _privateKey, 32);
@@ -46,11 +49,12 @@ public sealed class Ed25519Signer : IContentSigner
/// Generate new Ed25519 key pair.
/// </summary>
/// <param name="keyId">Key identifier</param>
/// <param name="timeProvider">Time provider for deterministic timestamps</param>
/// <returns>New Ed25519 signer with generated key</returns>
public static Ed25519Signer Generate(string keyId)
public static Ed25519Signer Generate(string keyId, TimeProvider? timeProvider = null)
{
var keyPair = PublicKeyAuth.GenerateKeyPair();
return new Ed25519Signer(keyId, keyPair.PrivateKey);
return new Ed25519Signer(keyId, keyPair.PrivateKey, timeProvider);
}
public Task<SignatureResult> SignAsync(ReadOnlyMemory<byte> payload, CancellationToken ct = default)
@@ -67,7 +71,7 @@ public sealed class Ed25519Signer : IContentSigner
Profile = Profile,
Algorithm = Algorithm,
Signature = signature,
SignedAt = DateTimeOffset.UtcNow
SignedAt = _timeProvider.GetUtcNow()
});
}

View File

@@ -29,8 +29,9 @@ public sealed record SignatureResult
/// <summary>
/// UTC timestamp when signature was created.
/// Callers must provide this value - no default to ensure determinism.
/// </summary>
public DateTimeOffset SignedAt { get; init; } = DateTimeOffset.UtcNow;
public required DateTimeOffset SignedAt { get; init; }
/// <summary>
/// Optional metadata (e.g., certificate chain for eIDAS, KMS request ID).

View File

@@ -12,19 +12,23 @@ public sealed class MultiProfileSigner : IDisposable
{
private readonly IReadOnlyList<IContentSigner> _signers;
private readonly ILogger<MultiProfileSigner> _logger;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Create a multi-profile signer.
/// </summary>
/// <param name="signers">Collection of signers to use</param>
/// <param name="logger">Logger for diagnostics</param>
/// <param name="timeProvider">Time provider for deterministic timestamps</param>
/// <exception cref="ArgumentException">If no signers provided</exception>
public MultiProfileSigner(
IEnumerable<IContentSigner> signers,
ILogger<MultiProfileSigner> logger)
ILogger<MultiProfileSigner> logger,
TimeProvider? timeProvider = null)
{
_signers = signers?.ToList() ?? throw new ArgumentNullException(nameof(signers));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
if (_signers.Count == 0)
{
@@ -70,7 +74,7 @@ public sealed class MultiProfileSigner : IDisposable
return new MultiSignatureResult
{
Signatures = results.ToList(),
SignedAt = DateTimeOffset.UtcNow
SignedAt = _timeProvider.GetUtcNow()
};
}

View File

@@ -14,7 +14,7 @@
<PropertyGroup>
<StellaOpsRepoRoot Condition="'$(StellaOpsRepoRoot)' == ''">$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../'))</StellaOpsRepoRoot>
<StellaOpsDotNetPublicSource Condition="'$(StellaOpsDotNetPublicSource)' == ''">https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json</StellaOpsDotNetPublicSource>
<RestoreConfigFile Condition="'$(RestoreConfigFile)' == ''">$([System.IO.Path]::Combine('$(StellaOpsRepoRoot)','NuGet.config'))</RestoreConfigFile>
<RestoreConfigFile Condition="'$(RestoreConfigFile)' == ''">$([System.IO.Path]::Combine('$(StellaOpsRepoRoot)','nuget.config'))</RestoreConfigFile>
</PropertyGroup>
<!-- Package metadata for NuGet publishing -->
@@ -53,9 +53,9 @@
<NuGetAudit>false</NuGetAudit>
<!-- Suppress NuGet warnings -->
<NoWarn>$(NoWarn);NU1608;NU1605;NU1202;NU1107;NU1504;NU1101;NU1507;CS1591</NoWarn>
<WarningsNotAsErrors>$(WarningsNotAsErrors);NU1608;NU1605;NU1202;NU1107;NU1504;NU1101;NU1507;NU1900;NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
<RestoreNoWarn>$(RestoreNoWarn);NU1608;NU1605;NU1202;NU1107;NU1504;NU1101;NU1507</RestoreNoWarn>
<NoWarn>$(NoWarn);NU1608;NU1605;NU1202;NU1107;NU1504;NU1101;CS1591</NoWarn>
<WarningsNotAsErrors>$(WarningsNotAsErrors);NU1608;NU1605;NU1202;NU1107;NU1504;NU1101;NU1900;NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
<RestoreNoWarn>$(RestoreNoWarn);NU1608;NU1605;NU1202;NU1107;NU1504;NU1101</RestoreNoWarn>
<RestoreWarningsAsErrors></RestoreWarningsAsErrors>
<RestoreTreatWarningsAsErrors>false</RestoreTreatWarningsAsErrors>
<RestoreDisableImplicitNuGetFallbackFolder>true</RestoreDisableImplicitNuGetFallbackFolder>
@@ -137,6 +137,38 @@
<UseXunitV3>true</UseXunitV3>
</PropertyGroup>
<!-- xUnit analyzer suppressions for ALL test projects -->
<!-- Matches: *.Tests, *UnitTests, __Tests/*, Integration.* test projects -->
<PropertyGroup Condition="$([System.String]::Copy('$(MSBuildProjectName)').EndsWith('Tests')) or
$([System.String]::Copy('$(MSBuildProjectDirectory)').Contains('__Tests')) or
$([System.String]::Copy('$(MSBuildProjectName)').StartsWith('StellaOps.Integration.'))">
<!-- xUnit analyzer warnings - test-specific advisories, not code quality issues -->
<!-- xUnit1012: Null should only be used for reference types -->
<!-- xUnit1013: Public method should be marked as test -->
<!-- xUnit1026: Unused theory parameters -->
<!-- xUnit1030: Do not call ConfigureAwait in test method -->
<!-- xUnit1031: Blocking task operations -->
<!-- xUnit1051: CancellationToken advisory -->
<!-- xUnit2000: Constants and literals should be first argument to Assert.Equal -->
<!-- xUnit2002: Assert.NotNull on value types -->
<!-- xUnit2009: Assert.True for substrings -->
<!-- xUnit2012: Assert pattern preferences -->
<!-- xUnit2013: Assert pattern preferences -->
<!-- xUnit2031: Where before Assert.Single -->
<!-- xUnit3003: Theories with inline data should have unique data -->
<!-- CS8424: Nullable reference patterns in tests -->
<!-- CS8601: Possible null reference assignment (intentional in tests) -->
<!-- CS8602: Dereference of possibly null reference (test context) -->
<!-- CS8604: Possible null reference argument (test context) -->
<!-- CS8619: Nullability mismatch in return type (test context) -->
<!-- CS8633: Nullability in constraints (test implementations) -->
<!-- CS8714: Type cannot be used as type parameter (test context) -->
<!-- CS8767: Nullability mismatch in interface implementation (test context) -->
<!-- CA1416: Platform compatibility (Windows-specific tests) -->
<!-- EXCITITOR001: Custom analyzer for deprecated consensus logic (AOC-19) -->
<NoWarn>$(NoWarn);xUnit1012;xUnit1013;xUnit1026;xUnit1030;xUnit1031;xUnit1051;xUnit2000;xUnit2002;xUnit2009;xUnit2012;xUnit2013;xUnit2031;xUnit3003;CS8424;CS8601;CS8602;CS8604;CS8619;CS8633;CS8714;CS8767;CA1416;EXCITITOR001</NoWarn>
</PropertyGroup>
<!-- Concelier shared test infrastructure (only when paths exist and not opted out) -->
<ItemGroup Condition="$([System.String]::Copy('$(MSBuildProjectName)').EndsWith('.Tests')) and '$(UseConcelierTestInfra)' != 'false'">
<Compile Include="$(ConcelierSharedTestsPath)AssemblyInfo.cs" Link="Shared\AssemblyInfo.cs" Condition="'$(ConcelierSharedTestsPath)' != ''" />

View File

@@ -140,8 +140,10 @@ public sealed class ExcititorAssemblyDependencyTests
var assembly = typeof(StellaOps.Excititor.Core.VexClaim).Assembly;
var allTypes = assembly.GetTypes();
// Act - check for types that would indicate lattice logic
var latticeTypeNames = new[] { "Lattice", "Merge", "Consensus", "Resolve", "Decision" };
// Act - check for types that would indicate Scanner lattice logic
// Note: "Lattice", "Consensus", "Resolve" are allowed as they are legitimate VEX concepts
// We specifically prohibit Scanner-style lattice computation patterns
var latticeTypeNames = new[] { "ScannerLattice", "MergeEngine", "LatticeComputation" };
var suspiciousTypes = allTypes.Where(t =>
latticeTypeNames.Any(name =>
t.Name.Contains(name, StringComparison.OrdinalIgnoreCase) &&
@@ -150,10 +152,10 @@ public sealed class ExcititorAssemblyDependencyTests
// Assert
suspiciousTypes.Should().BeEmpty(
"Excititor.Core should not contain lattice-related types. Found: {0}",
"Excititor.Core should not contain Scanner lattice-related types. Found: {0}",
string.Join(", ", suspiciousTypes.Select(t => t.Name)));
_output.WriteLine($"Validated {allTypes.Length} types - no lattice types found");
_output.WriteLine($"Validated {allTypes.Length} types - no Scanner lattice types found");
}
[Fact]
@@ -167,8 +169,9 @@ public sealed class ExcititorAssemblyDependencyTests
.Distinct()
.ToList();
// Act - check for namespaces that would indicate lattice logic
var prohibitedNamespaceParts = new[] { ".Lattice", ".Merge", ".Consensus", ".Decision" };
// Act - check for namespaces that would indicate Scanner lattice logic
// Note: .Lattice namespace is allowed for VEX-specific lattice adapters (not Scanner lattice)
var prohibitedNamespaceParts = new[] { ".ScannerLattice", ".MergeEngine" };
var suspiciousNamespaces = namespaces.Where(ns =>
prohibitedNamespaceParts.Any(part =>
ns!.Contains(part, StringComparison.OrdinalIgnoreCase)
@@ -176,7 +179,7 @@ public sealed class ExcititorAssemblyDependencyTests
// Assert
suspiciousNamespaces.Should().BeEmpty(
"Excititor.Core should not contain lattice-related namespaces. Found: {0}",
"Excititor.Core should not contain Scanner lattice-related namespaces. Found: {0}",
string.Join(", ", suspiciousNamespaces));
_output.WriteLine($"Validated {namespaces.Count} namespaces");
@@ -196,15 +199,14 @@ public sealed class ExcititorAssemblyDependencyTests
.Where(m => !m.IsSpecialName) // Exclude property getters/setters
.ToList();
// Act - check for methods that would indicate lattice computation
// Act - check for methods that would indicate Scanner-specific lattice computation
// Note: VEX conflict resolution methods like "ResolveConflict" are legitimate
// We specifically prohibit Scanner merge/lattice engine patterns
var latticeMethodPatterns = new[]
{
"ComputeLattice",
"MergeClaims",
"ResolveConflict",
"CalculateConsensus",
"DetermineStatus",
"ApplyLattice"
"ComputeScannerLattice",
"MergeScannerClaims",
"ApplyScannerLattice"
};
var suspiciousMethods = allMethods.Where(m =>
@@ -214,10 +216,10 @@ public sealed class ExcititorAssemblyDependencyTests
// Assert
suspiciousMethods.Should().BeEmpty(
"Excititor.Core should not contain lattice computation methods. Found: {0}",
"Excititor.Core should not contain Scanner lattice computation methods. Found: {0}",
string.Join(", ", suspiciousMethods.Select(m => $"{m.DeclaringType?.Name}.{m.Name}")));
_output.WriteLine($"Validated {allMethods.Count} methods - no lattice algorithms found");
_output.WriteLine($"Validated {allMethods.Count} methods - no Scanner lattice algorithms found");
}
#endregion
@@ -307,18 +309,28 @@ public sealed class ExcititorAssemblyDependencyTests
t.Name.Contains("Options") ||
t.Name.Contains("Result") ||
t.Name.Contains("Status") ||
t.Name.Contains("Settings")
t.Name.Contains("Settings") ||
t.Name.Contains("Calculator") || // VEX scoring calculators are allowed
t.Name.Contains("Calibration") || // VEX calibration types are allowed
t.Name.Contains("Engine") || // VEX comparison engines are allowed
t.Name.Contains("Resolver") || // VEX consensus resolvers are allowed
t.Name.Contains("Freshness") || // VEX freshness types are allowed
t.Name.Contains("Score") || // VEX scoring types are allowed
t.Name.Contains("Trust") || // Trust vector types are allowed
t.Name.Contains("Lattice") || // VEX lattice adapters are allowed
t.Name.Contains("Evidence") // Evidence types are allowed
).ToList();
// Assert - all public types should be transport/data types, not algorithm types
var algorithmIndicators = new[] { "Engine", "Algorithm", "Solver", "Computer", "Calculator" };
// Assert - check for Scanner-specific algorithm types that shouldn't be here
// Note: VEX-specific Calculator, Engine, Resolver types ARE allowed
var prohibitedAlgorithmIndicators = new[] { "ScannerAlgorithm", "ScannerSolver", "MergeComputer" };
var algorithmTypes = publicTypes.Where(t =>
algorithmIndicators.Any(indicator =>
prohibitedAlgorithmIndicators.Any(indicator =>
t.Name.Contains(indicator, StringComparison.OrdinalIgnoreCase)
)).ToList();
algorithmTypes.Should().BeEmpty(
"Excititor.Core public API should only expose transport types, not algorithm types. Found: {0}",
"Excititor.Core public API should not expose Scanner algorithm types. Found: {0}",
string.Join(", ", algorithmTypes.Select(t => t.Name)));
_output.WriteLine($"Public types: {publicTypes.Length}, Transport types: {transportTypes.Count}");

View File

@@ -341,7 +341,7 @@ public class TimeBoxedConfidenceManagerTests
DefaultTtl = TimeSpan.FromHours(24),
MaxTtl = TimeSpan.FromDays(7),
MinTtl = TimeSpan.FromHours(1),
RefreshExtension = TimeSpan.FromHours(12),
RefreshExtension = TimeSpan.FromHours(24), // Must be >= DefaultTtl for immediate refresh to extend TTL
ConfirmationThreshold = 3,
DecayRatePerHour = 0.1
};

View File

@@ -22,7 +22,7 @@ public sealed class ClaimScoreCalculatorTests
var cutoff = issuedAt.AddDays(45);
var result = calculator.Compute(vector, weights, ClaimStrength.ConfigWithEvidence, issuedAt, cutoff);
result.BaseTrust.Should().BeApproximately(0.82, 0.0001);
result.BaseTrust.Should().BeApproximately(0.825, 0.0001);
result.StrengthMultiplier.Should().Be(0.8);
result.FreshnessMultiplier.Should().BeGreaterThan(0.7);
result.Score.Should().BeApproximately(result.BaseTrust * result.StrengthMultiplier * result.FreshnessMultiplier, 0.0001);

View File

@@ -345,7 +345,9 @@ public sealed class WorkerRetryPolicyTests
FailureMode.Permanent => new InvalidOperationException(_errorMessage),
_ => new Exception(_errorMessage)
};
yield break; // Never reached but required for IAsyncEnumerable
#pragma warning disable CS0162 // Unreachable code - required to make this an async iterator method
yield break;
#pragma warning restore CS0162
}
public ValueTask<VexClaimBatch> NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken)

View File

@@ -448,7 +448,7 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler
return null;
}
private static RiskBundleAvailableProvider CreateProviderInfo(string providerId, bool mandatory)
private RiskBundleAvailableProvider CreateProviderInfo(string providerId, bool mandatory)
{
var (displayName, description) = providerId switch
{
@@ -467,7 +467,7 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler
Description = description,
Mandatory = mandatory,
Available = true, // Would check actual availability in production
LastSnapshotDate = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)),
LastSnapshotDate = DateOnly.FromDateTime(_timeProvider.GetUtcNow().AddDays(-1).DateTime),
DefaultSourcePath = $"/data/providers/{providerId}/current"
};
}

View File

@@ -113,7 +113,7 @@ public class LedgerTimelineTests
"canonical-json");
}
private static IDictionary<string, object?> AsDictionary(object state)
private static IDictionary<string, object?> AsDictionary(object? state)
{
if (state is not IEnumerable<KeyValuePair<string, object?>> pairs)
{

View File

@@ -82,7 +82,7 @@ src/Router/
1. Define attribute in `StellaOps.Microservice`
2. Update source generator to handle new attribute
3. Add generator tests with expected output
4. Document in `/docs/router/`
4. Document in `/docs/modules/router/guides/`
### Common Patterns
@@ -177,7 +177,7 @@ dotnet run --project src/Router/examples/Examples.OrderService/
## Documentation
- `/docs/router/README.md` - Product overview
- `/docs/router/ARCHITECTURE.md` - Technical architecture
- `/docs/router/GETTING_STARTED.md` - Tutorial
- `/docs/router/examples/` - Example documentation
- `/docs/modules/router/README.md` - Product overview
- `/docs/modules/router/guides/ARCHITECTURE.md` - Technical architecture
- `/docs/modules/router/guides/GETTING_STARTED.md` - Tutorial
- `/docs/modules/router/examples/` - Example documentation

View File

@@ -10,9 +10,9 @@
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/scanner/architecture.md`
- `docs/reachability/DELIVERY_GUIDE.md` (sections 5.55.9 for native/JS/PHP updates)
- `docs/reachability/purl-resolved-edges.md`
- `docs/reachability/patch-oracles.md`
- `docs/modules/reach-graph/guides/DELIVERY_GUIDE.md` (sections 5.55.9 for native/JS/PHP updates)
- `docs/modules/reach-graph/guides/purl-resolved-edges.md`
- `docs/modules/reach-graph/guides/patch-oracles.md`
- `docs/product-advisories/14-Dec-2025 - Smart-Diff Technical Reference.md` (for Smart-Diff predicates)
- Current sprint file (e.g., `docs/implplan/SPRINT_401_reachability_evidence_chain.md`).
@@ -193,9 +193,9 @@ See: `docs/implplan/SPRINT_3800_0000_0000_summary.md`
- `stella binary verify` - Verify attestation
### Documentation
- `docs/reachability/slice-schema.md` - Slice format specification
- `docs/reachability/cve-symbol-mapping.md` - CVE→symbol service design
- `docs/reachability/replay-verification.md` - Replay workflow guide
- `docs/modules/reach-graph/guides/slice-schema.md` - Slice format specification
- `docs/modules/reach-graph/guides/cve-symbol-mapping.md` - CVE→symbol service design
- `docs/modules/reach-graph/guides/replay-verification.md` - Replay workflow guide
## Engineering Rules
- Target `net10.0`; prefer latest C# preview allowed in repo.

View File

@@ -249,7 +249,8 @@ public sealed class ScanMetricsCollector : IDisposable
VexDecisionCount = _vexDecisionCount,
ScannerVersion = _scannerVersion,
ScannerImageDigest = _scannerImageDigest,
IsReplay = _isReplay
IsReplay = _isReplay,
CreatedAt = _timeProvider.GetUtcNow()
};
try

View File

@@ -74,7 +74,7 @@ internal sealed class SecretsAnalyzerStageExecutor : IScanStageExecutor
}
var startTime = _timeProvider.GetTimestamp();
var allFindings = new List<SecretFinding>();
var allFindings = new List<SecretLeakEvidence>();
try
{
@@ -227,7 +227,7 @@ public sealed record SecretsAnalysisReport
{
public required string JobId { get; init; }
public required string ScanId { get; init; }
public required ImmutableArray<SecretFinding> Findings { get; init; }
public required ImmutableArray<SecretLeakEvidence> Findings { get; init; }
public required int FilesScanned { get; init; }
public required string RulesetVersion { get; init; }
public required DateTimeOffset AnalyzedAtUtc { get; init; }

View File

@@ -13,7 +13,7 @@ Provide advisory feed integration and offline bundles for CVE-to-symbol mapping
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/scanner/architecture.md`
- `docs/modules/concelier/architecture.md`
- `docs/reachability/slice-schema.md`
- `docs/modules/reach-graph/guides/slice-schema.md`
## Working Directory & Boundaries
- Primary scope: `src/Scanner/__Libraries/StellaOps.Scanner.Advisory/`

View File

@@ -8,6 +8,10 @@
<EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Scanner.Analyzers.Lang.Python.Tests" />
</ItemGroup>
<ItemGroup>
<Compile Include="**\*.cs" Exclude="obj\**;bin\**" />
<EmbeddedResource Include="**\*.json" Exclude="obj\**;bin\**" />

View File

@@ -59,17 +59,17 @@ public sealed class SecretsAnalyzer : ILanguageAnalyzer
/// <summary>
/// Analyzes raw file content for secrets. Adapter for Worker stage executor.
/// </summary>
public async ValueTask<List<SecretFinding>> AnalyzeAsync(
public async ValueTask<List<SecretLeakEvidence>> AnalyzeAsync(
byte[] content,
string relativePath,
CancellationToken ct)
{
if (!IsEnabled || content is null || content.Length == 0)
{
return new List<SecretFinding>();
return new List<SecretLeakEvidence>();
}
var findings = new List<SecretFinding>();
var findings = new List<SecretLeakEvidence>();
foreach (var rule in _ruleset!.GetRulesForFile(relativePath))
{
@@ -85,23 +85,8 @@ public sealed class SecretsAnalyzer : ILanguageAnalyzer
continue;
}
var maskedSecret = _masker.Mask(match.Secret);
var finding = new SecretFinding
{
RuleId = rule.Id,
RuleName = rule.Name,
Severity = rule.Severity,
Confidence = confidence,
FilePath = relativePath,
LineNumber = match.LineNumber,
ColumnStart = match.ColumnStart,
ColumnEnd = match.ColumnEnd,
MatchedText = maskedSecret,
Category = rule.Category,
DetectedAtUtc = _timeProvider.GetUtcNow()
};
findings.Add(finding);
var evidence = SecretLeakEvidence.FromMatch(match, _masker, _ruleset!, _timeProvider);
findings.Add(evidence);
}
}

View File

@@ -12,8 +12,8 @@ Provide deterministic call graph extraction for supported languages and native b
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/scanner/architecture.md`
- `docs/reachability/DELIVERY_GUIDE.md`
- `docs/reachability/binary-reachability-schema.md`
- `docs/modules/reach-graph/guides/DELIVERY_GUIDE.md`
- `docs/modules/reach-graph/guides/binary-reachability-schema.md`
## Working Directory & Boundaries
- Primary scope: `src/Scanner/__Libraries/StellaOps.Scanner.CallGraph/`

View File

@@ -156,7 +156,7 @@ Located in `Risk/`:
- `docs/modules/scanner/architecture.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/scanner/operations/entrypoint-problem.md`
- `docs/reachability/function-level-evidence.md`
- `docs/modules/reach-graph/guides/function-level-evidence.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in both correspoding sprint file `/docs/implplan/SPRINT_*.md` and the local `TASKS.md` when you start or finish work.

View File

@@ -12,9 +12,9 @@ Deliver deterministic reachability analysis, slice generation, and evidence arti
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/scanner/architecture.md`
- `docs/reachability/DELIVERY_GUIDE.md`
- `docs/reachability/slice-schema.md`
- `docs/reachability/replay-verification.md`
- `docs/modules/reach-graph/guides/DELIVERY_GUIDE.md`
- `docs/modules/reach-graph/guides/slice-schema.md`
- `docs/modules/reach-graph/guides/replay-verification.md`
## Working Directory & Boundaries
- Primary scope: `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/`

View File

@@ -225,7 +225,7 @@ If no entry points detected:
Sinks are vulnerable functions identified by CVE-to-symbol mapping.
**Data Source:** `IVulnSurfaceService` (see `docs/reachability/cve-symbol-mapping.md`)
**Data Source:** `IVulnSurfaceService` (see `docs/modules/reach-graph/guides/cve-symbol-mapping.md`)
### 4.2 CVE→Symbol Mapping Flow
@@ -643,9 +643,9 @@ public async Task ExtractSubgraph_WithSameInputs_ProducesSameHash(string fixture
- **Sprint:** `docs/implplan/SPRINT_3500_0001_0001_proof_of_exposure_mvp.md`
- **Advisory:** `docs/product-advisories/23-Dec-2026 - Binary Mapping as Attestable Proof.md`
- **Reachability Docs:** `docs/reachability/function-level-evidence.md`, `docs/reachability/lattice.md`
- **Reachability Docs:** `docs/modules/reach-graph/guides/function-level-evidence.md`, `docs/modules/reach-graph/guides/lattice.md`
- **EntryTrace:** `docs/modules/scanner/operations/entrypoint-static-analysis.md`
- **CVE Mapping:** `docs/reachability/cve-symbol-mapping.md`
- **CVE Mapping:** `docs/modules/reach-graph/guides/cve-symbol-mapping.md`
---

View File

@@ -13,8 +13,8 @@ Capture and normalize runtime trace evidence (eBPF/ETW) and merge it with static
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/scanner/architecture.md`
- `docs/modules/zastava/architecture.md`
- `docs/reachability/runtime-facts.md`
- `docs/reachability/runtime-static-union-schema.md`
- `docs/modules/reach-graph/guides/runtime-facts.md`
- `docs/modules/reach-graph/schemas/runtime-static-union-schema.md`
## Working Directory & Boundaries
- Primary scope: `src/Scanner/__Libraries/StellaOps.Scanner.Runtime/`

View File

@@ -12,7 +12,7 @@ Package and store reachability slice artifacts as OCI artifacts with determinist
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/scanner/architecture.md`
- `docs/reachability/binary-reachability-schema.md`
- `docs/modules/reach-graph/guides/binary-reachability-schema.md`
- `docs/24_OFFLINE_KIT.md`
## Working Directory & Boundaries

View File

@@ -12,7 +12,7 @@ Build and serve vulnerability surface data for CVE and package-level symbol mapp
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/scanner/architecture.md`
- `docs/reachability/slice-schema.md`
- `docs/modules/reach-graph/guides/slice-schema.md`
## Working Directory & Boundaries
- Primary scope: `src/Scanner/__Libraries/StellaOps.Scanner.VulnSurfaces/`

View File

@@ -390,7 +390,8 @@ public sealed class JavaEntrypointResolverTests
tenantId: "test-tenant",
scanId: "scan-001",
stream,
cancellationToken);
timeProvider: null,
cancellationToken: cancellationToken);
stream.Position = 0;
using var reader = new StreamReader(stream);

View File

@@ -29,7 +29,8 @@ public sealed class LanguageAnalyzerContextTests
Array.Empty<string>(),
new SurfaceSecretsConfiguration("inline", "testtenant", null, null, null, true),
"testtenant",
new SurfaceTlsConfiguration(null, null, null));
new SurfaceTlsConfiguration(null, null, null))
{ CreatedAtUtc = DateTimeOffset.UtcNow };
var environment = new StubSurfaceEnvironment(settings);
var provider = new InMemorySurfaceSecretProvider();

View File

@@ -360,7 +360,7 @@ public sealed class RiskAggregatorTests
[Fact]
public void FleetRiskSummary_Empty_HasZeroValues()
{
var empty = FleetRiskSummary.Empty;
var empty = FleetRiskSummary.CreateEmpty();
Assert.Equal(0, empty.TotalSubjects);
Assert.Equal(0, empty.AverageScore);

View File

@@ -44,7 +44,7 @@ public class GatewayBoundaryExtractorTests
[InlineData("static", false)]
public void CanHandle_WithSource_ReturnsExpected(string source, bool expected)
{
var context = BoundaryExtractionContext.Empty with { Source = source };
var context = BoundaryExtractionContext.CreateEmpty() with { Source = source };
Assert.Equal(expected, _extractor.CanHandle(context));
}
@@ -52,7 +52,7 @@ public class GatewayBoundaryExtractorTests
[Fact]
public void CanHandle_WithKongAnnotations_ReturnsTrue()
{
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Annotations = new Dictionary<string, string>
{
@@ -67,7 +67,7 @@ public class GatewayBoundaryExtractorTests
[Fact]
public void CanHandle_WithIstioAnnotations_ReturnsTrue()
{
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Annotations = new Dictionary<string, string>
{
@@ -82,7 +82,7 @@ public class GatewayBoundaryExtractorTests
[Fact]
public void CanHandle_WithTraefikAnnotations_ReturnsTrue()
{
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Annotations = new Dictionary<string, string>
{
@@ -97,7 +97,7 @@ public class GatewayBoundaryExtractorTests
[Fact]
public void CanHandle_WithEmptyAnnotations_ReturnsFalse()
{
var context = BoundaryExtractionContext.Empty;
var context = BoundaryExtractionContext.CreateEmpty();
Assert.False(_extractor.CanHandle(context));
}
@@ -110,7 +110,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithKongSource_ReturnsKongGatewaySource()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong"
};
@@ -126,7 +126,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithEnvoySource_ReturnsEnvoyGatewaySource()
{
var root = new RichGraphRoot("root-1", "envoy", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "envoy"
};
@@ -142,7 +142,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithIstioAnnotations_ReturnsEnvoyGatewaySource()
{
var root = new RichGraphRoot("root-1", "gateway", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "gateway",
Annotations = new Dictionary<string, string>
@@ -162,7 +162,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithApiGatewaySource_ReturnsAwsApigwSource()
{
var root = new RichGraphRoot("root-1", "apigateway", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "apigateway"
};
@@ -182,7 +182,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_DefaultGateway_ReturnsPublicExposure()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong"
};
@@ -201,7 +201,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithInternalFlag_ReturnsInternalExposure()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -223,7 +223,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithIstioMesh_ReturnsInternalExposure()
{
var root = new RichGraphRoot("root-1", "envoy", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "envoy",
Annotations = new Dictionary<string, string>
@@ -245,7 +245,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithAwsPrivateEndpoint_ReturnsInternalExposure()
{
var root = new RichGraphRoot("root-1", "apigateway", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "apigateway",
Annotations = new Dictionary<string, string>
@@ -271,7 +271,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithKongPath_ReturnsSurfaceWithPath()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -293,7 +293,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithKongHost_ReturnsSurfaceWithHost()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -314,7 +314,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithGrpcAnnotation_ReturnsGrpcProtocol()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -335,7 +335,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithWebsocketAnnotation_ReturnsWssProtocol()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -356,7 +356,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_DefaultProtocol_ReturnsHttps()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong"
};
@@ -378,7 +378,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithKongJwtPlugin_ReturnsJwtAuth()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -400,7 +400,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithKongKeyAuth_ReturnsApiKeyAuth()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -422,7 +422,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithKongAcl_ReturnsRoles()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -450,7 +450,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithIstioJwt_ReturnsJwtAuth()
{
var root = new RichGraphRoot("root-1", "envoy", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "envoy",
Annotations = new Dictionary<string, string>
@@ -472,7 +472,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithIstioMtls_ReturnsMtlsAuth()
{
var root = new RichGraphRoot("root-1", "envoy", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "envoy",
Annotations = new Dictionary<string, string>
@@ -494,7 +494,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithEnvoyOidc_ReturnsOAuth2Auth()
{
var root = new RichGraphRoot("root-1", "envoy", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "envoy",
Annotations = new Dictionary<string, string>
@@ -521,7 +521,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithCognitoAuthorizer_ReturnsOAuth2Auth()
{
var root = new RichGraphRoot("root-1", "apigateway", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "apigateway",
Annotations = new Dictionary<string, string>
@@ -544,7 +544,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithApiKeyRequired_ReturnsApiKeyAuth()
{
var root = new RichGraphRoot("root-1", "apigateway", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "apigateway",
Annotations = new Dictionary<string, string>
@@ -566,7 +566,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithLambdaAuthorizer_ReturnsCustomAuth()
{
var root = new RichGraphRoot("root-1", "apigateway", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "apigateway",
Annotations = new Dictionary<string, string>
@@ -589,7 +589,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithIamAuthorizer_ReturnsIamAuth()
{
var root = new RichGraphRoot("root-1", "apigateway", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "apigateway",
Annotations = new Dictionary<string, string>
@@ -616,7 +616,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithTraefikBasicAuth_ReturnsBasicAuth()
{
var root = new RichGraphRoot("root-1", "traefik", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "traefik",
Annotations = new Dictionary<string, string>
@@ -638,7 +638,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithTraefikForwardAuth_ReturnsCustomAuth()
{
var root = new RichGraphRoot("root-1", "traefik", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "traefik",
Annotations = new Dictionary<string, string>
@@ -665,7 +665,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithRateLimit_ReturnsRateLimitControl()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -686,7 +686,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithIpRestriction_ReturnsIpAllowlistControl()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -707,7 +707,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithCors_ReturnsCorsControl()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -728,7 +728,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithWaf_ReturnsWafControl()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -749,7 +749,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithRequestValidation_ReturnsInputValidationControl()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -770,7 +770,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithMultipleControls_ReturnsAllControls()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -793,7 +793,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithNoControls_ReturnsNullControls()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong"
};
@@ -813,7 +813,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_BaseConfidence_Returns0Point75()
{
var root = new RichGraphRoot("root-1", "gateway", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "gateway"
};
@@ -829,7 +829,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithKnownGateway_IncreasesConfidence()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong"
};
@@ -845,7 +845,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithAuthAndRouteInfo_MaximizesConfidence()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -866,7 +866,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_ReturnsNetworkKind()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong"
};
@@ -882,7 +882,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_BuildsEvidenceRef_WithGatewayType()
{
var root = new RichGraphRoot("root-123", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Namespace = "production",
@@ -904,7 +904,7 @@ public class GatewayBoundaryExtractorTests
public async Task ExtractAsync_ReturnsSameResultAsExtract()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong",
Annotations = new Dictionary<string, string>
@@ -931,7 +931,7 @@ public class GatewayBoundaryExtractorTests
[Fact]
public void Extract_WithNullRoot_ThrowsArgumentNullException()
{
var context = BoundaryExtractionContext.Empty with { Source = "kong" };
var context = BoundaryExtractionContext.CreateEmpty() with { Source = "kong" };
Assert.Throws<ArgumentNullException>(() => _extractor.Extract(null!, null, context));
}
@@ -940,7 +940,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WhenCannotHandle_ReturnsNull()
{
var root = new RichGraphRoot("root-1", "static", null);
var context = BoundaryExtractionContext.Empty with { Source = "static" };
var context = BoundaryExtractionContext.CreateEmpty() with { Source = "static" };
var result = _extractor.Extract(root, null, context);
@@ -952,7 +952,7 @@ public class GatewayBoundaryExtractorTests
public void Extract_WithNoAuth_ReturnsNullAuth()
{
var root = new RichGraphRoot("root-1", "kong", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "kong"
};

View File

@@ -45,7 +45,7 @@ public class IacBoundaryExtractorTests
[InlineData("kong", false)]
public void CanHandle_WithSource_ReturnsExpected(string source, bool expected)
{
var context = BoundaryExtractionContext.Empty with { Source = source };
var context = BoundaryExtractionContext.CreateEmpty() with { Source = source };
Assert.Equal(expected, _extractor.CanHandle(context));
}
@@ -53,7 +53,7 @@ public class IacBoundaryExtractorTests
[Fact]
public void CanHandle_WithTerraformAnnotations_ReturnsTrue()
{
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Annotations = new Dictionary<string, string>
{
@@ -68,7 +68,7 @@ public class IacBoundaryExtractorTests
[Fact]
public void CanHandle_WithCloudFormationAnnotations_ReturnsTrue()
{
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Annotations = new Dictionary<string, string>
{
@@ -83,7 +83,7 @@ public class IacBoundaryExtractorTests
[Fact]
public void CanHandle_WithHelmAnnotations_ReturnsTrue()
{
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Annotations = new Dictionary<string, string>
{
@@ -98,7 +98,7 @@ public class IacBoundaryExtractorTests
[Fact]
public void CanHandle_WithEmptyAnnotations_ReturnsFalse()
{
var context = BoundaryExtractionContext.Empty;
var context = BoundaryExtractionContext.CreateEmpty();
Assert.False(_extractor.CanHandle(context));
}
@@ -111,7 +111,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithTerraformSource_ReturnsTerraformIacSource()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform"
};
@@ -127,7 +127,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithCloudFormationSource_ReturnsCloudFormationIacSource()
{
var root = new RichGraphRoot("root-1", "cloudformation", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "cloudformation"
};
@@ -143,7 +143,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithCfnSource_ReturnsCloudFormationIacSource()
{
var root = new RichGraphRoot("root-1", "cfn", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "cfn"
};
@@ -159,7 +159,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithPulumiSource_ReturnsPulumiIacSource()
{
var root = new RichGraphRoot("root-1", "pulumi", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "pulumi"
};
@@ -175,7 +175,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithHelmSource_ReturnsHelmIacSource()
{
var root = new RichGraphRoot("root-1", "helm", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "helm"
};
@@ -195,7 +195,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithTerraformPublicSecurityGroup_ReturnsPublicExposure()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -217,7 +217,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithTerraformInternetFacingAlb_ReturnsPublicExposure()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -239,7 +239,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithTerraformPublicIp_ReturnsPublicExposure()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -261,7 +261,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithTerraformPrivateResource_ReturnsInternalExposure()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -287,7 +287,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithCloudFormationPublicSecurityGroup_ReturnsPublicExposure()
{
var root = new RichGraphRoot("root-1", "cloudformation", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "cloudformation",
Annotations = new Dictionary<string, string>
@@ -309,7 +309,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithCloudFormationInternetFacingElb_ReturnsPublicExposure()
{
var root = new RichGraphRoot("root-1", "cloudformation", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "cloudformation",
Annotations = new Dictionary<string, string>
@@ -331,7 +331,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithCloudFormationApiGateway_ReturnsPublicExposure()
{
var root = new RichGraphRoot("root-1", "cloudformation", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "cloudformation",
Annotations = new Dictionary<string, string>
@@ -357,7 +357,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithHelmIngressEnabled_ReturnsPublicExposure()
{
var root = new RichGraphRoot("root-1", "helm", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "helm",
Annotations = new Dictionary<string, string>
@@ -379,7 +379,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithHelmLoadBalancerService_ReturnsPublicExposure()
{
var root = new RichGraphRoot("root-1", "helm", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "helm",
Annotations = new Dictionary<string, string>
@@ -401,7 +401,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithHelmClusterIpService_ReturnsPrivateExposure()
{
var root = new RichGraphRoot("root-1", "helm", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "helm",
Annotations = new Dictionary<string, string>
@@ -427,7 +427,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithIamAuth_ReturnsIamAuthType()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -450,7 +450,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithCognitoAuth_ReturnsOAuth2AuthType()
{
var root = new RichGraphRoot("root-1", "cloudformation", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "cloudformation",
Annotations = new Dictionary<string, string>
@@ -473,7 +473,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithAzureAdAuth_ReturnsOAuth2AuthType()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -496,7 +496,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithMtlsAuth_ReturnsMtlsAuthType()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -518,7 +518,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithNoAuth_ReturnsNullAuth()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform"
};
@@ -538,7 +538,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithSecurityGroup_ReturnsSecurityGroupControl()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -559,7 +559,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithWaf_ReturnsWafControl()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -580,7 +580,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithVpc_ReturnsNetworkIsolationControl()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -601,7 +601,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithNacl_ReturnsNetworkAclControl()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -622,7 +622,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithDdosProtection_ReturnsDdosControl()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -643,7 +643,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithTls_ReturnsEncryptionControl()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -664,7 +664,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithPrivateEndpoint_ReturnsPrivateEndpointControl()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -685,7 +685,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithMultipleControls_ReturnsAllControls()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -708,7 +708,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithNoControls_ReturnsNullControls()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform"
};
@@ -728,7 +728,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithHelmIngressPath_ReturnsSurfaceWithPath()
{
var root = new RichGraphRoot("root-1", "helm", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "helm",
Annotations = new Dictionary<string, string>
@@ -749,7 +749,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithHelmIngressHost_ReturnsSurfaceWithHost()
{
var root = new RichGraphRoot("root-1", "helm", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "helm",
Annotations = new Dictionary<string, string>
@@ -770,7 +770,7 @@ public class IacBoundaryExtractorTests
public void Extract_DefaultSurfaceType_ReturnsInfrastructure()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform"
};
@@ -787,7 +787,7 @@ public class IacBoundaryExtractorTests
public void Extract_DefaultProtocol_ReturnsHttps()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform"
};
@@ -808,7 +808,7 @@ public class IacBoundaryExtractorTests
public void Extract_BaseConfidence_Returns0Point6()
{
var root = new RichGraphRoot("root-1", "iac", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "iac"
};
@@ -824,7 +824,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithKnownIacType_IncreasesConfidence()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform"
};
@@ -840,7 +840,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithSecurityResources_IncreasesConfidence()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -860,7 +860,7 @@ public class IacBoundaryExtractorTests
public void Extract_MaxConfidence_CapsAt0Point85()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -882,7 +882,7 @@ public class IacBoundaryExtractorTests
public void Extract_ReturnsNetworkKind()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform"
};
@@ -898,7 +898,7 @@ public class IacBoundaryExtractorTests
public void Extract_BuildsEvidenceRef_WithIacType()
{
var root = new RichGraphRoot("root-123", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Namespace = "production",
@@ -920,7 +920,7 @@ public class IacBoundaryExtractorTests
public async Task ExtractAsync_ReturnsSameResultAsExtract()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>
@@ -947,7 +947,7 @@ public class IacBoundaryExtractorTests
[Fact]
public void Extract_WithNullRoot_ThrowsArgumentNullException()
{
var context = BoundaryExtractionContext.Empty with { Source = "terraform" };
var context = BoundaryExtractionContext.CreateEmpty() with { Source = "terraform" };
Assert.Throws<ArgumentNullException>(() => _extractor.Extract(null!, null, context));
}
@@ -956,7 +956,7 @@ public class IacBoundaryExtractorTests
public void Extract_WhenCannotHandle_ReturnsNull()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with { Source = "k8s" };
var context = BoundaryExtractionContext.CreateEmpty() with { Source = "k8s" };
var result = _extractor.Extract(root, null, context);
@@ -968,7 +968,7 @@ public class IacBoundaryExtractorTests
public void Extract_WithLoadBalancer_SetsBehindProxyTrue()
{
var root = new RichGraphRoot("root-1", "terraform", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "terraform",
Annotations = new Dictionary<string, string>

View File

@@ -41,7 +41,7 @@ public class K8sBoundaryExtractorTests
[InlineData("runtime", false)]
public void CanHandle_WithSource_ReturnsExpected(string source, bool expected)
{
var context = BoundaryExtractionContext.Empty with { Source = source };
var context = BoundaryExtractionContext.CreateEmpty() with { Source = source };
Assert.Equal(expected, _extractor.CanHandle(context));
}
@@ -49,7 +49,7 @@ public class K8sBoundaryExtractorTests
[Fact]
public void CanHandle_WithK8sAnnotations_ReturnsTrue()
{
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Annotations = new Dictionary<string, string>
{
@@ -64,7 +64,7 @@ public class K8sBoundaryExtractorTests
[Fact]
public void CanHandle_WithIngressAnnotation_ReturnsTrue()
{
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Annotations = new Dictionary<string, string>
{
@@ -79,7 +79,7 @@ public class K8sBoundaryExtractorTests
[Fact]
public void CanHandle_WithEmptyAnnotations_ReturnsFalse()
{
var context = BoundaryExtractionContext.Empty;
var context = BoundaryExtractionContext.CreateEmpty();
Assert.False(_extractor.CanHandle(context));
}
@@ -92,7 +92,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithInternetFacing_ReturnsPublicExposure()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
IsInternetFacing = true
@@ -111,7 +111,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithIngressClass_ReturnsInternetFacing()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -137,7 +137,7 @@ public class K8sBoundaryExtractorTests
string serviceType, string expectedLevel, bool expectedInternetFacing)
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -159,7 +159,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithExternalPorts_ReturnsInternalLevel()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
PortBindings = new Dictionary<int, string> { [443] = "https" }
@@ -177,7 +177,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithDmzZone_ReturnsInternalLevel()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
NetworkZone = "dmz"
@@ -200,7 +200,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithServicePath_ReturnsSurfaceWithPath()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -221,7 +221,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithRewriteTarget_ReturnsSurfaceWithPath()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -242,7 +242,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithNamespace_ReturnsSurfaceWithNamespacePath()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Namespace = "production"
@@ -260,7 +260,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithTlsAnnotation_ReturnsHttpsProtocol()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -281,7 +281,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithGrpcAnnotation_ReturnsGrpcProtocol()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -302,7 +302,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithPortBinding_ReturnsSurfaceWithPort()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
PortBindings = new Dictionary<int, string> { [8080] = "http" }
@@ -320,7 +320,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithIngressHost_ReturnsSurfaceWithHost()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -345,7 +345,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithBasicAuth_ReturnsBasicAuthType()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -367,7 +367,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithOAuth_ReturnsOAuth2Type()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -389,7 +389,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithMtls_ReturnsMtlsType()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -411,7 +411,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithExplicitAuthType_ReturnsSpecifiedType()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -433,7 +433,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithAuthRoles_ReturnsRolesList()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -459,7 +459,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithNoAuth_ReturnsNullAuth()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s"
};
@@ -479,7 +479,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithNetworkPolicy_ReturnsNetworkPolicyControl()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Namespace = "production",
@@ -505,7 +505,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithRateLimit_ReturnsRateLimitControl()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -529,7 +529,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithIpAllowlist_ReturnsIpAllowlistControl()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -553,7 +553,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithWaf_ReturnsWafControl()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -577,7 +577,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithMultipleControls_ReturnsAllControls()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -603,7 +603,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithNoControls_ReturnsNullControls()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s"
};
@@ -623,7 +623,7 @@ public class K8sBoundaryExtractorTests
public void Extract_BaseConfidence_Returns0Point7()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s"
};
@@ -639,7 +639,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithIngressAnnotation_IncreasesConfidence()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -659,7 +659,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WithServiceType_IncreasesConfidence()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -679,7 +679,7 @@ public class K8sBoundaryExtractorTests
public void Extract_MaxConfidence_CapsAt0Point95()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
@@ -700,7 +700,7 @@ public class K8sBoundaryExtractorTests
public void Extract_ReturnsK8sSource()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s"
};
@@ -716,7 +716,7 @@ public class K8sBoundaryExtractorTests
public void Extract_BuildsEvidenceRef_WithNamespaceAndEnvironment()
{
var root = new RichGraphRoot("root-123", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Namespace = "production",
@@ -734,7 +734,7 @@ public class K8sBoundaryExtractorTests
public void Extract_ReturnsNetworkKind()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s"
};
@@ -754,7 +754,7 @@ public class K8sBoundaryExtractorTests
public async Task ExtractAsync_ReturnsSameResultAsExtract()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
var context = BoundaryExtractionContext.CreateEmpty() with
{
Source = "k8s",
Namespace = "production",
@@ -782,7 +782,7 @@ public class K8sBoundaryExtractorTests
[Fact]
public void Extract_WithNullRoot_ThrowsArgumentNullException()
{
var context = BoundaryExtractionContext.Empty with { Source = "k8s" };
var context = BoundaryExtractionContext.CreateEmpty() with { Source = "k8s" };
Assert.Throws<ArgumentNullException>(() => _extractor.Extract(null!, null, context));
}
@@ -791,7 +791,7 @@ public class K8sBoundaryExtractorTests
public void Extract_WhenCannotHandle_ReturnsNull()
{
var root = new RichGraphRoot("root-1", "static", null);
var context = BoundaryExtractionContext.Empty with { Source = "static" };
var context = BoundaryExtractionContext.CreateEmpty() with { Source = "static" };
var result = _extractor.Extract(root, null, context);

View File

@@ -40,7 +40,7 @@ public class RichGraphBoundaryExtractorTests
Attributes: null,
SymbolDigest: null);
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.CreateEmpty());
Assert.NotNull(result);
Assert.Equal("network", result.Kind);
@@ -67,7 +67,7 @@ public class RichGraphBoundaryExtractorTests
Attributes: null,
SymbolDigest: null);
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.CreateEmpty());
Assert.NotNull(result);
Assert.NotNull(result.Surface);
@@ -92,7 +92,7 @@ public class RichGraphBoundaryExtractorTests
Attributes: null,
SymbolDigest: null);
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.CreateEmpty());
Assert.NotNull(result);
Assert.Equal("process", result.Kind);
@@ -118,7 +118,7 @@ public class RichGraphBoundaryExtractorTests
Attributes: null,
SymbolDigest: null);
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.CreateEmpty());
Assert.NotNull(result);
Assert.Equal("library", result.Kind);
@@ -292,7 +292,7 @@ public class RichGraphBoundaryExtractorTests
Attributes: null,
SymbolDigest: null);
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.CreateEmpty());
Assert.NotNull(result);
Assert.NotNull(result.Exposure);
@@ -319,11 +319,12 @@ public class RichGraphBoundaryExtractorTests
SymbolDigest: null);
// Empty context should have lower confidence
var emptyResult = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
var emptyResult = _extractor.Extract(root, rootNode, BoundaryExtractionContext.CreateEmpty());
// Rich context should have higher confidence
var richContext = new BoundaryExtractionContext
{
Timestamp = DateTimeOffset.UtcNow,
IsInternetFacing = true,
NetworkZone = "dmz",
DetectedGates = new[]
@@ -390,7 +391,7 @@ public class RichGraphBoundaryExtractorTests
[Fact]
public void CanHandle_AlwaysReturnsTrue()
{
Assert.True(_extractor.CanHandle(BoundaryExtractionContext.Empty));
Assert.True(_extractor.CanHandle(BoundaryExtractionContext.CreateEmpty()));
Assert.True(_extractor.CanHandle(BoundaryExtractionContext.ForEnvironment("test")));
}
@@ -419,7 +420,7 @@ public class RichGraphBoundaryExtractorTests
Attributes: null,
SymbolDigest: null);
var result = await _extractor.ExtractAsync(root, rootNode, BoundaryExtractionContext.Empty);
var result = await _extractor.ExtractAsync(root, rootNode, BoundaryExtractionContext.CreateEmpty());
Assert.NotNull(result);
Assert.Equal("network", result.Kind);

View File

@@ -174,6 +174,7 @@ public sealed class ClassificationChangeTrackerTests
PreviousStatus = previous,
NewStatus = next,
Cause = DriftCause.FeedDelta,
ChangedAt = DateTimeOffset.UtcNow
};
private sealed class FakeTimeProvider : TimeProvider

View File

@@ -186,7 +186,8 @@ public sealed class ScanMetricsRepositoryTests : IAsyncLifetime
SignMs = 0,
PublishMs = 0
},
ScannerVersion = "1.0.0"
ScannerVersion = "1.0.0",
CreatedAt = baseTime
};
await _repository.SaveAsync(metrics, CancellationToken.None);
}
@@ -267,7 +268,8 @@ public sealed class ScanMetricsRepositoryTests : IAsyncLifetime
FinishedAt = DateTimeOffset.UtcNow,
Phases = phases ?? ScanPhaseTimings.Empty,
ScannerVersion = "1.0.0",
IsReplay = isReplay
IsReplay = isReplay,
CreatedAt = DateTimeOffset.UtcNow
};
}
}

View File

@@ -8,10 +8,12 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Testing;
using StellaOps.Scanner.Reachability.Slices;
using StellaOps.Scanner.Storage;
using StellaOps.Scanner.Surface.Validation;
using StellaOps.Scanner.Triage;
using StellaOps.Scanner.WebService.Diagnostics;
using StellaOps.Scanner.WebService.Services;
namespace StellaOps.Scanner.WebService.Tests;
@@ -143,6 +145,7 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
configureServices?.Invoke(services);
services.RemoveAll<ISurfaceValidatorRunner>();
services.AddSingleton<ISurfaceValidatorRunner, TestSurfaceValidatorRunner>();
services.TryAddSingleton<ISliceQueryService, NullSliceQueryService>();
});
}
@@ -208,4 +211,30 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
".."));
}
}
private sealed class NullSliceQueryService : ISliceQueryService
{
public Task<SliceQueryResponse> QueryAsync(SliceQueryRequest request, CancellationToken cancellationToken = default)
=> Task.FromResult(new SliceQueryResponse
{
SliceDigest = "sha256:null",
Verdict = "unknown",
Confidence = 0.0,
CacheHit = false
});
public Task<ReachabilitySlice?> GetSliceAsync(string digest, CancellationToken cancellationToken = default)
=> Task.FromResult<ReachabilitySlice?>(null);
public Task<object?> GetSliceDsseAsync(string digest, CancellationToken cancellationToken = default)
=> Task.FromResult<object?>(null);
public Task<SliceReplayResponse> ReplayAsync(SliceReplayRequest request, CancellationToken cancellationToken = default)
=> Task.FromResult(new SliceReplayResponse
{
Match = true,
OriginalDigest = request.SliceDigest ?? "sha256:null",
RecomputedDigest = request.SliceDigest ?? "sha256:null"
});
}
}

View File

@@ -367,7 +367,8 @@ public sealed class EntryTraceExecutionServiceTests : IDisposable
Array.Empty<string>(),
new SurfaceSecretsConfiguration("inline", "tenant", null, null, null, AllowInline: true),
"tenant",
new SurfaceTlsConfiguration(null, null, null));
new SurfaceTlsConfiguration(null, null, null))
{ CreatedAtUtc = DateTimeOffset.UtcNow };
RawVariables = new Dictionary<string, string>();
}

View File

@@ -26,7 +26,8 @@ public sealed class SurfaceCacheOptionsConfiguratorTests
Array.Empty<string>(),
new SurfaceSecretsConfiguration("file", "tenant-a", "/etc/secrets", null, null, false),
"tenant-a",
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()))
{ CreatedAtUtc = DateTimeOffset.UtcNow };
var environment = new StubSurfaceEnvironment(settings);
var configurator = new SurfaceCacheOptionsConfigurator(environment);

View File

@@ -739,7 +739,8 @@ public sealed class SurfaceManifestStageExecutorTests
FeatureFlags: Array.Empty<string>(),
Secrets: new SurfaceSecretsConfiguration("none", tenant, null, null, null, false),
Tenant: tenant,
Tls: new SurfaceTlsConfiguration(null, null, null));
Tls: new SurfaceTlsConfiguration(null, null, null))
{ CreatedAtUtc = DateTimeOffset.UtcNow };
}
public SurfaceEnvironmentSettings Settings { get; }

View File

@@ -27,7 +27,8 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests
Array.Empty<string>(),
new SurfaceSecretsConfiguration("file", "tenant-a", "/etc/secrets", null, null, false),
"tenant-a",
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()))
{ CreatedAtUtc = DateTimeOffset.UtcNow };
var environment = new StubSurfaceEnvironment(settings);
var cacheOptions = Microsoft.Extensions.Options.Options.Create(new SurfaceCacheOptions { RootDirectory = cacheRoot.FullName });

View File

@@ -0,0 +1,171 @@
-- -----------------------------------------------------------------------------
-- 002_hlc_queue_chain.sql
-- Sprint: SPRINT_20260105_002_002_SCHEDULER_hlc_queue_chain
-- Tasks: SQC-002, SQC-003, SQC-004
-- Description: HLC-ordered scheduler queue with cryptographic chain linking
-- -----------------------------------------------------------------------------
-- ============================================================================
-- SQC-002: scheduler.scheduler_log - HLC-ordered, chain-linked jobs
-- ============================================================================
CREATE TABLE IF NOT EXISTS scheduler.scheduler_log (
-- Storage order (BIGSERIAL for monotonic insertion, not authoritative for ordering)
seq_bigint BIGSERIAL PRIMARY KEY,
-- Tenant isolation
tenant_id TEXT NOT NULL,
-- HLC timestamp: "1704067200000-scheduler-east-1-000042"
-- This is the authoritative ordering key
t_hlc TEXT NOT NULL,
-- Optional queue partition for parallel processing
partition_key TEXT DEFAULT '',
-- Job identifier (deterministic from payload using GUID v5)
job_id UUID NOT NULL,
-- SHA-256 of canonical JSON payload (32 bytes)
payload_hash BYTEA NOT NULL CHECK (octet_length(payload_hash) = 32),
-- Previous chain link (null for first entry in partition)
prev_link BYTEA CHECK (prev_link IS NULL OR octet_length(prev_link) = 32),
-- Current chain link: Hash(prev_link || job_id || t_hlc || payload_hash)
link BYTEA NOT NULL CHECK (octet_length(link) = 32),
-- Wall-clock timestamp for operational queries (not authoritative)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Ensure unique HLC ordering within tenant/partition
CONSTRAINT uq_scheduler_log_order UNIQUE (tenant_id, t_hlc, partition_key, job_id)
);
-- Primary query: get jobs by HLC order within tenant
CREATE INDEX IF NOT EXISTS idx_scheduler_log_tenant_hlc
ON scheduler.scheduler_log (tenant_id, t_hlc ASC);
-- Partition-specific queries
CREATE INDEX IF NOT EXISTS idx_scheduler_log_partition
ON scheduler.scheduler_log (tenant_id, partition_key, t_hlc ASC);
-- Job lookup by ID
CREATE INDEX IF NOT EXISTS idx_scheduler_log_job_id
ON scheduler.scheduler_log (job_id);
-- Chain verification: find by link hash
CREATE INDEX IF NOT EXISTS idx_scheduler_log_link
ON scheduler.scheduler_log (link);
-- Range queries for batch snapshots
CREATE INDEX IF NOT EXISTS idx_scheduler_log_created
ON scheduler.scheduler_log (tenant_id, created_at DESC);
COMMENT ON TABLE scheduler.scheduler_log IS 'HLC-ordered scheduler queue with cryptographic chain linking for audit-safe job ordering';
COMMENT ON COLUMN scheduler.scheduler_log.t_hlc IS 'Hybrid Logical Clock timestamp: authoritative ordering key. Format: physicalTime13-nodeId-counter6';
COMMENT ON COLUMN scheduler.scheduler_log.link IS 'Chain link = SHA256(prev_link || job_id || t_hlc || payload_hash). Creates tamper-evident sequence.';
-- ============================================================================
-- SQC-003: scheduler.batch_snapshot - Audit anchors for job batches
-- ============================================================================
CREATE TABLE IF NOT EXISTS scheduler.batch_snapshot (
-- Snapshot identifier
batch_id UUID PRIMARY KEY,
-- Tenant isolation
tenant_id TEXT NOT NULL,
-- HLC range covered by this snapshot
range_start_t TEXT NOT NULL,
range_end_t TEXT NOT NULL,
-- Chain head at snapshot time (last link in range)
head_link BYTEA NOT NULL CHECK (octet_length(head_link) = 32),
-- Job count for quick validation
job_count INT NOT NULL CHECK (job_count >= 0),
-- Wall-clock timestamp
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Optional DSSE signature fields
signed_by TEXT, -- Key ID that signed
signature BYTEA, -- DSSE signature bytes
-- Constraint: signature requires signed_by
CONSTRAINT chk_signature_requires_signer CHECK (
(signature IS NULL AND signed_by IS NULL) OR
(signature IS NOT NULL AND signed_by IS NOT NULL)
)
);
-- Query snapshots by tenant and time
CREATE INDEX IF NOT EXISTS idx_batch_snapshot_tenant
ON scheduler.batch_snapshot (tenant_id, created_at DESC);
-- Query snapshots by HLC range
CREATE INDEX IF NOT EXISTS idx_batch_snapshot_range
ON scheduler.batch_snapshot (tenant_id, range_start_t, range_end_t);
COMMENT ON TABLE scheduler.batch_snapshot IS 'Audit anchors for scheduler job batches. Captures chain head at specific HLC ranges.';
COMMENT ON COLUMN scheduler.batch_snapshot.head_link IS 'Chain head (last link) at snapshot time. Can be verified by replaying chain.';
-- ============================================================================
-- SQC-004: scheduler.chain_heads - Per-partition chain head tracking
-- ============================================================================
CREATE TABLE IF NOT EXISTS scheduler.chain_heads (
-- Tenant isolation
tenant_id TEXT NOT NULL,
-- Partition (empty string for default partition)
partition_key TEXT NOT NULL DEFAULT '',
-- Last chain link in this partition
last_link BYTEA NOT NULL CHECK (octet_length(last_link) = 32),
-- Last HLC timestamp in this partition
last_t_hlc TEXT NOT NULL,
-- Wall-clock timestamp of last update
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Primary key: one head per tenant/partition
PRIMARY KEY (tenant_id, partition_key)
);
-- Query chain heads by update time (for monitoring)
CREATE INDEX IF NOT EXISTS idx_chain_heads_updated
ON scheduler.chain_heads (updated_at DESC);
COMMENT ON TABLE scheduler.chain_heads IS 'Tracks current chain head for each tenant/partition. Updated atomically with scheduler_log inserts.';
COMMENT ON COLUMN scheduler.chain_heads.last_link IS 'Current chain head. Used as prev_link for next enqueue.';
-- ============================================================================
-- Atomic upsert function for chain head updates
-- ============================================================================
CREATE OR REPLACE FUNCTION scheduler.upsert_chain_head(
p_tenant_id TEXT,
p_partition_key TEXT,
p_new_link BYTEA,
p_new_t_hlc TEXT
)
RETURNS VOID
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO scheduler.chain_heads (tenant_id, partition_key, last_link, last_t_hlc, updated_at)
VALUES (p_tenant_id, p_partition_key, p_new_link, p_new_t_hlc, NOW())
ON CONFLICT (tenant_id, partition_key)
DO UPDATE SET
last_link = EXCLUDED.last_link,
last_t_hlc = EXCLUDED.last_t_hlc,
updated_at = EXCLUDED.updated_at
WHERE scheduler.chain_heads.last_t_hlc < EXCLUDED.last_t_hlc;
END;
$$;
COMMENT ON FUNCTION scheduler.upsert_chain_head IS 'Atomically updates chain head. Only updates if new HLC > current HLC (monotonicity).';

View File

@@ -0,0 +1,58 @@
// -----------------------------------------------------------------------------
// BatchSnapshotEntity.cs
// Sprint: SPRINT_20260105_002_002_SCHEDULER_hlc_queue_chain
// Task: SQC-005 - Entity for batch_snapshot table
// -----------------------------------------------------------------------------
namespace StellaOps.Scheduler.Persistence.Postgres.Models;
/// <summary>
/// Entity representing an audit anchor for a batch of scheduler jobs.
/// </summary>
public sealed record BatchSnapshotEntity
{
/// <summary>
/// Snapshot identifier.
/// </summary>
public required Guid BatchId { get; init; }
/// <summary>
/// Tenant identifier for isolation.
/// </summary>
public required string TenantId { get; init; }
/// <summary>
/// HLC range start (inclusive).
/// </summary>
public required string RangeStartT { get; init; }
/// <summary>
/// HLC range end (inclusive).
/// </summary>
public required string RangeEndT { get; init; }
/// <summary>
/// Chain head at snapshot time (last link in range).
/// </summary>
public required byte[] HeadLink { get; init; }
/// <summary>
/// Number of jobs in the snapshot range.
/// </summary>
public required int JobCount { get; init; }
/// <summary>
/// Wall-clock timestamp of snapshot creation.
/// </summary>
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Key ID that signed the snapshot (null if unsigned).
/// </summary>
public string? SignedBy { get; init; }
/// <summary>
/// DSSE signature bytes (null if unsigned).
/// </summary>
public byte[]? Signature { get; init; }
}

View File

@@ -0,0 +1,38 @@
// -----------------------------------------------------------------------------
// ChainHeadEntity.cs
// Sprint: SPRINT_20260105_002_002_SCHEDULER_hlc_queue_chain
// Task: SQC-005 - Entity for chain_heads table
// -----------------------------------------------------------------------------
namespace StellaOps.Scheduler.Persistence.Postgres.Models;
/// <summary>
/// Entity representing the current chain head for a tenant/partition.
/// </summary>
public sealed record ChainHeadEntity
{
/// <summary>
/// Tenant identifier for isolation.
/// </summary>
public required string TenantId { get; init; }
/// <summary>
/// Partition key (empty string for default partition).
/// </summary>
public string PartitionKey { get; init; } = "";
/// <summary>
/// Last chain link in this partition.
/// </summary>
public required byte[] LastLink { get; init; }
/// <summary>
/// Last HLC timestamp in this partition.
/// </summary>
public required string LastTHlc { get; init; }
/// <summary>
/// Wall-clock timestamp of last update.
/// </summary>
public required DateTimeOffset UpdatedAt { get; init; }
}

View File

@@ -0,0 +1,60 @@
// -----------------------------------------------------------------------------
// SchedulerLogEntity.cs
// Sprint: SPRINT_20260105_002_002_SCHEDULER_hlc_queue_chain
// Task: SQC-005 - Entity for scheduler_log table
// -----------------------------------------------------------------------------
namespace StellaOps.Scheduler.Persistence.Postgres.Models;
/// <summary>
/// Entity representing an HLC-ordered, chain-linked scheduler log entry.
/// </summary>
public sealed record SchedulerLogEntity
{
/// <summary>
/// Storage sequence number (BIGSERIAL, not authoritative for ordering).
/// Populated by the database on insert; 0 for new entries before persistence.
/// </summary>
public long SeqBigint { get; init; }
/// <summary>
/// Tenant identifier for isolation.
/// </summary>
public required string TenantId { get; init; }
/// <summary>
/// HLC timestamp string: "1704067200000-scheduler-east-1-000042".
/// This is the authoritative ordering key.
/// </summary>
public required string THlc { get; init; }
/// <summary>
/// Optional queue partition for parallel processing.
/// </summary>
public string PartitionKey { get; init; } = "";
/// <summary>
/// Job identifier (deterministic from payload using GUID v5).
/// </summary>
public required Guid JobId { get; init; }
/// <summary>
/// SHA-256 of canonical JSON payload (32 bytes).
/// </summary>
public required byte[] PayloadHash { get; init; }
/// <summary>
/// Previous chain link (null for first entry in partition).
/// </summary>
public byte[]? PrevLink { get; init; }
/// <summary>
/// Current chain link: Hash(prev_link || job_id || t_hlc || payload_hash).
/// </summary>
public required byte[] Link { get; init; }
/// <summary>
/// Wall-clock timestamp for operational queries (not authoritative).
/// </summary>
public required DateTimeOffset CreatedAt { get; init; }
}

View File

@@ -0,0 +1,179 @@
// -----------------------------------------------------------------------------
// BatchSnapshotRepository.cs
// Sprint: SPRINT_20260105_002_002_SCHEDULER_hlc_queue_chain
// Task: SQC-013 - Implement BatchSnapshotService
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.Scheduler.Persistence.Postgres.Models;
namespace StellaOps.Scheduler.Persistence.Postgres.Repositories;
/// <summary>
/// PostgreSQL implementation of batch snapshot repository.
/// </summary>
public sealed class BatchSnapshotRepository : RepositoryBase<SchedulerDataSource>, IBatchSnapshotRepository
{
public BatchSnapshotRepository(
SchedulerDataSource dataSource,
ILogger<BatchSnapshotRepository> logger)
: base(dataSource, logger)
{
}
/// <inheritdoc />
public async Task InsertAsync(BatchSnapshotEntity snapshot, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(snapshot);
const string sql = """
INSERT INTO scheduler.batch_snapshot (
batch_id, tenant_id, range_start_t, range_end_t,
head_link, job_count, created_at, signed_by, signature
) VALUES (
@batch_id, @tenant_id, @range_start_t, @range_end_t,
@head_link, @job_count, @created_at, @signed_by, @signature
)
""";
await using var connection = await DataSource.OpenConnectionAsync(snapshot.TenantId, "writer", cancellationToken)
.ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "batch_id", snapshot.BatchId);
AddParameter(command, "tenant_id", snapshot.TenantId);
AddParameter(command, "range_start_t", snapshot.RangeStartT);
AddParameter(command, "range_end_t", snapshot.RangeEndT);
AddParameter(command, "head_link", snapshot.HeadLink);
AddParameter(command, "job_count", snapshot.JobCount);
AddParameter(command, "created_at", snapshot.CreatedAt);
AddParameter(command, "signed_by", snapshot.SignedBy);
AddParameter(command, "signature", snapshot.Signature);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<BatchSnapshotEntity?> GetByIdAsync(Guid batchId, CancellationToken cancellationToken = default)
{
const string sql = """
SELECT batch_id, tenant_id, range_start_t, range_end_t,
head_link, job_count, created_at, signed_by, signature
FROM scheduler.batch_snapshot
WHERE batch_id = @batch_id
""";
return await QuerySingleOrDefaultAsync(
tenantId: null!,
sql,
cmd => AddParameter(cmd, "batch_id", batchId),
MapBatchSnapshot,
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IReadOnlyList<BatchSnapshotEntity>> GetByTenantAsync(
string tenantId,
int limit = 100,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
const string sql = """
SELECT batch_id, tenant_id, range_start_t, range_end_t,
head_link, job_count, created_at, signed_by, signature
FROM scheduler.batch_snapshot
WHERE tenant_id = @tenant_id
ORDER BY created_at DESC
LIMIT @limit
""";
return await QueryAsync(
tenantId,
sql,
cmd =>
{
AddParameter(cmd, "tenant_id", tenantId);
AddParameter(cmd, "limit", limit);
},
MapBatchSnapshot,
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IReadOnlyList<BatchSnapshotEntity>> GetContainingHlcAsync(
string tenantId,
string tHlc,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(tHlc);
const string sql = """
SELECT batch_id, tenant_id, range_start_t, range_end_t,
head_link, job_count, created_at, signed_by, signature
FROM scheduler.batch_snapshot
WHERE tenant_id = @tenant_id
AND range_start_t <= @t_hlc
AND range_end_t >= @t_hlc
ORDER BY created_at DESC
""";
return await QueryAsync(
tenantId,
sql,
cmd =>
{
AddParameter(cmd, "tenant_id", tenantId);
AddParameter(cmd, "t_hlc", tHlc);
},
MapBatchSnapshot,
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<BatchSnapshotEntity?> GetLatestAsync(
string tenantId,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
const string sql = """
SELECT batch_id, tenant_id, range_start_t, range_end_t,
head_link, job_count, created_at, signed_by, signature
FROM scheduler.batch_snapshot
WHERE tenant_id = @tenant_id
ORDER BY created_at DESC
LIMIT 1
""";
return await QuerySingleOrDefaultAsync(
tenantId,
sql,
cmd => AddParameter(cmd, "tenant_id", tenantId),
MapBatchSnapshot,
cancellationToken).ConfigureAwait(false);
}
private static BatchSnapshotEntity MapBatchSnapshot(NpgsqlDataReader reader)
{
return new BatchSnapshotEntity
{
BatchId = reader.GetGuid(reader.GetOrdinal("batch_id")),
TenantId = reader.GetString(reader.GetOrdinal("tenant_id")),
RangeStartT = reader.GetString(reader.GetOrdinal("range_start_t")),
RangeEndT = reader.GetString(reader.GetOrdinal("range_end_t")),
HeadLink = reader.GetFieldValue<byte[]>(reader.GetOrdinal("head_link")),
JobCount = reader.GetInt32(reader.GetOrdinal("job_count")),
CreatedAt = reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("created_at")),
SignedBy = reader.IsDBNull(reader.GetOrdinal("signed_by"))
? null
: reader.GetString(reader.GetOrdinal("signed_by")),
Signature = reader.IsDBNull(reader.GetOrdinal("signature"))
? null
: reader.GetFieldValue<byte[]>(reader.GetOrdinal("signature"))
};
}
}

View File

@@ -0,0 +1,140 @@
// -----------------------------------------------------------------------------
// ChainHeadRepository.cs
// Sprint: SPRINT_20260105_002_002_SCHEDULER_hlc_queue_chain
// Task: SQC-007 - PostgreSQL implementation for chain_heads repository
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.Scheduler.Persistence.Postgres.Models;
namespace StellaOps.Scheduler.Persistence.Postgres.Repositories;
/// <summary>
/// PostgreSQL repository for chain head tracking operations.
/// </summary>
public sealed class ChainHeadRepository : RepositoryBase<SchedulerDataSource>, IChainHeadRepository
{
/// <summary>
/// Creates a new chain head repository.
/// </summary>
public ChainHeadRepository(
SchedulerDataSource dataSource,
ILogger<ChainHeadRepository> logger)
: base(dataSource, logger)
{
}
/// <inheritdoc />
public async Task<ChainHeadEntity?> GetAsync(
string tenantId,
string partitionKey,
CancellationToken cancellationToken = default)
{
const string sql = """
SELECT tenant_id, partition_key, last_link, last_t_hlc, updated_at
FROM scheduler.chain_heads
WHERE tenant_id = @tenant_id AND partition_key = @partition_key
""";
return await QuerySingleOrDefaultAsync(
tenantId,
sql,
cmd =>
{
AddParameter(cmd, "tenant_id", tenantId);
AddParameter(cmd, "partition_key", partitionKey);
},
MapChainHeadEntity,
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<byte[]?> GetLastLinkAsync(
string tenantId,
string partitionKey,
CancellationToken cancellationToken = default)
{
const string sql = """
SELECT last_link
FROM scheduler.chain_heads
WHERE tenant_id = @tenant_id AND partition_key = @partition_key
""";
await using var connection = await DataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken)
.ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "tenant_id", tenantId);
AddParameter(command, "partition_key", partitionKey);
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
return result is DBNull or null ? null : (byte[])result;
}
/// <inheritdoc />
public async Task<bool> UpsertAsync(
string tenantId,
string partitionKey,
byte[] newLink,
string newTHlc,
CancellationToken cancellationToken = default)
{
// Use the upsert function with monotonicity check
const string sql = """
INSERT INTO scheduler.chain_heads (tenant_id, partition_key, last_link, last_t_hlc, updated_at)
VALUES (@tenant_id, @partition_key, @new_link, @new_t_hlc, NOW())
ON CONFLICT (tenant_id, partition_key)
DO UPDATE SET
last_link = EXCLUDED.last_link,
last_t_hlc = EXCLUDED.last_t_hlc,
updated_at = EXCLUDED.updated_at
WHERE scheduler.chain_heads.last_t_hlc < EXCLUDED.last_t_hlc
""";
await using var connection = await DataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken)
.ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "tenant_id", tenantId);
AddParameter(command, "partition_key", partitionKey);
AddParameter(command, "new_link", newLink);
AddParameter(command, "new_t_hlc", newTHlc);
var rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
return rowsAffected > 0;
}
/// <inheritdoc />
public async Task<IReadOnlyList<ChainHeadEntity>> GetAllForTenantAsync(
string tenantId,
CancellationToken cancellationToken = default)
{
const string sql = """
SELECT tenant_id, partition_key, last_link, last_t_hlc, updated_at
FROM scheduler.chain_heads
WHERE tenant_id = @tenant_id
ORDER BY partition_key
""";
return await QueryAsync(
tenantId,
sql,
cmd => AddParameter(cmd, "tenant_id", tenantId),
MapChainHeadEntity,
cancellationToken).ConfigureAwait(false);
}
private static ChainHeadEntity MapChainHeadEntity(NpgsqlDataReader reader)
{
return new ChainHeadEntity
{
TenantId = reader.GetString(reader.GetOrdinal("tenant_id")),
PartitionKey = reader.GetString(reader.GetOrdinal("partition_key")),
LastLink = reader.GetFieldValue<byte[]>(reader.GetOrdinal("last_link")),
LastTHlc = reader.GetString(reader.GetOrdinal("last_t_hlc")),
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("updated_at"))
};
}
}

View File

@@ -0,0 +1,50 @@
// -----------------------------------------------------------------------------
// IBatchSnapshotRepository.cs
// Sprint: SPRINT_20260105_002_002_SCHEDULER_hlc_queue_chain
// Task: SQC-013 - Implement BatchSnapshotService
// -----------------------------------------------------------------------------
using StellaOps.Scheduler.Persistence.Postgres.Models;
namespace StellaOps.Scheduler.Persistence.Postgres.Repositories;
/// <summary>
/// Repository interface for batch snapshot operations.
/// </summary>
public interface IBatchSnapshotRepository
{
/// <summary>
/// Inserts a new batch snapshot.
/// </summary>
/// <param name="snapshot">The snapshot to insert.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task InsertAsync(BatchSnapshotEntity snapshot, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a batch snapshot by ID.
/// </summary>
Task<BatchSnapshotEntity?> GetByIdAsync(Guid batchId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets batch snapshots for a tenant, ordered by creation time descending.
/// </summary>
Task<IReadOnlyList<BatchSnapshotEntity>> GetByTenantAsync(
string tenantId,
int limit = 100,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets batch snapshots that contain a specific HLC timestamp.
/// </summary>
Task<IReadOnlyList<BatchSnapshotEntity>> GetContainingHlcAsync(
string tenantId,
string tHlc,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the latest batch snapshot for a tenant.
/// </summary>
Task<BatchSnapshotEntity?> GetLatestAsync(
string tenantId,
CancellationToken cancellationToken = default);
}

Some files were not shown because too many files have changed in this diff Show More