This commit is contained in:
StellaOps Bot
2026-01-07 21:30:44 +02:00
1359 changed files with 61692 additions and 11378 deletions

View File

@@ -689,7 +689,7 @@ static Task<IResult> HandlePolicyCompile(
BundleName = request.BundleName,
Version = "1.0.0",
RuleCount = request.RuleIds.Count,
CompiledAt = now.ToString("O"),
CompiledAt = now.ToString("O", System.Globalization.CultureInfo.InvariantCulture),
ContentHash = $"sha256:{contentHash}",
SignatureId = null // Would be signed in production
};
@@ -751,10 +751,10 @@ static async Task<IResult> HandleGetConsent(
return Results.Ok(new AiConsentStatusResponse
{
Consented = record.Consented,
ConsentedAt = record.ConsentedAt?.ToString("O"),
ConsentedAt = record.ConsentedAt?.ToString("O", System.Globalization.CultureInfo.InvariantCulture),
ConsentedBy = record.UserId,
Scope = record.Scope,
ExpiresAt = record.ExpiresAt?.ToString("O"),
ExpiresAt = record.ExpiresAt?.ToString("O", System.Globalization.CultureInfo.InvariantCulture),
SessionLevel = record.SessionLevel
});
}
@@ -763,6 +763,7 @@ static async Task<IResult> HandleGrantConsent(
HttpContext httpContext,
AiConsentGrantRequest request,
IAiConsentStore consentStore,
TimeProvider timeProvider,
CancellationToken cancellationToken)
{
if (!request.DataShareAcknowledged)
@@ -786,8 +787,8 @@ static async Task<IResult> HandleGrantConsent(
return Results.Ok(new AiConsentGrantResponse
{
Consented = record.Consented,
ConsentedAt = record.ConsentedAt?.ToString("O") ?? DateTimeOffset.UtcNow.ToString("O"),
ExpiresAt = record.ExpiresAt?.ToString("O")
ConsentedAt = record.ConsentedAt?.ToString("O", System.Globalization.CultureInfo.InvariantCulture) ?? timeProvider.GetUtcNow().ToString("O", System.Globalization.CultureInfo.InvariantCulture),
ExpiresAt = record.ExpiresAt?.ToString("O", System.Globalization.CultureInfo.InvariantCulture)
});
}
@@ -863,7 +864,7 @@ static async Task<IResult> HandleJustify(
ConfidenceScore = result.ConfidenceScore,
EvidenceSuggestions = result.EvidenceSuggestions,
ModelVersion = result.ModelVersion,
GeneratedAt = result.GeneratedAt.ToString("O"),
GeneratedAt = result.GeneratedAt.ToString("O", System.Globalization.CultureInfo.InvariantCulture),
TraceId = result.TraceId
});
}
@@ -919,7 +920,7 @@ static Task<IResult> HandleGetRateLimits(
Feature = l.Feature,
Limit = l.Limit,
Remaining = l.Remaining,
ResetsAt = l.ResetsAt.ToString("O")
ResetsAt = l.ResetsAt.ToString("O", System.Globalization.CultureInfo.InvariantCulture)
}).ToList();
return Task.FromResult(Results.Ok(response));

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -16,6 +17,7 @@ public sealed class EvidenceAnchoredExplanationGenerator : IExplanationGenerator
private readonly IExplanationInferenceClient _inferenceClient;
private readonly ICitationExtractor _citationExtractor;
private readonly IExplanationStore _store;
private readonly TimeProvider _timeProvider;
private const double EvidenceBackedThreshold = 0.8;
@@ -24,13 +26,15 @@ public sealed class EvidenceAnchoredExplanationGenerator : IExplanationGenerator
IExplanationPromptService promptService,
IExplanationInferenceClient inferenceClient,
ICitationExtractor citationExtractor,
IExplanationStore store)
IExplanationStore store,
TimeProvider? timeProvider = null)
{
_evidenceService = evidenceService;
_promptService = promptService;
_inferenceClient = inferenceClient;
_citationExtractor = citationExtractor;
_store = store;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<ExplanationResult> GenerateAsync(ExplanationRequest request, CancellationToken cancellationToken = default)
@@ -91,7 +95,7 @@ public sealed class EvidenceAnchoredExplanationGenerator : IExplanationGenerator
ModelId = inferenceResult.ModelId,
PromptTemplateVersion = prompt.TemplateVersion,
InputHashes = inputHashes,
GeneratedAt = DateTime.UtcNow.ToString("O"),
GeneratedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
OutputHash = outputHash
};

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -14,6 +15,7 @@ public sealed class AiPolicyIntentParser : IPolicyIntentParser
private readonly IPolicyPromptService _promptService;
private readonly IPolicyInferenceClient _inferenceClient;
private readonly IPolicyIntentStore _intentStore;
private readonly TimeProvider _timeProvider;
private static readonly string[] FewShotExamples = new[]
{
@@ -27,11 +29,13 @@ public sealed class AiPolicyIntentParser : IPolicyIntentParser
public AiPolicyIntentParser(
IPolicyPromptService promptService,
IPolicyInferenceClient inferenceClient,
IPolicyIntentStore intentStore)
IPolicyIntentStore intentStore,
TimeProvider? timeProvider = null)
{
_promptService = promptService;
_inferenceClient = inferenceClient;
_intentStore = intentStore;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<PolicyParseResult> ParseAsync(
@@ -61,7 +65,7 @@ public sealed class AiPolicyIntentParser : IPolicyIntentParser
Success = intent.Confidence >= 0.7,
ErrorMessage = intent.Confidence < 0.7 ? "Ambiguous input - clarification needed" : null,
ModelId = inferenceResult.ModelId,
ParsedAt = DateTime.UtcNow.ToString("O")
ParsedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
};
}
@@ -93,7 +97,7 @@ public sealed class AiPolicyIntentParser : IPolicyIntentParser
Intent = clarifiedIntent,
Success = clarifiedIntent.Confidence >= 0.8,
ModelId = inferenceResult.ModelId,
ParsedAt = DateTime.UtcNow.ToString("O")
ParsedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
};
}

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
@@ -10,6 +11,13 @@ namespace StellaOps.AdvisoryAI.PolicyStudio;
/// </summary>
public sealed class LatticeRuleGenerator : IPolicyRuleGenerator
{
private readonly TimeProvider _timeProvider;
public LatticeRuleGenerator(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<RuleGenerationResult> GenerateAsync(
PolicyIntent intent,
CancellationToken cancellationToken = default)
@@ -58,7 +66,7 @@ public sealed class LatticeRuleGenerator : IPolicyRuleGenerator
Success = true,
Warnings = warnings,
IntentId = intent.IntentId,
GeneratedAt = DateTime.UtcNow.ToString("O")
GeneratedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
});
}

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -391,7 +392,7 @@ public sealed class PolicyBundleCompiler : IPolicyBundleCompiler
Warnings = warnings,
ValidationReport = validationReport,
TestReport = testReport,
CompiledAt = _timeProvider.GetUtcNow().ToString("O")
CompiledAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
};
}
@@ -412,7 +413,7 @@ public sealed class PolicyBundleCompiler : IPolicyBundleCompiler
Warnings = warnings,
ValidationReport = validationReport,
TestReport = testReport,
CompiledAt = DateTime.UtcNow.ToString("O"),
CompiledAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
BundleDigest = bundleDigest
};
}
@@ -492,7 +493,7 @@ public sealed class PolicyBundleCompiler : IPolicyBundleCompiler
ContentDigest = contentDigest,
Signature = string.Empty,
Algorithm = "none",
SignedAt = _timeProvider.GetUtcNow().ToString("O")
SignedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
};
}
@@ -509,7 +510,7 @@ public sealed class PolicyBundleCompiler : IPolicyBundleCompiler
Algorithm = signature.Algorithm,
KeyId = options.KeyId,
SignerIdentity = options.SignerIdentity,
SignedAt = _timeProvider.GetUtcNow().ToString("O"),
SignedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
CertificateChain = signature.CertificateChain
};
}

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
@@ -10,6 +11,13 @@ namespace StellaOps.AdvisoryAI.PolicyStudio;
/// </summary>
public sealed class PropertyBasedTestSynthesizer : ITestCaseSynthesizer
{
private readonly TimeProvider _timeProvider;
public PropertyBasedTestSynthesizer(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<IReadOnlyList<PolicyTestCase>> SynthesizeAsync(
IReadOnlyList<LatticeRule> rules,
CancellationToken cancellationToken = default)
@@ -53,7 +61,7 @@ public sealed class PropertyBasedTestSynthesizer : ITestCaseSynthesizer
Passed = results.Count(r => r.Passed),
Failed = results.Count(r => !r.Passed),
Results = results,
RunAt = DateTime.UtcNow.ToString("O")
RunAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
});
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Globalization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.AdvisoryAI.Abstractions;
@@ -106,7 +107,7 @@ internal sealed class ConcelierAdvisoryDocumentProvider : IAdvisoryDocumentProvi
["vendor"] = record.Document.Source.Vendor,
["connector"] = record.Document.Source.Connector,
["content_hash"] = record.Document.Upstream.ContentHash,
["ingested_at"] = record.IngestedAt.UtcDateTime.ToString("O"),
["ingested_at"] = record.IngestedAt.UtcDateTime.ToString("O", CultureInfo.InvariantCulture),
};
if (!string.IsNullOrWhiteSpace(record.Document.Source.Stream))

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -88,7 +89,7 @@ public sealed class AiRemediationPlanner : IRemediationPlanner
NotReadyReason = notReadyReason,
ConfidenceScore = inferenceResult.Confidence,
ModelId = inferenceResult.ModelId,
GeneratedAt = _timeProvider.GetUtcNow().ToString("O"),
GeneratedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
InputHashes = inputHashes,
EvidenceRefs = new List<string> { versionResult.CurrentVersion, versionResult.RecommendedVersion }
};

View File

