Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 }
|
||||
};
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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>
|
||||
@@ -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). |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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.
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user