@@ -1,3 +1,5 @@
using System.Globalization;
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
@@ -7,42 +9,56 @@ namespace StellaOps.AdvisoryAI.Remediation;
/// </summary>
public sealed class AzureDevOpsPullRequestGenerator : IPullRequestGenerator
{
private readonly TimeProvider _timeProvider;
private readonly Func<Guid> _guidFactory;
private readonly Func<int, int, int> _randomFactory;
public AzureDevOpsPullRequestGenerator(
TimeProvider? timeProvider = null,
Func<Guid>? guidFactory = null,
Func<int, int, int>? randomFactory = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_guidFactory = guidFactory ?? Guid.NewGuid;
_randomFactory = randomFactory ?? Random.Shared.Next;
}
public string ScmType => "azure-devops";
public Task<PullRequestResult> CreatePullRequestAsync(
RemediationPlan plan,
CancellationToken cancellationToken = default)
{
var nowStr = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
if (!plan.PrReady)
{
return Task.FromResult(new PullRequestResult
{
PrId = $"ado-pr-{Guid.NewGuid():N}",
PrId = $"ado-pr-{_guidFactory():N}",
PrNumber = 0,
Url = string.Empty,
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 = nowStr,
UpdatedAt = nowStr
});
}
var branchName = GenerateBranchName(plan);
var prId = $"ado-pr-{Guid.NewGuid():N}";
var now = DateTime.UtcNow.ToString("O");
var branchName = GenerateBranchName(plan, _timeProvider);
var prId = $"ado-pr-{_guidFactory():N}";
// In a real implementation, this would use Azure DevOps REST API
return Task.FromResult(new PullRequestResult
{
PrId = prId,
PrNumber = new Random().Next(1000, 9999),
PrNumber = _randomFactory(1000, 9999),
Url = $"https://dev.azure.com/{ExtractOrgProject(plan.Request.RepositoryUrl)}/_git/{ExtractRepoName(plan.Request.RepositoryUrl)}/pullrequest/{prId}",
BranchName = branchName,
Status = PullRequestStatus.Creating,
StatusMessage = "Pull request is being created",
CreatedAt = now,
UpdatedAt = now
CreatedAt = nowStr,
UpdatedAt = nowStr
});
}
@@ -50,7 +66,7 @@ public sealed class AzureDevOpsPullRequestGenerator : IPullRequestGenerator
string prId,
CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow.ToString("O");
var now = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
return Task.FromResult(new PullRequestResult
{
PrId = prId,
@@ -80,10 +96,10 @@ public sealed class AzureDevOpsPullRequestGenerator : IPullRequestGenerator
return Task.CompletedTask;
}
private static string GenerateBranchName(RemediationPlan plan)
private static string GenerateBranchName(RemediationPlan plan, TimeProvider timeProvider)
{
var vulnId = plan.Request.VulnerabilityId.Replace(":", "-").ToLowerInvariant();
var timestamp = DateTime.UtcNow.ToString("yyyyMMdd");
var timestamp = timeProvider.GetUtcNow().ToString("yyyyMMdd", CultureInfo.InvariantCulture);
return $"stellaops/fix-{vulnId}-{timestamp}";
}

View File

@@ -1,3 +1,5 @@
using System.Globalization;
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
@@ -9,11 +11,19 @@ public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
{
private readonly IRemediationPlanStore _planStore;
private readonly TimeProvider _timeProvider;
private readonly Func<Guid> _guidFactory;
private readonly Func<int, int, int> _randomFactory;
public GitHubPullRequestGenerator(IRemediationPlanStore planStore, TimeProvider? timeProvider = null)
public GitHubPullRequestGenerator(
IRemediationPlanStore planStore,
TimeProvider? timeProvider = null,
Func<Guid>? guidFactory = null,
Func<int, int, int>? randomFactory = null)
{
_planStore = planStore;
_timeProvider = timeProvider ?? TimeProvider.System;
_guidFactory = guidFactory ?? Guid.NewGuid;
_randomFactory = randomFactory ?? Random.Shared.Next;
}
public string ScmType => "github";
@@ -22,19 +32,20 @@ public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
RemediationPlan plan,
CancellationToken cancellationToken = default)
{
var nowStr = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
// Validate plan is PR-ready
if (!plan.PrReady)
{
return new PullRequestResult
{
PrId = $"pr-{Guid.NewGuid():N}",
PrId = $"pr-{_guidFactory():N}",
PrNumber = 0,
Url = string.Empty,
BranchName = string.Empty,
Status = PullRequestStatus.Failed,
StatusMessage = plan.NotReadyReason ?? "Plan is not PR-ready",
CreatedAt = _timeProvider.GetUtcNow().ToString("O"),
UpdatedAt = _timeProvider.GetUtcNow().ToString("O")
CreatedAt = nowStr,
UpdatedAt = nowStr
};
}
@@ -47,19 +58,18 @@ public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
// 3. Commit changes
// 4. Create PR via GitHub API
var prId = $"gh-pr-{Guid.NewGuid():N}";
var now = _timeProvider.GetUtcNow().ToString("O");
var prId = $"gh-pr-{_guidFactory():N}";
return new PullRequestResult
{
PrId = prId,
PrNumber = new Random().Next(1000, 9999), // Placeholder
PrNumber = _randomFactory(1000, 9999), // Placeholder
Url = $"https://github.com/{ExtractOwnerRepo(plan.Request.RepositoryUrl)}/pull/{prId}",
BranchName = branchName,
Status = PullRequestStatus.Creating,
StatusMessage = "Pull request is being created",
CreatedAt = now,
UpdatedAt = now
CreatedAt = nowStr,
UpdatedAt = nowStr
};
}
@@ -68,7 +78,7 @@ public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
CancellationToken cancellationToken = default)
{
// In a real implementation, this would query GitHub API
var now = _timeProvider.GetUtcNow().ToString("O");
var now = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
return Task.FromResult(new PullRequestResult
{
@@ -104,7 +114,7 @@ public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
private string GenerateBranchName(RemediationPlan plan)
{
var vulnId = plan.Request.VulnerabilityId.Replace(":", "-").ToLowerInvariant();
var timestamp = _timeProvider.GetUtcNow().ToString("yyyyMMdd");
var timestamp = _timeProvider.GetUtcNow().ToString("yyyyMMdd", CultureInfo.InvariantCulture);
return $"stellaops/fix-{vulnId}-{timestamp}";
}

View File

@@ -1,3 +1,5 @@
using System.Globalization;
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
@@ -8,10 +10,17 @@ namespace StellaOps.AdvisoryAI.Remediation;
public sealed class GitLabMergeRequestGenerator : IPullRequestGenerator
{
private readonly TimeProvider _timeProvider;
private readonly Func<Guid> _guidFactory;
private readonly Func<int, int, int> _randomFactory;
public GitLabMergeRequestGenerator(TimeProvider? timeProvider = null)
public GitLabMergeRequestGenerator(
TimeProvider? timeProvider = null,
Func<Guid>? guidFactory = null,
Func<int, int, int>? randomFactory = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_guidFactory = guidFactory ?? Guid.NewGuid;
_randomFactory = randomFactory ?? Random.Shared.Next;
}
public string ScmType => "gitlab";
@@ -20,36 +29,36 @@ public sealed class GitLabMergeRequestGenerator : IPullRequestGenerator
RemediationPlan plan,
CancellationToken cancellationToken = default)
{
var nowStr = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
if (!plan.PrReady)
{
return Task.FromResult(new PullRequestResult
{
PrId = $"mr-{Guid.NewGuid():N}",
PrId = $"mr-{_guidFactory():N}",
PrNumber = 0,
Url = string.Empty,
BranchName = string.Empty,
Status = PullRequestStatus.Failed,
StatusMessage = plan.NotReadyReason ?? "Plan is not MR-ready",
CreatedAt = _timeProvider.GetUtcNow().ToString("O"),
UpdatedAt = _timeProvider.GetUtcNow().ToString("O")
CreatedAt = nowStr,
UpdatedAt = nowStr
});
}
var branchName = GenerateBranchName(plan);
var mrId = $"gl-mr-{Guid.NewGuid():N}";
var now = _timeProvider.GetUtcNow().ToString("O");
var mrId = $"gl-mr-{_guidFactory():N}";
// In a real implementation, this would use GitLab API
return Task.FromResult(new PullRequestResult
{
PrId = mrId,
PrNumber = new Random().Next(1000, 9999),
PrNumber = _randomFactory(1000, 9999),
Url = $"https://gitlab.com/{ExtractProjectPath(plan.Request.RepositoryUrl)}/-/merge_requests/{mrId}",
BranchName = branchName,
Status = PullRequestStatus.Creating,
StatusMessage = "Merge request is being created",
CreatedAt = now,
UpdatedAt = now
CreatedAt = nowStr,
UpdatedAt = nowStr
});
}
@@ -57,7 +66,7 @@ public sealed class GitLabMergeRequestGenerator : IPullRequestGenerator
string prId,
CancellationToken cancellationToken = default)
{
var now = _timeProvider.GetUtcNow().ToString("O");
var now = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
return Task.FromResult(new PullRequestResult
{
PrId = prId,
@@ -90,7 +99,7 @@ public sealed class GitLabMergeRequestGenerator : IPullRequestGenerator
private string GenerateBranchName(RemediationPlan plan)
{
var vulnId = plan.Request.VulnerabilityId.Replace(":", "-").ToLowerInvariant();
var timestamp = _timeProvider.GetUtcNow().ToString("yyyyMMdd");
var timestamp = _timeProvider.GetUtcNow().ToString("yyyyMMdd", CultureInfo.InvariantCulture);
return $"stellaops/fix-{vulnId}-{timestamp}";
}

View File

@@ -30,7 +30,7 @@ public sealed class SemanticVersionTests
[InlineData("1.0.0-")]
[InlineData("")]
[InlineData(null)]
public void Parse_InvalidInputs_Throws(string value)
public void Parse_InvalidInputs_Throws(string? value)
{
var act = () => SemanticVersion.Parse(value!);
act.Should().Throw<FormatException>();

View File

@@ -12,12 +12,6 @@
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />

View File

@@ -4,6 +4,7 @@
// Part of Step 5: Graph Emission
// =============================================================================
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -27,7 +28,7 @@ public sealed class EvidenceGraph
/// Generation timestamp in ISO 8601 UTC format.
/// </summary>
[JsonPropertyName("generatedAt")]
public string GeneratedAt { get; init; } = DateTimeOffset.UnixEpoch.ToString("O");
public string GeneratedAt { get; init; } = DateTimeOffset.UnixEpoch.ToString("O", CultureInfo.InvariantCulture);
/// <summary>
/// Generator tool identifier.

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using StellaOps.AirGap.Importer.Contracts;
using StellaOps.AirGap.Importer.Reconciliation.Parsers;
using StellaOps.AirGap.Importer.Reconciliation.Signing;
@@ -229,7 +230,7 @@ public sealed class EvidenceReconciler : IEvidenceReconciler
return new EvidenceGraph
{
GeneratedAt = generatedAtUtc.ToString("O"),
GeneratedAt = generatedAtUtc.ToString("O", CultureInfo.InvariantCulture),
Nodes = nodes,
Edges = edges,
Metadata = new EvidenceGraphMetadata

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.AirGap.Importer.Contracts;
@@ -274,7 +275,7 @@ public sealed class ImportValidator
["bundleType"] = request.BundleType,
["bundleDigest"] = request.BundleDigest,
["manifestVersion"] = request.ManifestVersion,
["manifestCreatedAt"] = request.ManifestCreatedAt.ToString("O"),
["manifestCreatedAt"] = request.ManifestCreatedAt.ToString("O", CultureInfo.InvariantCulture),
["forceActivate"] = request.ForceActivate.ToString()
};

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -20,7 +21,7 @@ public sealed record TimeStatusDto(
public static TimeStatusDto FromStatus(TimeStatus status)
{
return new TimeStatusDto(
status.Anchor.AnchorTime.ToUniversalTime().ToString("O"),
status.Anchor.AnchorTime.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture),
status.Anchor.Format,
status.Anchor.Source,
status.Anchor.SignatureFingerprint,
@@ -31,7 +32,7 @@ public sealed record TimeStatusDto(
status.Staleness.IsWarning,
status.Staleness.IsBreach,
status.ContentStaleness,
status.EvaluatedAtUtc.ToUniversalTime().ToString("O"));
status.EvaluatedAtUtc.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture));
}
public string ToJson()

View File

@@ -6,6 +6,7 @@
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -93,7 +94,7 @@ public sealed class ExcititorVexImportTarget : IVexImportTarget
Content: contentBytes,
Metadata: ImmutableDictionary<string, string>.Empty
.Add("importSource", "airgap-snapshot")
.Add("snapshotAt", data.SnapshotAt.ToString("O")));
.Add("snapshotAt", data.SnapshotAt.ToString("O", CultureInfo.InvariantCulture)));
await _sink.StoreAsync(document, cancellationToken);
created++;

View File

@@ -59,6 +59,9 @@ public static class AirGapSyncServiceCollectionExtensions
// Bundle exporter
services.TryAddSingleton<IAirGapBundleExporter, AirGapBundleExporter>();
// Bundle DSSE signer (OMP-010)
services.TryAddSingleton<IAirGapBundleDsseSigner, AirGapBundleDsseSigner>();
return services;
}

View File

@@ -0,0 +1,275 @@
// <copyright file="AirGapBundleDsseSigner.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// </copyright>
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.AirGap.Sync.Models;
using StellaOps.Canonical.Json;
namespace StellaOps.AirGap.Sync.Services;
/// <summary>
/// Options for air-gap bundle DSSE signing.
/// </summary>
public sealed class AirGapBundleDsseOptions
{
/// <summary>
/// Configuration section name.
/// </summary>
public const string SectionName = "AirGap:BundleSigning";
/// <summary>
/// Gets or sets the signing mode: "hmac" for HMAC-SHA256, "none" to disable.
/// </summary>
public string Mode { get; set; } = "none";
/// <summary>
/// Gets or sets the HMAC secret key as Base64.
/// Required when Mode is "hmac".
/// </summary>
public string? SecretBase64 { get; set; }
/// <summary>
/// Gets or sets the key identifier for the signature.
/// </summary>
public string KeyId { get; set; } = "airgap-bundle-signer";
/// <summary>
/// Gets or sets the payload type for DSSE envelope.
/// </summary>
public string PayloadType { get; set; } = "application/vnd.stellaops.airgap.bundle+json";
}
/// <summary>
/// Result of a bundle signature operation.
/// </summary>
/// <param name="KeyId">The key ID used for signing.</param>
/// <param name="Signature">The signature bytes.</param>
/// <param name="SignatureBase64">The signature as Base64 string.</param>
public sealed record AirGapBundleSignatureResult(
string KeyId,
byte[] Signature,
string SignatureBase64);
/// <summary>
/// Interface for air-gap bundle DSSE signing.
/// </summary>
public interface IAirGapBundleDsseSigner
{
/// <summary>
/// Signs an air-gap bundle manifest and returns the signature result.
/// </summary>
/// <param name="bundle">The bundle to sign.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Signature result with key ID and signature.</returns>
Task<AirGapBundleSignatureResult?> SignAsync(
AirGapBundle bundle,
CancellationToken cancellationToken = default);
/// <summary>
/// Verifies an air-gap bundle signature.
/// </summary>
/// <param name="bundle">The bundle to verify.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if signature is valid or signing is disabled; false if invalid.</returns>
Task<AirGapBundleVerificationResult> VerifyAsync(
AirGapBundle bundle,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets whether signing is enabled.
/// </summary>
bool IsEnabled { get; }
}
/// <summary>
/// Result of bundle signature verification.
/// </summary>
/// <param name="IsValid">Whether the signature is valid.</param>
/// <param name="Reason">The reason for the result.</param>
public sealed record AirGapBundleVerificationResult(bool IsValid, string Reason)
{
/// <summary>
/// Verification succeeded.
/// </summary>
public static AirGapBundleVerificationResult Valid { get; } = new(true, "Signature verified");
/// <summary>
/// Signing is disabled, so verification is skipped.
/// </summary>
public static AirGapBundleVerificationResult SigningDisabled { get; } = new(true, "Signing disabled");
/// <summary>
/// Bundle has no signature but signing is enabled.
/// </summary>
public static AirGapBundleVerificationResult MissingSignature { get; } = new(false, "Bundle is not signed");
/// <summary>
/// Signature verification failed.
/// </summary>
public static AirGapBundleVerificationResult InvalidSignature { get; } = new(false, "Signature verification failed");
}
/// <summary>
/// DSSE signer for air-gap bundles using HMAC-SHA256.
/// </summary>
public sealed class AirGapBundleDsseSigner : IAirGapBundleDsseSigner
{
private const string DssePrefix = "DSSEv1 ";
private readonly IOptions<AirGapBundleDsseOptions> _options;
private readonly ILogger<AirGapBundleDsseSigner> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="AirGapBundleDsseSigner"/> class.
/// </summary>
public AirGapBundleDsseSigner(
IOptions<AirGapBundleDsseOptions> options,
ILogger<AirGapBundleDsseSigner> logger)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc/>
public bool IsEnabled => string.Equals(_options.Value.Mode, "hmac", StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public Task<AirGapBundleSignatureResult?> SignAsync(
AirGapBundle bundle,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(bundle);
cancellationToken.ThrowIfCancellationRequested();
var opts = _options.Value;
if (!IsEnabled)
{
_logger.LogDebug("Air-gap bundle DSSE signing is disabled");
return Task.FromResult<AirGapBundleSignatureResult?>(null);
}
if (string.IsNullOrWhiteSpace(opts.SecretBase64))
{
throw new InvalidOperationException("HMAC signing mode requires SecretBase64 to be configured");
}
byte[] secret;
try
{
secret = Convert.FromBase64String(opts.SecretBase64);
}
catch (FormatException ex)
{
throw new InvalidOperationException("SecretBase64 is not valid Base64", ex);
}
// Compute PAE (Pre-Authentication Encoding) per DSSE spec
var pae = ComputePreAuthenticationEncoding(opts.PayloadType, bundle.ManifestDigest);
var signature = ComputeHmacSha256(secret, pae);
var signatureBase64 = Convert.ToBase64String(signature);
_logger.LogInformation(
"Signed air-gap bundle {BundleId} with key {KeyId}",
bundle.BundleId,
opts.KeyId);
return Task.FromResult<AirGapBundleSignatureResult?>(
new AirGapBundleSignatureResult(opts.KeyId, signature, signatureBase64));
}
/// <inheritdoc/>
public Task<AirGapBundleVerificationResult> VerifyAsync(
AirGapBundle bundle,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(bundle);
cancellationToken.ThrowIfCancellationRequested();
var opts = _options.Value;
if (!IsEnabled)
{
_logger.LogDebug("Air-gap bundle DSSE signing is disabled, skipping verification");
return Task.FromResult(AirGapBundleVerificationResult.SigningDisabled);
}
if (string.IsNullOrWhiteSpace(bundle.Signature))
{
_logger.LogWarning("Air-gap bundle {BundleId} has no signature", bundle.BundleId);
return Task.FromResult(AirGapBundleVerificationResult.MissingSignature);
}
if (string.IsNullOrWhiteSpace(opts.SecretBase64))
{
throw new InvalidOperationException("HMAC signing mode requires SecretBase64 to be configured");
}
byte[] secret;
try
{
secret = Convert.FromBase64String(opts.SecretBase64);
}
catch (FormatException ex)
{
throw new InvalidOperationException("SecretBase64 is not valid Base64", ex);
}
byte[] expectedSignature;
try
{
expectedSignature = Convert.FromBase64String(bundle.Signature);
}
catch (FormatException)
{
_logger.LogWarning("Air-gap bundle {BundleId} has invalid Base64 signature", bundle.BundleId);
return Task.FromResult(AirGapBundleVerificationResult.InvalidSignature);
}
// Compute PAE and expected signature
var pae = ComputePreAuthenticationEncoding(opts.PayloadType, bundle.ManifestDigest);
var computedSignature = ComputeHmacSha256(secret, pae);
if (!CryptographicOperations.FixedTimeEquals(expectedSignature, computedSignature))
{
_logger.LogWarning(
"Air-gap bundle {BundleId} signature verification failed",
bundle.BundleId);
return Task.FromResult(AirGapBundleVerificationResult.InvalidSignature);
}
_logger.LogDebug(
"Air-gap bundle {BundleId} signature verified successfully",
bundle.BundleId);
return Task.FromResult(AirGapBundleVerificationResult.Valid);
}
/// <summary>
/// Computes DSSE Pre-Authentication Encoding (PAE).
/// PAE = "DSSEv1" SP len(payloadType) SP payloadType SP len(payload) SP payload
/// where len() returns ASCII decimal length, and SP is a space character.
/// </summary>
private static byte[] ComputePreAuthenticationEncoding(string payloadType, string manifestDigest)
{
var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadType);
var manifestDigestBytes = Encoding.UTF8.GetBytes(manifestDigest);
// Format: "DSSEv1 {payloadType.Length} {payloadType} {payload.Length} {payload}"
var paeString = string.Create(
CultureInfo.InvariantCulture,
$"{DssePrefix}{payloadTypeBytes.Length} {payloadType} {manifestDigestBytes.Length} {manifestDigest}");
return Encoding.UTF8.GetBytes(paeString);
}
private static byte[] ComputeHmacSha256(byte[] key, byte[] data)
{
using var hmac = new HMACSHA256(key);
return hmac.ComputeHash(data);
}
}

View File

@@ -11,10 +11,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.runner.visualstudio" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@@ -0,0 +1,242 @@
// <copyright file="AirGapBundleDsseSignerTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// </copyright>
using System.Security.Cryptography;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.AirGap.Sync.Models;
using StellaOps.AirGap.Sync.Services;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.AirGap.Sync.Tests;
/// <summary>
/// Unit tests for <see cref="AirGapBundleDsseSigner"/>.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class AirGapBundleDsseSignerTests
{
private static readonly string TestSecretBase64 = Convert.ToBase64String(
RandomNumberGenerator.GetBytes(32));
[Fact]
public async Task SignAsync_WhenDisabled_ReturnsNull()
{
// Arrange
var options = Options.Create(new AirGapBundleDsseOptions { Mode = "none" });
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
var bundle = CreateTestBundle();
// Act
var result = await signer.SignAsync(bundle);
// Assert
result.Should().BeNull();
signer.IsEnabled.Should().BeFalse();
}
[Fact]
public async Task SignAsync_WhenEnabled_ReturnsValidSignature()
{
// Arrange
var options = Options.Create(new AirGapBundleDsseOptions
{
Mode = "hmac",
SecretBase64 = TestSecretBase64,
KeyId = "test-key"
});
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
var bundle = CreateTestBundle();
// Act
var result = await signer.SignAsync(bundle);
// Assert
result.Should().NotBeNull();
result!.KeyId.Should().Be("test-key");
result.Signature.Should().NotBeEmpty();
result.SignatureBase64.Should().NotBeNullOrWhiteSpace();
signer.IsEnabled.Should().BeTrue();
}
[Fact]
public async Task SignAsync_DeterministicForSameInput()
{
// Arrange
var options = Options.Create(new AirGapBundleDsseOptions
{
Mode = "hmac",
SecretBase64 = TestSecretBase64
});
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
var bundle = CreateTestBundle();
// Act
var result1 = await signer.SignAsync(bundle);
var result2 = await signer.SignAsync(bundle);
// Assert
result1!.SignatureBase64.Should().Be(result2!.SignatureBase64);
}
[Fact]
public async Task SignAsync_DifferentForDifferentManifest()
{
// Arrange
var options = Options.Create(new AirGapBundleDsseOptions
{
Mode = "hmac",
SecretBase64 = TestSecretBase64
});
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
var bundle1 = CreateTestBundle(manifestDigest: "sha256:aaa");
var bundle2 = CreateTestBundle(manifestDigest: "sha256:bbb");
// Act
var result1 = await signer.SignAsync(bundle1);
var result2 = await signer.SignAsync(bundle2);
// Assert
result1!.SignatureBase64.Should().NotBe(result2!.SignatureBase64);
}
[Fact]
public async Task VerifyAsync_WhenDisabled_ReturnsSigningDisabled()
{
// Arrange
var options = Options.Create(new AirGapBundleDsseOptions { Mode = "none" });
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
var bundle = CreateTestBundle();
// Act
var result = await signer.VerifyAsync(bundle);
// Assert
result.Should().Be(AirGapBundleVerificationResult.SigningDisabled);
}
[Fact]
public async Task VerifyAsync_WhenNoSignature_ReturnsMissingSignature()
{
// Arrange
var options = Options.Create(new AirGapBundleDsseOptions
{
Mode = "hmac",
SecretBase64 = TestSecretBase64
});
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
var bundle = CreateTestBundle(signature: null);
// Act
var result = await signer.VerifyAsync(bundle);
// Assert
result.Should().Be(AirGapBundleVerificationResult.MissingSignature);
}
[Fact]
public async Task VerifyAsync_WithValidSignature_ReturnsValid()
{
// Arrange
var options = Options.Create(new AirGapBundleDsseOptions
{
Mode = "hmac",
SecretBase64 = TestSecretBase64
});
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
var bundle = CreateTestBundle();
// Sign the bundle first
var signResult = await signer.SignAsync(bundle);
var signedBundle = bundle with { Signature = signResult!.SignatureBase64, SignedBy = signResult.KeyId };
// Act
var verifyResult = await signer.VerifyAsync(signedBundle);
// Assert
verifyResult.Should().Be(AirGapBundleVerificationResult.Valid);
}
[Fact]
public async Task VerifyAsync_WithTamperedSignature_ReturnsInvalid()
{
// Arrange
var options = Options.Create(new AirGapBundleDsseOptions
{
Mode = "hmac",
SecretBase64 = TestSecretBase64
});
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
var bundle = CreateTestBundle();
// Sign and then tamper
var signResult = await signer.SignAsync(bundle);
var tamperedBundle = bundle with
{
Signature = signResult!.SignatureBase64,
ManifestDigest = "sha256:tampered"
};
// Act
var verifyResult = await signer.VerifyAsync(tamperedBundle);
// Assert
verifyResult.Should().Be(AirGapBundleVerificationResult.InvalidSignature);
}
[Fact]
public async Task VerifyAsync_WithInvalidBase64Signature_ReturnsInvalid()
{
// Arrange
var options = Options.Create(new AirGapBundleDsseOptions
{
Mode = "hmac",
SecretBase64 = TestSecretBase64
});
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
var bundle = CreateTestBundle(signature: "not-valid-base64!!!");
// Act
var verifyResult = await signer.VerifyAsync(bundle);
// Assert
verifyResult.Should().Be(AirGapBundleVerificationResult.InvalidSignature);
}
[Fact]
public void SignAsync_WithMissingSecret_ThrowsInvalidOperation()
{
// Arrange
var options = Options.Create(new AirGapBundleDsseOptions
{
Mode = "hmac",
SecretBase64 = null
});
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
var bundle = CreateTestBundle();
// Act & Assert
var act = async () => await signer.SignAsync(bundle);
act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*SecretBase64*");
}
private static AirGapBundle CreateTestBundle(
string? manifestDigest = null,
string? signature = null)
{
return new AirGapBundle
{
BundleId = Guid.Parse("11111111-1111-1111-1111-111111111111"),
TenantId = "test-tenant",
CreatedAt = DateTimeOffset.Parse("2026-01-07T12:00:00Z"),
CreatedByNodeId = "test-node",
JobLogs = new List<NodeJobLog>(),
ManifestDigest = manifestDigest ?? "sha256:abc123def456",
Signature = signature
};
}
}

View File

@@ -11,10 +11,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@@ -95,6 +95,7 @@ public class Rfc3161VerifierTests
Assert.False(result.IsValid);
// Should report either decode or error
Assert.True(result.Reason?.Contains("rfc3161-") ?? false);
Assert.NotNull(result.Reason);
Assert.Contains("rfc3161-", result.Reason);
}
}

View File

@@ -11,10 +11,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="xunit.runner.visualstudio" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@@ -13,9 +13,6 @@
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.v3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />

View File

@@ -21,9 +21,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.v3" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0051-M | DONE | Maintainability audit for StellaOps.Attestor.Envelope. |
| AUDIT-0051-T | DONE | Test coverage audit for StellaOps.Attestor.Envelope. |
| AUDIT-0051-A | DONE | Applied audit remediation for envelope signing/serialization. |
| AUDIT-0051-M | DONE | Revalidated maintainability for StellaOps.Attestor.Envelope. |
| AUDIT-0051-T | DONE | Revalidated test coverage for StellaOps.Attestor.Envelope. |
| AUDIT-0051-A | DONE | Revalidated; no new issues. |

View File

@@ -12,10 +12,6 @@
<ItemGroup>
<PackageReference Include="FsCheck.Xunit.v3" />
<PackageReference Include="FsCheck" />
<PackageReference Include="xunit.runner.visualstudio" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@@ -5,7 +5,7 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0052-M | DONE | Maintainability audit for StellaOps.Attestor.Envelope.Tests. |
| AUDIT-0052-T | DONE | Test coverage audit for StellaOps.Attestor.Envelope.Tests. |
| AUDIT-0052-A | TODO | Pending approval for changes. |
| AUDIT-0052-M | DONE | Revalidated maintainability for StellaOps.Attestor.Envelope.Tests. |
| AUDIT-0052-T | DONE | Revalidated test coverage for StellaOps.Attestor.Envelope.Tests. |
| AUDIT-0052-A | DONE | Waived (test project; revalidated 2026-01-06). |
| VAL-SMOKE-001 | DONE | Stabilized DSSE signature tests under xUnit v3. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0069-M | DONE | Maintainability audit for StellaOps.Attestor.Types.Generator. |
| AUDIT-0069-T | DONE | Test coverage audit for StellaOps.Attestor.Types.Generator. |
| AUDIT-0069-A | DONE | Applied repo-root override, schema id fix, canonicalization, strict validation, prune, and tests. |
| AUDIT-0069-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
| AUDIT-0069-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
| AUDIT-0069-A | TODO | Reopened after revalidation 2026-01-06. |

View File

@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Formats.Asn1;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -626,7 +627,7 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
if (entry.Proof?.Checkpoint?.Timestamp is not null)
{
attributes = attributes.Add("checkpointTs", entry.Proof.Checkpoint.Timestamp.Value.ToString("O"));
attributes = attributes.Add("checkpointTs", entry.Proof.Checkpoint.Timestamp.Value.ToString("O", CultureInfo.InvariantCulture));
}
return new PolicyEvaluationResult

View File

@@ -26,25 +26,28 @@ public class DistributedVerificationProvider : IVerificationProvider
private readonly ILogger<DistributedVerificationProvider> _logger;
private readonly DistributedVerificationOptions _options;
private readonly HttpClient _httpClient;
private readonly TimeProvider _timeProvider;
private readonly ConcurrentDictionary<string, CircuitBreakerState> _circuitStates = new();
private readonly ConsistentHashRing _hashRing;
public DistributedVerificationProvider(
ILogger<DistributedVerificationProvider> logger,
IOptions<DistributedVerificationOptions> options,
HttpClient httpClient)
HttpClient httpClient,
TimeProvider? timeProvider = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_timeProvider = timeProvider ?? TimeProvider.System;
if (_options.Nodes == null || _options.Nodes.Count == 0)
{
throw new ArgumentException("At least one verification node must be configured");
}
_hashRing = new ConsistentHashRing(_options.Nodes, _options.VirtualNodeMultiplier);
_logger.LogInformation("Initialized distributed verification provider with {NodeCount} nodes", _options.Nodes.Count);
}
@@ -106,7 +109,7 @@ public class DistributedVerificationProvider : IVerificationProvider
RequestId = request.RequestId,
Status = VerificationStatus.Error,
ErrorMessage = $"All verification nodes failed. {exceptions.Count} errors occurred.",
Timestamp = DateTimeOffset.UtcNow,
Timestamp = _timeProvider.GetUtcNow(),
};
}
@@ -144,7 +147,7 @@ public class DistributedVerificationProvider : IVerificationProvider
HealthyNodeCount = healthyCount,
TotalNodeCount = totalCount,
NodeStatuses = results.ToDictionary(r => r.Key, r => r.Value),
Timestamp = DateTimeOffset.UtcNow,
Timestamp = _timeProvider.GetUtcNow(),
};
}
@@ -237,8 +240,8 @@ public class DistributedVerificationProvider : IVerificationProvider
}
// Allow recovery after cooldown period
if (state.LastFailure.HasValue &&
DateTimeOffset.UtcNow - state.LastFailure.Value > _options.CircuitBreakerCooldown)
if (state.LastFailure.HasValue &&
_timeProvider.GetUtcNow() - state.LastFailure.Value > _options.CircuitBreakerCooldown)
{
state.FailureCount = 0;
state.LastFailure = null;
@@ -252,7 +255,7 @@ public class DistributedVerificationProvider : IVerificationProvider
{
var state = _circuitStates.GetOrAdd(node.Id, _ => new CircuitBreakerState());
state.FailureCount++;
state.LastFailure = DateTimeOffset.UtcNow;
state.LastFailure = _timeProvider.GetUtcNow();
if (state.FailureCount >= _options.CircuitBreakerThreshold)
{

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0071-M | DONE | Maintainability audit for StellaOps.Attestor.Verify. |
| AUDIT-0071-T | DONE | Test coverage audit for StellaOps.Attestor.Verify. |
| AUDIT-0071-A | DONE | Applied DSSE PAE spec, SAN parsing, keyless chain store fix, KMS count fix, distributed provider cleanup, and tests. |
| AUDIT-0071-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
| AUDIT-0071-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
| AUDIT-0071-A | TODO | Reopened after revalidation 2026-01-06. |

View File

@@ -13,10 +13,6 @@
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="xunit.runner.visualstudio" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -645,7 +646,7 @@ internal sealed class AttestorSubmissionService : IAttestorSubmissionService
Aggregator = witness.Aggregator,
Status = witness.Status,
RootHash = witness.RootHash,
RetrievedAt = witness.RetrievedAt == default ? null : witness.RetrievedAt.ToString("O"),
RetrievedAt = witness.RetrievedAt == default ? null : witness.RetrievedAt.ToString("O", CultureInfo.InvariantCulture),
Statement = witness.Statement,
Signature = witness.Signature,
KeyId = witness.KeyId,
@@ -667,7 +668,7 @@ internal sealed class AttestorSubmissionService : IAttestorSubmissionService
Origin = proof.Checkpoint.Origin,
Size = proof.Checkpoint.Size,
RootHash = proof.Checkpoint.RootHash,
Timestamp = proof.Checkpoint.Timestamp?.ToString("O")
Timestamp = proof.Checkpoint.Timestamp?.ToString("O", CultureInfo.InvariantCulture)
},
Inclusion = proof.Inclusion is null ? null : new AttestorSubmissionResult.InclusionProof
{

View File

@@ -5,7 +5,7 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0055-M | DONE | Maintainability audit for StellaOps.Attestor.Infrastructure. |
| AUDIT-0055-T | DONE | Test coverage audit for StellaOps.Attestor.Infrastructure. |
| AUDIT-0055-A | DONE | Applied audit remediation and added infrastructure tests. |
| AUDIT-0055-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0055-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0055-A | TODO | Reopened after revalidation 2026-01-06. |
| VAL-SMOKE-001 | DONE | Fixed continuation token behavior; unit tests pass. |

View File

@@ -13,10 +13,6 @@
<PackageReference Include="NSubstitute" />
<PackageReference Include="Testcontainers" />
<PackageReference Include="Testcontainers.PostgreSql" />
<PackageReference Include="xunit.runner.visualstudio" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

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

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
@@ -131,7 +132,7 @@ internal static class AttestorWebServiceEndpoints
Algorithm = result.Algorithm,
Mode = result.Mode,
Provider = result.Provider,
SignedAt = result.SignedAt.ToString("O")
SignedAt = result.SignedAt.ToString("O", CultureInfo.InvariantCulture)
}
};
return Results.Ok(response);
@@ -251,14 +252,14 @@ internal static class AttestorWebServiceEndpoints
{
KeyId = result.SignerKeyId,
Algorithm = result.Algorithm,
SignedAt = result.SignedAt.ToString("O")
SignedAt = result.SignedAt.ToString("O", CultureInfo.InvariantCulture)
},
Rekor = result.RekorEntry is null ? null : new InTotoRekorEntryDto
{
LogId = result.RekorEntry.LogId,
LogIndex = result.RekorEntry.LogIndex,
Uuid = result.RekorEntry.Uuid,
IntegratedTime = result.RekorEntry.IntegratedTime?.ToString("O")
IntegratedTime = result.RekorEntry.IntegratedTime?.ToString("O", CultureInfo.InvariantCulture)
}
};
@@ -424,7 +425,7 @@ internal static class AttestorWebServiceEndpoints
Origin = entry.Proof.Checkpoint.Origin,
Size = entry.Proof.Checkpoint.Size,
RootHash = entry.Proof.Checkpoint.RootHash,
Timestamp = entry.Proof.Checkpoint.Timestamp?.ToString("O")
Timestamp = entry.Proof.Checkpoint.Timestamp?.ToString("O", CultureInfo.InvariantCulture)
},
Inclusion = entry.Proof.Inclusion is null ? null : new AttestationInclusionDto
{
@@ -448,7 +449,7 @@ internal static class AttestorWebServiceEndpoints
Origin = entry.Mirror.Proof.Checkpoint.Origin,
Size = entry.Mirror.Proof.Checkpoint.Size,
RootHash = entry.Mirror.Proof.Checkpoint.RootHash,
Timestamp = entry.Mirror.Proof.Checkpoint.Timestamp?.ToString("O")
Timestamp = entry.Mirror.Proof.Checkpoint.Timestamp?.ToString("O", CultureInfo.InvariantCulture)
},
Inclusion = entry.Mirror.Proof.Inclusion is null ? null : new AttestationInclusionDto
{
@@ -474,7 +475,7 @@ internal static class AttestorWebServiceEndpoints
{
Uuid = entry.RekorUuid,
Status = entry.Status,
CreatedAt = entry.CreatedAt.ToString("O"),
CreatedAt = entry.CreatedAt.ToString("O", CultureInfo.InvariantCulture),
Artifact = new AttestationArtifactDto
{
Sha256 = entry.Artifact.Sha256,

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Http;
using StellaOps.Attestor.Core.Bulk;
@@ -84,9 +85,9 @@ internal static class BulkVerificationContracts
{
Id = job.Id,
Status = job.Status.ToString().ToLowerInvariant(),
CreatedAt = job.CreatedAt.ToString("O"),
StartedAt = job.StartedAt?.ToString("O"),
CompletedAt = job.CompletedAt?.ToString("O"),
CreatedAt = job.CreatedAt.ToString("O", CultureInfo.InvariantCulture),
StartedAt = job.StartedAt?.ToString("O", CultureInfo.InvariantCulture),
CompletedAt = job.CompletedAt?.ToString("O", CultureInfo.InvariantCulture),
Processed = job.ProcessedCount,
Succeeded = job.SucceededCount,
Failed = job.FailedCount,
@@ -112,8 +113,8 @@ internal static class BulkVerificationContracts
{
Index = item.Index,
Status = item.Status.ToString().ToLowerInvariant(),
StartedAt = item.StartedAt?.ToString("O"),
CompletedAt = item.CompletedAt?.ToString("O"),
StartedAt = item.StartedAt?.ToString("O", CultureInfo.InvariantCulture),
CompletedAt = item.CompletedAt?.ToString("O", CultureInfo.InvariantCulture),
Error = item.Error,
Request = new BulkVerificationItemRequestDto
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -154,7 +155,7 @@ public class VerdictController : ControllerBase
Envelope = Convert.ToBase64String(Encoding.UTF8.GetBytes(envelopeJson)),
RekorLogIndex = rekorLogIndex,
KeyId = signResult.KeyId ?? request.KeyId ?? "default",
CreatedAt = _timeProvider.GetUtcNow().ToString("O")
CreatedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
};
_logger.LogInformation(

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0072-M | DONE | Maintainability audit for StellaOps.Attestor.WebService. |
| AUDIT-0072-T | DONE | Test coverage audit for StellaOps.Attestor.WebService. |
| AUDIT-0072-A | DONE | Addressed WebService audit findings (composition split, feature gating, auth/rate limits, TimeProvider, tests). |
| AUDIT-0072-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
| AUDIT-0072-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
| AUDIT-0072-A | TODO | Reopened after revalidation 2026-01-06. |

View File

@@ -23,6 +23,7 @@ public sealed class KmsOrgKeySigner : IOrgKeySigner
private readonly IKmsProvider _kmsProvider;
private readonly ILogger<KmsOrgKeySigner> _logger;
private readonly OrgSigningOptions _options;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Create a new KMS organization key signer.
@@ -30,11 +31,13 @@ public sealed class KmsOrgKeySigner : IOrgKeySigner
public KmsOrgKeySigner(
IKmsProvider kmsProvider,
ILogger<KmsOrgKeySigner> logger,
IOptions<OrgSigningOptions> options)
IOptions<OrgSigningOptions> options,
TimeProvider? timeProvider = null)
{
_kmsProvider = kmsProvider ?? throw new ArgumentNullException(nameof(kmsProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options?.Value ?? new OrgSigningOptions();
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
@@ -62,7 +65,7 @@ public sealed class KmsOrgKeySigner : IOrgKeySigner
}
// Check key expiry
if (keyInfo.ValidUntil.HasValue && keyInfo.ValidUntil.Value < DateTimeOffset.UtcNow)
if (keyInfo.ValidUntil.HasValue && keyInfo.ValidUntil.Value < _timeProvider.GetUtcNow())
{
throw new InvalidOperationException($"Signing key '{keyId}' has expired.");
}
@@ -87,7 +90,7 @@ public sealed class KmsOrgKeySigner : IOrgKeySigner
KeyId = keyId,
Algorithm = keyInfo.Algorithm,
Signature = Convert.ToBase64String(signatureBytes),
SignedAt = DateTimeOffset.UtcNow,
SignedAt = _timeProvider.GetUtcNow(),
CertificateChain = certChain
};
}
@@ -140,9 +143,10 @@ public sealed class KmsOrgKeySigner : IOrgKeySigner
// List keys and find the active one based on rotation policy
var keys = await ListKeysAsync(cancellationToken);
var now = _timeProvider.GetUtcNow();
var activeKey = keys
.Where(k => k.IsActive)
.Where(k => !k.ValidUntil.HasValue || k.ValidUntil.Value > DateTimeOffset.UtcNow)
.Where(k => !k.ValidUntil.HasValue || k.ValidUntil.Value > now)
.OrderByDescending(k => k.ValidFrom)
.FirstOrDefault();
@@ -253,14 +257,16 @@ public sealed class LocalOrgKeySigner : IOrgKeySigner
{
private readonly Dictionary<string, (ECDsa Key, OrgKeyInfo Info)> _keys = new();
private readonly ILogger<LocalOrgKeySigner> _logger;
private readonly TimeProvider _timeProvider;
private string? _activeKeyId;
/// <summary>
/// Create a new local key signer.
/// </summary>
public LocalOrgKeySigner(ILogger<LocalOrgKeySigner> logger)
public LocalOrgKeySigner(ILogger<LocalOrgKeySigner> logger, TimeProvider? timeProvider = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <summary>
@@ -276,7 +282,7 @@ public sealed class LocalOrgKeySigner : IOrgKeySigner
keyId,
"ECDSA_P256",
fingerprint,
DateTimeOffset.UtcNow,
_timeProvider.GetUtcNow(),
null,
isActive);
@@ -308,7 +314,7 @@ public sealed class LocalOrgKeySigner : IOrgKeySigner
KeyId = keyId,
Algorithm = "ECDSA_P256",
Signature = Convert.ToBase64String(signature),
SignedAt = DateTimeOffset.UtcNow,
SignedAt = _timeProvider.GetUtcNow(),
CertificateChain = null
});
}

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0053-M | DONE | Maintainability audit for StellaOps.Attestor.GraphRoot. |
| AUDIT-0053-T | DONE | Test coverage audit for StellaOps.Attestor.GraphRoot. |
| AUDIT-0053-A | DONE | Applied audit remediation for graph root attestation. |
| AUDIT-0053-M | DONE | Revalidated maintainability for StellaOps.Attestor.GraphRoot. |
| AUDIT-0053-T | DONE | Revalidated test coverage for StellaOps.Attestor.GraphRoot. |
| AUDIT-0053-A | DONE | Revalidated; no new issues. |

View File

@@ -4,6 +4,7 @@
// Task: Implement OCI registry attachment via ORAS
// -----------------------------------------------------------------------------
using System.Globalization;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.Extensions.Logging;
@@ -327,7 +328,7 @@ public sealed class OrasAttestationAttacher : IOciAttestationAttacher
{
var annotations = new Dictionary<string, string>(StringComparer.Ordinal)
{
[AnnotationKeys.Created] = createdAt.ToString("O"),
[AnnotationKeys.Created] = createdAt.ToString("O", CultureInfo.InvariantCulture),
[AnnotationKeys.PredicateType] = predicateType,
[AnnotationKeys.CosignSignature] = "" // Cosign compatibility placeholder
};

View File

@@ -5,7 +5,7 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0056-M | DONE | Maintainability audit for StellaOps.Attestor.Oci. |
| AUDIT-0056-T | DONE | Test coverage audit for StellaOps.Attestor.Oci. |
| AUDIT-0056-A | DONE | Applied audit remediation for OCI attacher and references. |
| AUDIT-0056-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0056-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0056-A | TODO | Reopened after revalidation 2026-01-06. |
| VAL-SMOKE-001 | DONE | Fixed build issue in Attestor OCI attacher. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0058-M | DONE | Maintainability audit for StellaOps.Attestor.Offline. |
| AUDIT-0058-T | DONE | Test coverage audit for StellaOps.Attestor.Offline. |
| AUDIT-0058-A | DONE | Applied DSSE verification, config defaults, offline kit gating, and deterministic ordering. |
| AUDIT-0058-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0058-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0058-A | TODO | Reopened after revalidation 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-0060-M | DONE | Maintainability audit for StellaOps.Attestor.Persistence. |
| AUDIT-0060-T | DONE | Test coverage audit for StellaOps.Attestor.Persistence. |
| AUDIT-0060-A | DONE | Applied defaults, normalization, deterministic matching, perf script, tests. |
| AUDIT-0060-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0060-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0060-A | TODO | Reopened after revalidation 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-0062-M | DONE | Maintainability audit for StellaOps.Attestor.ProofChain. |
| AUDIT-0062-T | DONE | Test coverage audit for StellaOps.Attestor.ProofChain. |
| AUDIT-0062-A | DONE | Applied determinism, time providers, canonicalization, schema validation, tests. |
| AUDIT-0062-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0062-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0062-A | TODO | Reopened after revalidation 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-0064-M | DONE | Maintainability audit for StellaOps.Attestor.StandardPredicates. |
| AUDIT-0064-T | DONE | Test coverage audit for StellaOps.Attestor.StandardPredicates. |
| AUDIT-0064-A | DONE | Applied canonicalization, registry normalization, parser metadata fixes, tests. |
| AUDIT-0064-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0064-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0064-A | TODO | Reopened after revalidation 2026-01-06. |

View File

@@ -12,7 +12,6 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="Moq" />
<PackageReference Include="FluentAssertions" />

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0068-M | DONE | Maintainability audit for TrustVerdict tests. |
| AUDIT-0068-T | DONE | Test coverage audit for TrustVerdict tests. |
| AUDIT-0068-A | TODO | Pending approval for changes. |
| AUDIT-0068-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
| AUDIT-0068-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
| AUDIT-0068-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-0067-M | DONE | Maintainability audit for StellaOps.Attestor.TrustVerdict. |
| AUDIT-0067-T | DONE | Test coverage audit for StellaOps.Attestor.TrustVerdict. |
| AUDIT-0067-A | DONE | Applied audit fixes for TrustVerdict library. |
| AUDIT-0067-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
| AUDIT-0067-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
| AUDIT-0067-A | TODO | Reopened after revalidation 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-0054-M | DONE | Maintainability audit for StellaOps.Attestor.GraphRoot.Tests. |
| AUDIT-0054-T | DONE | Test coverage audit for StellaOps.Attestor.GraphRoot.Tests. |
| AUDIT-0054-A | TODO | Pending approval for changes. |
| AUDIT-0054-M | DONE | Revalidated maintainability for StellaOps.Attestor.GraphRoot.Tests. |
| AUDIT-0054-T | DONE | Revalidated test coverage for StellaOps.Attestor.GraphRoot.Tests. |
| AUDIT-0054-A | DONE | Waived (test project; revalidated 2026-01-06). |

View File

@@ -5,5 +5,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0055-A | DONE | Added infrastructure regression tests for audit remediation. |
| AUDIT-0055-A | TODO | Reopened after revalidation 2026-01-06 (additional coverage needed). |
| VAL-SMOKE-001 | DONE | Removed xUnit v2 references and verified unit tests pass. |

View File

@@ -11,10 +11,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.runner.visualstudio" >
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
<PackageReference Include="Testcontainers" />

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0057-M | DONE | Maintainability audit for StellaOps.Attestor.Oci.Tests. |
| AUDIT-0057-T | DONE | Test coverage audit for StellaOps.Attestor.Oci.Tests. |
| AUDIT-0057-A | TODO | Pending approval for changes. |
| AUDIT-0057-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0057-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0057-A | DONE | Waived after revalidation 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-0059-M | DONE | Maintainability audit for StellaOps.Attestor.Offline.Tests. |
| AUDIT-0059-T | DONE | Test coverage audit for StellaOps.Attestor.Offline.Tests. |
| AUDIT-0059-A | TODO | Pending approval for changes. |
| AUDIT-0059-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0059-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0059-A | DONE | Waived after revalidation 2026-01-06. |

View File

@@ -15,10 +15,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="xunit.runner.visualstudio" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0061-M | DONE | Maintainability audit for StellaOps.Attestor.Persistence.Tests. |
| AUDIT-0061-T | DONE | Test coverage audit for StellaOps.Attestor.Persistence.Tests. |
| AUDIT-0061-A | TODO | Pending approval for changes. |
| AUDIT-0061-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0061-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0061-A | DONE | Waived after revalidation 2026-01-06. |

View File

@@ -14,10 +14,6 @@
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="xunit.runner.visualstudio" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -5,7 +5,7 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0063-M | DONE | Maintainability audit for StellaOps.Attestor.ProofChain.Tests. |
| AUDIT-0063-T | DONE | Test coverage audit for StellaOps.Attestor.ProofChain.Tests. |
| AUDIT-0063-A | TODO | Pending approval for changes. |
| AUDIT-0063-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0063-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0063-A | DONE | Waived after revalidation 2026-01-06. |
| VAL-SMOKE-001 | DONE | Fixed detached payload reference expectations; 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-0065-M | DONE | Maintainability audit for StandardPredicates tests. |
| AUDIT-0065-T | DONE | Test coverage audit for StandardPredicates tests. |
| AUDIT-0065-A | TODO | Pending approval for changes. |
| AUDIT-0065-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0065-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0065-A | DONE | Waived after revalidation 2026-01-06. |

View File

@@ -12,11 +12,6 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="JsonSchema.Net" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

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

View File

@@ -12,11 +12,6 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio" >
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" >
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0079-M | DONE | Maintainability audit for StellaOps.Auth.Abstractions.Tests. |
| AUDIT-0079-T | DONE | Test coverage audit for StellaOps.Auth.Abstractions.Tests. |
| AUDIT-0079-A | TODO | Pending approval for changes. |
| AUDIT-0079-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0079-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0079-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-0078-M | DONE | Maintainability audit for StellaOps.Auth.Abstractions. |
| AUDIT-0078-T | DONE | Test coverage audit for StellaOps.Auth.Abstractions. |
| AUDIT-0078-A | DONE | Scope ordering, warning discipline, and coverage gaps addressed. |
| AUDIT-0078-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0078-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0078-A | DONE | Revalidated 2026-01-06 (no changes). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0081-M | DONE | Maintainability audit for StellaOps.Auth.Client.Tests. |
| AUDIT-0081-T | DONE | Test coverage audit for StellaOps.Auth.Client.Tests. |
| AUDIT-0081-A | TODO | Pending approval for changes. |
| AUDIT-0081-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0081-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0081-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-0080-M | DONE | Maintainability audit for StellaOps.Auth.Client. |
| AUDIT-0080-T | DONE | Test coverage audit for StellaOps.Auth.Client. |
| AUDIT-0080-A | DONE | Retry options, shared cache, and coverage gaps addressed. |
| AUDIT-0080-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0080-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0080-A | DONE | Revalidated 2026-01-06 (no changes). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0084-M | DONE | Maintainability audit for StellaOps.Auth.ServerIntegration.Tests. |
| AUDIT-0084-T | DONE | Test coverage audit for StellaOps.Auth.ServerIntegration.Tests. |
| AUDIT-0084-A | TODO | Pending approval for changes. |
| AUDIT-0084-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0084-T | DONE | Revalidated 2026-01-06 (coverage updated). |
| AUDIT-0084-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-0083-M | DONE | Maintainability audit for StellaOps.Auth.ServerIntegration. |
| AUDIT-0083-T | DONE | Test coverage audit for StellaOps.Auth.ServerIntegration. |
| AUDIT-0083-A | DONE | Metadata fallback, scope normalization, and coverage gaps addressed. |
| AUDIT-0083-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0083-T | DONE | Revalidated 2026-01-06 (tests cover metadata caching, bypass checks, scope normalization). |
| AUDIT-0083-A | TODO | Reopened 2026-01-06: remove Guid.NewGuid fallback for correlation IDs; keep tests deterministic. |

View File

@@ -16,9 +16,5 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="xunit.runner.visualstudio" >
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</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-0091-M | DONE | Maintainability audit for StellaOps.Authority.Plugin.Ldap.Tests. |
| AUDIT-0091-T | DONE | Test coverage audit for StellaOps.Authority.Plugin.Ldap.Tests. |
| AUDIT-0091-A | TODO | Pending approval for changes. |
| AUDIT-0091-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0091-T | DONE | Revalidated 2026-01-06 (coverage reviewed). |
| AUDIT-0091-A | DONE | Waived (test project; revalidated 2026-01-06). |

View File

@@ -22,6 +22,7 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
private readonly IOptionsMonitor<LdapPluginOptions> optionsMonitor;
private readonly LdapClientProvisioningStore clientProvisioningStore;
private readonly ILogger<LdapIdentityProviderPlugin> logger;
private readonly TimeProvider timeProvider;
private readonly LdapCapabilityProbe capabilityProbe;
private readonly AuthorityIdentityProviderCapabilities manifestCapabilities;
private readonly SemaphoreSlim capabilityGate = new(1, 1);
@@ -38,7 +39,8 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
ILdapConnectionFactory connectionFactory,
IOptionsMonitor<LdapPluginOptions> optionsMonitor,
LdapClientProvisioningStore clientProvisioningStore,
ILogger<LdapIdentityProviderPlugin> logger)
ILogger<LdapIdentityProviderPlugin> logger,
TimeProvider? timeProvider = null)
{
this.pluginContext = pluginContext ?? throw new ArgumentNullException(nameof(pluginContext));
this.credentialStore = credentialStore ?? throw new ArgumentNullException(nameof(credentialStore));
@@ -47,6 +49,7 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
this.optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
this.clientProvisioningStore = clientProvisioningStore ?? throw new ArgumentNullException(nameof(clientProvisioningStore));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.timeProvider = timeProvider ?? TimeProvider.System;
capabilityProbe = new LdapCapabilityProbe(pluginContext.Manifest.Name, connectionFactory, logger);
@@ -142,7 +145,7 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
var checkBootstrap = manifestCapabilities.SupportsBootstrap && options.Bootstrap.Enabled;
var fingerprint = LdapCapabilitySnapshotCache.ComputeFingerprint(options, checkProvisioning, checkBootstrap);
if (LdapCapabilitySnapshotCache.TryGet(Name, fingerprint, DateTimeOffset.UtcNow, out var snapshot))
if (LdapCapabilitySnapshotCache.TryGet(Name, fingerprint, timeProvider.GetUtcNow(), out var snapshot))
{
UpdateCapabilities(snapshot, checkProvisioning, checkBootstrap, logDegrade: true);
}
@@ -158,7 +161,7 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
var checkProvisioning = manifestCapabilities.SupportsClientProvisioning && options.ClientProvisioning.Enabled;
var checkBootstrap = manifestCapabilities.SupportsBootstrap && options.Bootstrap.Enabled;
var fingerprint = LdapCapabilitySnapshotCache.ComputeFingerprint(options, checkProvisioning, checkBootstrap);
var now = DateTimeOffset.UtcNow;
var now = timeProvider.GetUtcNow();
if (LdapCapabilitySnapshotCache.TryGet(Name, fingerprint, now, out var cached))
{
@@ -169,7 +172,7 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
await capabilityGate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (LdapCapabilitySnapshotCache.TryGet(Name, fingerprint, DateTimeOffset.UtcNow, out cached))
if (LdapCapabilitySnapshotCache.TryGet(Name, fingerprint, timeProvider.GetUtcNow(), out cached))
{
UpdateCapabilities(cached, checkProvisioning, checkBootstrap, logDegrade: true);
return;
@@ -183,7 +186,7 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
cancellationToken)
.ConfigureAwait(false);
LdapCapabilitySnapshotCache.Set(Name, fingerprint, DateTimeOffset.UtcNow, options.CapabilityProbe.CacheTtl, snapshot);
LdapCapabilitySnapshotCache.Set(Name, fingerprint, timeProvider.GetUtcNow(), options.CapabilityProbe.CacheTtl, snapshot);
UpdateCapabilities(snapshot, checkProvisioning, checkBootstrap, logDegrade: true);
}
finally

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0090-M | DONE | Maintainability audit for StellaOps.Authority.Plugin.Ldap. |
| AUDIT-0090-T | DONE | Test coverage audit for StellaOps.Authority.Plugin.Ldap. |
| AUDIT-0090-A | DONE | Applied LDAP plugin updates, tests, and docs. |
| AUDIT-0090-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0090-T | DONE | Revalidated 2026-01-06 (coverage reviewed). |
| AUDIT-0090-A | TODO | Reopened 2026-01-06: fix TWA override, TimeProvider/IGuidGenerator use, and plugin health 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-0093-M | DONE | Maintainability audit for StellaOps.Authority.Plugin.Oidc.Tests. |
| AUDIT-0093-T | DONE | Test coverage audit for StellaOps.Authority.Plugin.Oidc.Tests. |
| AUDIT-0093-A | TODO | Pending approval for changes. |
| AUDIT-0093-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0093-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0093-A | DONE | Waived (test project; revalidated 2026-01-06). |

View File

@@ -3,6 +3,7 @@
// Credential store for validating OIDC tokens.
// -----------------------------------------------------------------------------
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.Extensions.Caching.Memory;
@@ -161,7 +162,7 @@ internal sealed class OidcCredentialStore : IUserCredentialStore
new[]
{
new AuthEventProperty { Name = "oidc_issuer", Value = ClassifiedString.Public(jwtToken.Issuer) },
new AuthEventProperty { Name = "token_valid_until", Value = ClassifiedString.Public(jwtToken.ValidTo.ToString("O")) }
new AuthEventProperty { Name = "token_valid_until", Value = ClassifiedString.Public(jwtToken.ValidTo.ToString("O", CultureInfo.InvariantCulture)) }
});
}
catch (SecurityTokenExpiredException ex)

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0092-M | DONE | Maintainability audit for StellaOps.Authority.Plugin.Oidc. |
| AUDIT-0092-T | DONE | Test coverage audit for StellaOps.Authority.Plugin.Oidc. |
| AUDIT-0092-A | DONE | Applied OIDC plugin updates and tests. |
| AUDIT-0092-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0092-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0092-A | TODO | Revalidated 2026-01-06 (open findings). |

View File

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

View File

@@ -3,6 +3,7 @@
// Credential store for validating SAML assertions.
// -----------------------------------------------------------------------------
using System.Globalization;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
@@ -31,6 +32,7 @@ internal sealed class SamlCredentialStore : IUserCredentialStore
private readonly IMemoryCache sessionCache;
private readonly ILogger<SamlCredentialStore> logger;
private readonly IHttpClientFactory httpClientFactory;
private readonly TimeProvider timeProvider;
private readonly Saml2SecurityTokenHandler tokenHandler;
private X509Certificate2? idpSigningCertificate;
private string? certificateCacheKey;
@@ -42,13 +44,15 @@ internal sealed class SamlCredentialStore : IUserCredentialStore
IOptionsMonitor<SamlPluginOptions> optionsMonitor,
IMemoryCache sessionCache,
ILogger<SamlCredentialStore> logger,
IHttpClientFactory httpClientFactory)
IHttpClientFactory httpClientFactory,
TimeProvider? timeProvider = null)
{
this.pluginName = pluginName ?? throw new ArgumentNullException(nameof(pluginName));
this.optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
this.sessionCache = sessionCache ?? throw new ArgumentNullException(nameof(sessionCache));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
this.timeProvider = timeProvider ?? TimeProvider.System;
tokenHandler = new Saml2SecurityTokenHandler();
@@ -162,7 +166,7 @@ internal sealed class SamlCredentialStore : IUserCredentialStore
["email"] = email,
["issuer"] = token.Assertion.Issuer?.Value,
["session_index"] = token.Assertion.Id?.Value,
["auth_instant"] = token.Assertion.IssueInstant.ToString("O")
["auth_instant"] = token.Assertion.IssueInstant.ToString("O", CultureInfo.InvariantCulture)
};
var user = new AuthorityUserDescriptor(
@@ -398,7 +402,7 @@ internal sealed class SamlCredentialStore : IUserCredentialStore
{
idpSigningCertificate = certificate;
certificateCacheKey = key;
lastMetadataRefresh = DateTimeOffset.UtcNow;
lastMetadataRefresh = timeProvider.GetUtcNow();
return;
}
@@ -427,7 +431,7 @@ internal sealed class SamlCredentialStore : IUserCredentialStore
return true;
}
return DateTimeOffset.UtcNow - lastMetadataRefresh.Value >= options.MetadataRefreshInterval;
return timeProvider.GetUtcNow() - lastMetadataRefresh.Value >= options.MetadataRefreshInterval;
}
private static string BuildCertificateCacheKey(SamlPluginOptions options)

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0094-M | DONE | Maintainability audit for StellaOps.Authority.Plugin.Saml. |
| AUDIT-0094-T | DONE | Test coverage audit for StellaOps.Authority.Plugin.Saml. |
| AUDIT-0094-A | DONE | Applied SAML plugin updates, tests, and docs. |
| AUDIT-0094-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0094-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0094-A | TODO | Revalidated 2026-01-06 (open findings). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0097-M | DONE | Maintainability audit for StellaOps.Authority.Plugin.Standard.Tests. |
| AUDIT-0097-T | DONE | Test coverage audit for StellaOps.Authority.Plugin.Standard.Tests. |
| AUDIT-0097-A | TODO | Pending approval for changes. |
| AUDIT-0097-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0097-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0097-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-0096-M | DONE | Maintainability audit for StellaOps.Authority.Plugin.Standard. |
| AUDIT-0096-T | DONE | Test coverage audit for StellaOps.Authority.Plugin.Standard. |
| AUDIT-0096-A | DONE | Pending approval for changes. |
| AUDIT-0096-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0096-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0096-A | TODO | Revalidated 2026-01-06 (open findings). |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0099-M | DONE | Maintainability audit for StellaOps.Authority.Plugins.Abstractions.Tests. |
| AUDIT-0099-T | DONE | Test coverage audit for StellaOps.Authority.Plugins.Abstractions.Tests. |
| AUDIT-0099-A | TODO | Pending approval for changes. |
| AUDIT-0099-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0099-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0099-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-0098-M | DONE | Maintainability audit for StellaOps.Authority.Plugins.Abstractions. |
| AUDIT-0098-T | DONE | Test coverage audit for StellaOps.Authority.Plugins.Abstractions. |
| AUDIT-0098-A | DONE | Pending approval for changes. |
| AUDIT-0098-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0098-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0098-A | TODO | Revalidated 2026-01-06 (open findings). |

View File

@@ -14,10 +14,6 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.runner.visualstudio" >
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Authority\StellaOps.Authority.csproj" />

View File

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

View File

@@ -1,6 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
@@ -98,8 +99,8 @@ internal sealed class AckTokenPayload
writer.WriteString("channel", Channel);
writer.WriteString("webhook", Webhook);
writer.WriteString("nonce", Nonce);
writer.WriteString("issuedAt", IssuedAt.UtcDateTime.ToString("O"));
writer.WriteString("expiresAt", ExpiresAt.UtcDateTime.ToString("O"));
writer.WriteString("issuedAt", IssuedAt.UtcDateTime.ToString("O", CultureInfo.InvariantCulture));
writer.WriteString("expiresAt", ExpiresAt.UtcDateTime.ToString("O", CultureInfo.InvariantCulture));
writer.WritePropertyName("actions");
writer.WriteStartArray();

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Globalization;
using System.Text.Json;
using StellaOps.Authority.Persistence.Documents;
using StellaOps.Authority.Persistence.Sessions;
@@ -453,7 +454,7 @@ internal sealed class PostgresTokenStore : IAuthorityTokenStore, IAuthorityRefre
if (document.RevokedAt is not null)
{
properties["revoked_at"] = document.RevokedAt.Value.ToUniversalTime().ToString("O");
properties["revoked_at"] = document.RevokedAt.Value.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture);
}
if (!string.IsNullOrWhiteSpace(document.RevokedReason))

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0085-M | DONE | Maintainability audit for StellaOps.Authority. |
| AUDIT-0085-T | DONE | Test coverage audit for StellaOps.Authority. |
| AUDIT-0085-A | DONE | Store determinism, replay tracking, issuer IDs, and tests. |
| AUDIT-0085-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0085-T | DONE | Revalidated 2026-01-06 (coverage reviewed). |
| AUDIT-0085-A | TODO | Reopened 2026-01-06: remove Guid.NewGuid/DateTimeOffset.UtcNow, fix branding error messages, and modularize Program.cs. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0086-M | DONE | Maintainability audit for StellaOps.Authority.Core. |
| AUDIT-0086-T | DONE | Test coverage audit for StellaOps.Authority.Core. |
| AUDIT-0086-A | DONE | Deterministic builder defaults, replay verifier handling, and tests. |
| AUDIT-0086-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0086-T | DONE | Revalidated 2026-01-06 (coverage reviewed). |
| AUDIT-0086-A | TODO | Reopened 2026-01-06: remove Guid.NewGuid default and switch digest to canonical JSON. |

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0088-M | DONE | Maintainability audit for StellaOps.Authority.Persistence. |
| AUDIT-0088-T | DONE | Test coverage audit for StellaOps.Authority.Persistence. |
| AUDIT-0088-A | DONE | Applied updates and tests. |
| AUDIT-0088-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0088-T | DONE | Revalidated 2026-01-06 (coverage reviewed). |
| AUDIT-0088-A | TODO | Reopened 2026-01-06: replace Guid.NewGuid ID paths with deterministic generator. |

View File

@@ -0,0 +1,22 @@
# Authority ConfigDiff Tests Charter
## Mission
- Maintain deterministic tests for Authority configuration diffing.
## Responsibilities
- Validate config diff output stability and edge cases.
- Keep fixtures deterministic and offline-safe.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- docs/modules/authority/architecture.md
## Definition of Done
- Tests are deterministic and offline-safe.
- Coverage includes mismatch detection and ordering behavior.
## Working Agreement
- Use fixed time and ids in fixtures.
- Avoid non-deterministic ordering; assert sorted output.

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0087-M | DONE | Maintainability audit for StellaOps.Authority.Core.Tests. |
| AUDIT-0087-T | DONE | Test coverage audit for StellaOps.Authority.Core.Tests. |
| AUDIT-0087-A | TODO | Pending approval for changes. |
| AUDIT-0087-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0087-T | DONE | Revalidated 2026-01-06 (coverage reviewed). |
| AUDIT-0087-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-0089-M | DONE | Maintainability audit for StellaOps.Authority.Persistence.Tests. |
| AUDIT-0089-T | DONE | Test coverage audit for StellaOps.Authority.Persistence.Tests. |
| AUDIT-0089-A | TODO | Pending approval for changes. |
| AUDIT-0089-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0089-T | DONE | Revalidated 2026-01-06 (coverage reviewed). |
| AUDIT-0089-A | DONE | Waived (test project; revalidated 2026-01-06). |

View File

@@ -8,10 +8,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.runner.visualstudio" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0105-M | DONE | Maintainability audit for StellaOps.Bench.LinkNotMerge.Vex.Tests. |
| AUDIT-0105-T | DONE | Test coverage audit for StellaOps.Bench.LinkNotMerge.Vex.Tests. |
| AUDIT-0105-A | TODO | Pending approval for changes. |
| AUDIT-0105-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0105-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0105-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-0104-M | DONE | Maintainability audit for StellaOps.Bench.LinkNotMerge.Vex. |
| AUDIT-0104-T | DONE | Test coverage audit for StellaOps.Bench.LinkNotMerge.Vex. |
| AUDIT-0104-A | TODO | Pending approval for changes. |
| AUDIT-0104-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0104-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0104-A | DONE | Waived (benchmark project; revalidated 2026-01-06). |

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