docs re-org, audit fixes, build fixes
This commit is contained in:
@@ -72,6 +72,11 @@ public sealed class HeaderScopeAuthenticationHandler : AuthenticationHandler<Aut
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var scope in value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
scopes.Add(scope);
|
||||
|
||||
@@ -72,7 +72,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
null,
|
||||
new[] { new FeedBuildConfig("nvd-feed", "nvd", "2025-06-15", feedPath, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var bundleOutputPath = Path.Combine(_onlineEnvPath, "bundle");
|
||||
|
||||
@@ -120,7 +121,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
DateTimeOffset.UtcNow.AddDays(30),
|
||||
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedPath, "feeds/all-feeds.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
||||
new[] { new PolicyBuildConfig("policy-1", "default", "1.0", policyPath, "policies/default.rego", PolicyType.OpaRego) },
|
||||
new[] { new CryptoBuildConfig("crypto-1", "trust-root", certPath, "certs/root.pem", CryptoComponentType.TrustRoot, null) });
|
||||
new[] { new CryptoBuildConfig("crypto-1", "trust-root", certPath, "certs/root.pem", CryptoComponentType.TrustRoot, null) },
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var bundlePath = Path.Combine(_onlineEnvPath, "multi-bundle");
|
||||
|
||||
@@ -161,7 +163,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
null,
|
||||
new[] { new FeedBuildConfig("feed", "nvd", "v1", feedPath, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var bundlePath = Path.Combine(_onlineEnvPath, "corrupt-source");
|
||||
var manifest = await builder.BuildAsync(request, bundlePath);
|
||||
@@ -219,7 +222,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
null,
|
||||
Array.Empty<FeedBuildConfig>(),
|
||||
new[] { new PolicyBuildConfig("security-policy", "security", "1.0", policyPath, "policies/security.rego", PolicyType.OpaRego) },
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var bundlePath = Path.Combine(_onlineEnvPath, "policy-bundle");
|
||||
|
||||
@@ -273,7 +277,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
new PolicyBuildConfig("policy-2", "policy2", "1.0", policy2Path, "policies/policy2.rego", PolicyType.OpaRego),
|
||||
new PolicyBuildConfig("policy-3", "policy3", "1.0", policy3Path, "policies/policy3.rego", PolicyType.OpaRego)
|
||||
},
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var bundlePath = Path.Combine(_onlineEnvPath, "multi-policy");
|
||||
|
||||
@@ -315,7 +320,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
null,
|
||||
Array.Empty<FeedBuildConfig>(),
|
||||
new[] { new PolicyBuildConfig("signed-policy", "signed", "1.0", policyPath, "policies/signed.rego", PolicyType.OpaRego) },
|
||||
new[] { new CryptoBuildConfig("signing-cert", "signing", certPath, "certs/signing.pem", CryptoComponentType.SigningKey, null) });
|
||||
new[] { new CryptoBuildConfig("signing-cert", "signing", certPath, "certs/signing.pem", CryptoComponentType.SigningKey, null) },
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var bundlePath = Path.Combine(_onlineEnvPath, "signed-bundle");
|
||||
|
||||
|
||||
@@ -142,7 +142,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), FeedFormat.StellaOpsNative)
|
||||
},
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act - First export
|
||||
var manifest1 = await builder.BuildAsync(request, outputPath1);
|
||||
@@ -163,7 +164,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), FeedFormat.StellaOpsNative)
|
||||
},
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var manifest2 = await builder.BuildAsync(request2, outputPath2);
|
||||
|
||||
@@ -278,7 +280,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
new FeedBuildConfig("f3", "osv", "v1", feed3, "feeds/f3.json", DateTimeOffset.UtcNow, FeedFormat.OsvJson)
|
||||
},
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, Path.Combine(_tempRoot, "multi"));
|
||||
@@ -332,7 +335,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
new FeedBuildConfig("f1", "binary", "v1", source1, "data/binary.bin", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
||||
},
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var request2 = new BundleBuildRequest(
|
||||
"binary-test",
|
||||
@@ -343,7 +347,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
new FeedBuildConfig("f1", "binary", "v1", source2, "data/binary.bin", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
||||
},
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest1 = await builder.BuildAsync(request1, Path.Combine(_tempRoot, "bin1"));
|
||||
@@ -407,7 +412,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), FeedFormat.StellaOpsNative)
|
||||
},
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
}
|
||||
|
||||
private BundleManifest CreateDeterministicManifest(string name)
|
||||
|
||||
@@ -259,7 +259,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
null,
|
||||
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedFile1, "feeds/nvd.json", DateTimeOffset.Parse("2025-01-01T00:00:00Z"), FeedFormat.StellaOpsNative) },
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var request2 = new BundleBuildRequest(
|
||||
"determinism-test",
|
||||
@@ -267,7 +268,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
null,
|
||||
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedFile2, "feeds/nvd.json", DateTimeOffset.Parse("2025-01-01T00:00:00Z"), FeedFormat.StellaOpsNative) },
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var outputPath1 = Path.Combine(_tempRoot, "determinism-output1");
|
||||
var outputPath2 = Path.Combine(_tempRoot, "determinism-output2");
|
||||
@@ -363,7 +365,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
imported.Feeds[0].SnapshotAt,
|
||||
imported.Feeds[0].Format) },
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var bundlePath2 = Path.Combine(_tempRoot, "roundtrip2");
|
||||
var manifest2 = await builder.BuildAsync(reexportRequest, bundlePath2);
|
||||
@@ -409,7 +412,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
null,
|
||||
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", feedSourcePath, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
}
|
||||
|
||||
private static BundleManifest CreateTestManifest()
|
||||
|
||||
@@ -49,7 +49,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
null,
|
||||
Array.Empty<FeedBuildConfig>(),
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -93,7 +94,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
FeedFormat.StellaOpsNative)
|
||||
},
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -139,7 +141,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
"policies/default.rego",
|
||||
PolicyType.OpaRego)
|
||||
},
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -182,7 +185,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
"certs/root.pem",
|
||||
CryptoComponentType.TrustRoot,
|
||||
DateTimeOffset.UtcNow.AddYears(10))
|
||||
});
|
||||
},
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -225,7 +229,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
{
|
||||
new PolicyBuildConfig("p1", "default", "1.0", policy, "policies/default.rego", PolicyType.OpaRego)
|
||||
},
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -261,7 +266,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
new FeedBuildConfig("f1", "test", "v1", feedFile, "feeds/test.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
||||
},
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -288,7 +294,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
new FeedBuildConfig("f1", "test", "v1", feedFile, "feeds/test.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative)
|
||||
},
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -328,7 +335,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
new[]
|
||||
{
|
||||
new CryptoBuildConfig("c1", "root", certFile, "crypto/certs/ca/root.pem", CryptoComponentType.TrustRoot, null)
|
||||
});
|
||||
},
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -369,7 +377,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
new FeedBuildConfig("f1", "test", "v1", feedFile, "feeds/test.json", DateTimeOffset.UtcNow, format)
|
||||
},
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -404,7 +413,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
{
|
||||
new PolicyBuildConfig("p1", "test", "1.0", policyFile, "policies/test", type)
|
||||
},
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -440,7 +450,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
new[]
|
||||
{
|
||||
new CryptoBuildConfig("c1", "test", certFile, "certs/test", type, null)
|
||||
});
|
||||
},
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -468,7 +479,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
expiresAt,
|
||||
Array.Empty<FeedBuildConfig>(),
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
@@ -496,7 +508,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
new[]
|
||||
{
|
||||
new CryptoBuildConfig("c1", "root", certFile, "certs/root.pem", CryptoComponentType.TrustRoot, componentExpiry)
|
||||
});
|
||||
},
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
// Act
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
|
||||
@@ -49,7 +49,8 @@ public class BundleManifestTests
|
||||
null,
|
||||
new[] { new FeedBuildConfig("feed-1", "nvd", "v1", sourceFile, "feeds/nvd.json", DateTimeOffset.UtcNow, FeedFormat.StellaOpsNative) },
|
||||
Array.Empty<PolicyBuildConfig>(),
|
||||
Array.Empty<CryptoBuildConfig>());
|
||||
Array.Empty<CryptoBuildConfig>(),
|
||||
Array.Empty<RuleBundleBuildConfig>());
|
||||
|
||||
var outputPath = Path.Combine(tempRoot, "bundle");
|
||||
var manifest = await builder.BuildAsync(request, outputPath);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/StellaOps.Scanner.Analyzers.Lang.Node.csproj" />
|
||||
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Go/StellaOps.Scanner.Analyzers.Lang.Go.csproj" />
|
||||
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/StellaOps.Scanner.Analyzers.Lang.Java.csproj" />
|
||||
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python/StellaOps.Scanner.Analyzers.Lang.Python.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -26,10 +26,12 @@ internal sealed class ForensicVerifier : IForensicVerifier
|
||||
};
|
||||
|
||||
private readonly ILogger<ForensicVerifier> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public ForensicVerifier(ILogger<ForensicVerifier> logger)
|
||||
public ForensicVerifier(ILogger<ForensicVerifier> logger, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<ForensicVerificationResult> VerifyBundleAsync(
|
||||
@@ -42,7 +44,7 @@ internal sealed class ForensicVerifier : IForensicVerifier
|
||||
|
||||
var errors = new List<ForensicVerificationError>();
|
||||
var warnings = new List<string>();
|
||||
var verifiedAt = DateTimeOffset.UtcNow;
|
||||
var verifiedAt = _timeProvider.GetUtcNow();
|
||||
|
||||
_logger.LogDebug("Verifying forensic bundle at {BundlePath}", bundlePath);
|
||||
|
||||
@@ -440,7 +442,7 @@ internal sealed class ForensicVerifier : IForensicVerifier
|
||||
matchingRoot.PublicKey);
|
||||
|
||||
// Check time validity
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var timeValid = (!matchingRoot.NotBefore.HasValue || now >= matchingRoot.NotBefore.Value) &&
|
||||
(!matchingRoot.NotAfter.HasValue || now <= matchingRoot.NotAfter.Value);
|
||||
|
||||
|
||||
@@ -17,17 +17,20 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
||||
private readonly ITrustPolicyLoader _trustPolicyLoader;
|
||||
private readonly IDsseSignatureVerifier _dsseVerifier;
|
||||
private readonly ILogger<ImageAttestationVerifier> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public ImageAttestationVerifier(
|
||||
IOciRegistryClient registryClient,
|
||||
ITrustPolicyLoader trustPolicyLoader,
|
||||
IDsseSignatureVerifier dsseVerifier,
|
||||
ILogger<ImageAttestationVerifier> logger)
|
||||
ILogger<ImageAttestationVerifier> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_registryClient = registryClient ?? throw new ArgumentNullException(nameof(registryClient));
|
||||
_trustPolicyLoader = trustPolicyLoader ?? throw new ArgumentNullException(nameof(trustPolicyLoader));
|
||||
_dsseVerifier = dsseVerifier ?? throw new ArgumentNullException(nameof(dsseVerifier));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<ImageVerificationResult> VerifyAsync(
|
||||
@@ -51,7 +54,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
||||
ImageDigest = digest,
|
||||
Registry = reference.Registry,
|
||||
Repository = reference.Repository,
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
VerifiedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
OciReferrersResponse referrers;
|
||||
@@ -191,7 +194,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
||||
Digest = candidate.Digest,
|
||||
SignerIdentity = verification.KeyId,
|
||||
Message = verification.Error ?? "Signature verification failed",
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
VerifiedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -206,7 +209,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
||||
Digest = candidate.Digest,
|
||||
SignerIdentity = signerKeyId,
|
||||
Message = "Signer not allowed by trust policy",
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
VerifiedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -220,14 +223,14 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
||||
Digest = candidate.Digest,
|
||||
SignerIdentity = signerKeyId,
|
||||
Message = "Rekor receipt missing",
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
VerifiedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
if (policy.MaxAge.HasValue)
|
||||
{
|
||||
var created = GetCreatedAt(candidate);
|
||||
if (created.HasValue && DateTimeOffset.UtcNow - created.Value > policy.MaxAge.Value)
|
||||
if (created.HasValue && _timeProvider.GetUtcNow() - created.Value > policy.MaxAge.Value)
|
||||
{
|
||||
return new AttestationVerification
|
||||
{
|
||||
@@ -237,7 +240,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
||||
Digest = candidate.Digest,
|
||||
SignerIdentity = signerKeyId,
|
||||
Message = "Attestation exceeded max age",
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
VerifiedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -250,7 +253,7 @@ public sealed class ImageAttestationVerifier : IImageAttestationVerifier
|
||||
Digest = candidate.Digest,
|
||||
SignerIdentity = signerKeyId,
|
||||
Message = "Signature valid",
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
VerifiedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -76,7 +76,7 @@ public sealed class CertFrFeedClient
|
||||
}
|
||||
|
||||
var advisoryId = ResolveAdvisoryId(itemElement, detailUri);
|
||||
items.Add(new CertFrFeedItem(advisoryId, detailUri, published.ToUniversalTime(), title, summary));
|
||||
items.Add(new CertFrFeedItem(advisoryId, detailUri, published.Value.ToUniversalTime(), title, summary));
|
||||
}
|
||||
|
||||
_diagnostics.FeedFetchSuccess(items.Count);
|
||||
|
||||
@@ -73,6 +73,7 @@ public sealed class CachePerformanceBenchmarkTests : IAsyncLifetime
|
||||
_cacheService = new ValkeyAdvisoryCacheService(
|
||||
_connectionFactory,
|
||||
options,
|
||||
metrics: null,
|
||||
NullLogger<ValkeyAdvisoryCacheService>.Instance);
|
||||
|
||||
await ValueTask.CompletedTask;
|
||||
|
||||
@@ -9,13 +9,16 @@ public sealed class ExportRetentionService : IExportRetentionService
|
||||
{
|
||||
private readonly IExportRetentionStore _retentionStore;
|
||||
private readonly ILogger<ExportRetentionService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public ExportRetentionService(
|
||||
IExportRetentionStore retentionStore,
|
||||
ILogger<ExportRetentionService> logger)
|
||||
ILogger<ExportRetentionService> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_retentionStore = retentionStore ?? throw new ArgumentNullException(nameof(retentionStore));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -26,7 +29,7 @@ public sealed class ExportRetentionService : IExportRetentionService
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var retention = request.OverrideRetention ?? new ExportRetentionConfig();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Starting retention prune for tenant {TenantId}, profile {ProfileId}, execute={Execute}",
|
||||
|
||||
@@ -11,6 +11,12 @@ public sealed class InMemoryExportScheduleStore : IExportScheduleStore
|
||||
private readonly ConcurrentDictionary<Guid, Guid> _runToProfile = new();
|
||||
private readonly ConcurrentDictionary<Guid, List<ScheduledProfileInfo>> _profilesByTenant = new();
|
||||
private readonly object _lock = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryExportScheduleStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a profile for testing.
|
||||
@@ -106,7 +112,7 @@ public sealed class InMemoryExportScheduleStore : IExportScheduleStore
|
||||
{
|
||||
if (_statusByProfile.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var newFailureCount = success ? 0 : existing.ConsecutiveFailures + 1;
|
||||
|
||||
_statusByProfile[profileId] = existing with
|
||||
|
||||
@@ -32,12 +32,14 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
};
|
||||
|
||||
private readonly ILogger<LineageEvidencePackService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ConcurrentDictionary<Guid, CachedPack> _packCache = new();
|
||||
private readonly string _tempDirectory;
|
||||
|
||||
public LineageEvidencePackService(ILogger<LineageEvidencePackService> logger)
|
||||
public LineageEvidencePackService(ILogger<LineageEvidencePackService> logger, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_tempDirectory = Path.Combine(Path.GetTempPath(), "stellaops-evidence-packs");
|
||||
Directory.CreateDirectory(_tempDirectory);
|
||||
}
|
||||
@@ -187,7 +189,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
Entries = entries.ToImmutableArray(),
|
||||
TotalSizeBytes = entries.Sum(e => e.SizeBytes),
|
||||
FileCount = entries.Count,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
// Write manifest
|
||||
@@ -205,7 +207,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
VexVerdictDigests = vexDocuments.Select(v => v.Digest).ToImmutableArray(),
|
||||
PolicyVerdictDigest = policyVerdict?.Digest,
|
||||
ReplayHash = ComputeReplayHash(artifactDigest, sbomDigest, manifest.MerkleRoot),
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
GeneratedAt = _timeProvider.GetUtcNow(),
|
||||
Attestations = attestations.ToImmutableArray(),
|
||||
SbomDocuments = sbomDocuments.ToImmutableArray(),
|
||||
VexDocuments = vexDocuments.ToImmutableArray(),
|
||||
@@ -224,7 +226,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
{
|
||||
Pack = pack,
|
||||
ZipPath = zipPath,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddHours(24)
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddHours(24)
|
||||
};
|
||||
|
||||
// Clean up temp directory
|
||||
@@ -246,7 +248,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
Success = true,
|
||||
Pack = pack,
|
||||
DownloadUrl = $"/api/v1/lineage/export/{packId}/download",
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddHours(24),
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddHours(24),
|
||||
SizeBytes = zipInfo.Length,
|
||||
Warnings = warnings.ToImmutableArray()
|
||||
};
|
||||
@@ -268,7 +270,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
string tenantId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (_packCache.TryGetValue(packId, out var cached) && cached.ExpiresAt > DateTimeOffset.UtcNow)
|
||||
if (_packCache.TryGetValue(packId, out var cached) && cached.ExpiresAt > _timeProvider.GetUtcNow())
|
||||
{
|
||||
if (cached.Pack.TenantId == tenantId)
|
||||
{
|
||||
@@ -285,7 +287,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
string tenantId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (_packCache.TryGetValue(packId, out var cached) && cached.ExpiresAt > DateTimeOffset.UtcNow)
|
||||
if (_packCache.TryGetValue(packId, out var cached) && cached.ExpiresAt > _timeProvider.GetUtcNow())
|
||||
{
|
||||
if (cached.Pack.TenantId == tenantId && File.Exists(cached.ZipPath))
|
||||
{
|
||||
@@ -347,7 +349,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
bomFormat = "CycloneDX",
|
||||
specVersion = "1.6",
|
||||
version = 1,
|
||||
metadata = new { timestamp = DateTimeOffset.UtcNow.ToString("O") },
|
||||
metadata = new { timestamp = _timeProvider.GetUtcNow().ToString("O") },
|
||||
components = Array.Empty<object>()
|
||||
}, JsonOptions);
|
||||
|
||||
@@ -383,7 +385,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
dataLicense = "CC0-1.0",
|
||||
name = artifactDigest,
|
||||
documentNamespace = $"https://stellaops.io/spdx/{artifactDigest}",
|
||||
creationInfo = new { created = DateTimeOffset.UtcNow.ToString("O") },
|
||||
creationInfo = new { created = _timeProvider.GetUtcNow().ToString("O") },
|
||||
packages = Array.Empty<object>()
|
||||
}, JsonOptions);
|
||||
|
||||
@@ -418,7 +420,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
context = "https://openvex.dev/ns/v0.2.0",
|
||||
id = $"urn:stellaops:vex:{artifactDigest}",
|
||||
author = "StellaOps",
|
||||
timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
||||
timestamp = _timeProvider.GetUtcNow().ToString("O"),
|
||||
statements = Array.Empty<object>()
|
||||
}, JsonOptions);
|
||||
|
||||
@@ -457,7 +459,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
tenantId,
|
||||
verdict = "pass",
|
||||
policyVersion = "1.0.0",
|
||||
evaluatedAt = DateTimeOffset.UtcNow.ToString("O"),
|
||||
evaluatedAt = _timeProvider.GetUtcNow().ToString("O"),
|
||||
rules = new { total = 0, passed = 0, failed = 0, warned = 0 }
|
||||
}, JsonOptions);
|
||||
|
||||
@@ -477,7 +479,7 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
RulesPassed = 0,
|
||||
RulesFailed = 0,
|
||||
RulesWarned = 0,
|
||||
EvaluatedAt = DateTimeOffset.UtcNow,
|
||||
EvaluatedAt = _timeProvider.GetUtcNow(),
|
||||
FileName = fileName
|
||||
};
|
||||
}
|
||||
@@ -528,9 +530,9 @@ public sealed class LineageEvidencePackService : ILineageEvidencePackService
|
||||
return ComputeHash(combined);
|
||||
}
|
||||
|
||||
private static string ComputeReplayHash(string artifactDigest, string sbomDigest, string merkleRoot)
|
||||
private string ComputeReplayHash(string artifactDigest, string sbomDigest, string merkleRoot)
|
||||
{
|
||||
var input = $"{artifactDigest}|{sbomDigest}|{merkleRoot}|{DateTimeOffset.UtcNow:O}";
|
||||
var input = $"{artifactDigest}|{sbomDigest}|{merkleRoot}|{_timeProvider.GetUtcNow():O}";
|
||||
return $"sha256:{ComputeHash(input)}";
|
||||
}
|
||||
|
||||
|
||||
@@ -149,14 +149,15 @@ public sealed record ExportVerificationResult
|
||||
/// <summary>
|
||||
/// When verification was performed.
|
||||
/// </summary>
|
||||
public DateTimeOffset VerifiedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public required DateTimeOffset VerifiedAt { get; init; }
|
||||
|
||||
public static ExportVerificationResult Failed(Guid runId, params VerificationError[] errors)
|
||||
public static ExportVerificationResult Failed(Guid runId, DateTimeOffset verifiedAt, params VerificationError[] errors)
|
||||
=> new()
|
||||
{
|
||||
Status = VerificationStatus.Invalid,
|
||||
RunId = runId,
|
||||
Errors = errors
|
||||
Errors = errors,
|
||||
VerifiedAt = verifiedAt
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,22 +14,26 @@ public sealed class ExportVerificationService : IExportVerificationService
|
||||
private readonly IExportArtifactStore _artifactStore;
|
||||
private readonly IPackRunAttestationStore? _packRunStore;
|
||||
private readonly ILogger<ExportVerificationService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public ExportVerificationService(
|
||||
IExportArtifactStore artifactStore,
|
||||
ILogger<ExportVerificationService> logger)
|
||||
: this(artifactStore, null, logger)
|
||||
ILogger<ExportVerificationService> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
: this(artifactStore, null, logger, timeProvider)
|
||||
{
|
||||
}
|
||||
|
||||
public ExportVerificationService(
|
||||
IExportArtifactStore artifactStore,
|
||||
IPackRunAttestationStore? packRunStore,
|
||||
ILogger<ExportVerificationService> logger)
|
||||
ILogger<ExportVerificationService> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_artifactStore = artifactStore ?? throw new ArgumentNullException(nameof(artifactStore));
|
||||
_packRunStore = packRunStore;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -52,6 +56,7 @@ public sealed class ExportVerificationService : IExportVerificationService
|
||||
{
|
||||
return ExportVerificationResult.Failed(
|
||||
request.RunId,
|
||||
_timeProvider.GetUtcNow(),
|
||||
new VerificationError
|
||||
{
|
||||
Code = VerificationErrorCodes.ManifestNotFound,
|
||||
@@ -64,6 +69,7 @@ public sealed class ExportVerificationService : IExportVerificationService
|
||||
{
|
||||
return ExportVerificationResult.Failed(
|
||||
request.RunId,
|
||||
_timeProvider.GetUtcNow(),
|
||||
new VerificationError
|
||||
{
|
||||
Code = VerificationErrorCodes.TenantMismatch,
|
||||
@@ -234,7 +240,8 @@ public sealed class ExportVerificationService : IExportVerificationService
|
||||
Encryption = encryptionResult,
|
||||
Attestation = attestationStatus,
|
||||
Errors = errors,
|
||||
Warnings = warnings
|
||||
Warnings = warnings,
|
||||
VerifiedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
||||
private readonly IExceptionApplicationRepository _applicationRepository;
|
||||
private readonly ConcurrentDictionary<string, ReportJob> _jobs = new();
|
||||
private readonly ILogger<ExceptionReportGenerator> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
@@ -30,11 +31,13 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
||||
public ExceptionReportGenerator(
|
||||
IExceptionRepository exceptionRepository,
|
||||
IExceptionApplicationRepository applicationRepository,
|
||||
ILogger<ExceptionReportGenerator> logger)
|
||||
ILogger<ExceptionReportGenerator> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_exceptionRepository = exceptionRepository;
|
||||
_applicationRepository = applicationRepository;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<ExceptionReportJobResponse> CreateReportAsync(
|
||||
@@ -42,7 +45,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var jobId = $"exc-rpt-{Guid.NewGuid():N}";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
var job = new ReportJob
|
||||
{
|
||||
@@ -151,7 +154,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
||||
try
|
||||
{
|
||||
job.Status = "running";
|
||||
job.StartedAt = DateTimeOffset.UtcNow;
|
||||
job.StartedAt = _timeProvider.GetUtcNow();
|
||||
|
||||
var filter = job.Request.Filter ?? new ExceptionFilter
|
||||
{
|
||||
@@ -232,7 +235,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
||||
var document = new ExceptionReportDocument
|
||||
{
|
||||
ReportId = job.JobId,
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
GeneratedAt = _timeProvider.GetUtcNow(),
|
||||
TenantId = job.TenantId,
|
||||
RequesterId = job.RequesterId,
|
||||
Title = job.Request.Title ?? "Exception Report",
|
||||
@@ -289,7 +292,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
||||
job.ContentHash = $"sha256:{Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant()}";
|
||||
job.Progress = 100;
|
||||
job.Status = "completed";
|
||||
job.CompletedAt = DateTimeOffset.UtcNow;
|
||||
job.CompletedAt = _timeProvider.GetUtcNow();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Completed exception report {JobId} with {Count} exceptions, {Size} bytes",
|
||||
@@ -300,7 +303,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator
|
||||
_logger.LogError(ex, "Failed to generate exception report {JobId}", job.JobId);
|
||||
job.Status = "failed";
|
||||
job.ErrorMessage = ex.Message;
|
||||
job.CompletedAt = DateTimeOffset.UtcNow;
|
||||
job.CompletedAt = _timeProvider.GetUtcNow();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@ namespace StellaOps.Findings.Ledger.WebService.Mappings;
|
||||
|
||||
public static class LedgerEventMapping
|
||||
{
|
||||
public static LedgerEventDraft ToDraft(this LedgerEventRequest request)
|
||||
public static LedgerEventDraft ToDraft(this LedgerEventRequest request, TimeProvider? timeProvider = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var recordedAt = (request.RecordedAt ?? DateTimeOffset.UtcNow).ToUniversalTime();
|
||||
timeProvider ??= TimeProvider.System;
|
||||
var recordedAt = (request.RecordedAt ?? timeProvider.GetUtcNow()).ToUniversalTime();
|
||||
var payload = request.Payload is null ? new JsonObject() : (JsonObject)request.Payload.DeepClone();
|
||||
|
||||
var eventObject = new JsonObject
|
||||
|
||||
@@ -1768,6 +1768,7 @@ app.MapPost("/v1/alerts/{alertId}/bundle/verify", async Task<Results<Ok<BundleVe
|
||||
[FromBody] BundleVerificationRequest request,
|
||||
[FromServices] IAlertService alertService,
|
||||
[FromServices] IEvidenceBundleService bundleService,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var alert = await alertService.GetAlertAsync(alertId, cancellationToken).ConfigureAwait(false);
|
||||
@@ -1786,7 +1787,7 @@ app.MapPost("/v1/alerts/{alertId}/bundle/verify", async Task<Results<Ok<BundleVe
|
||||
{
|
||||
AlertId = alertId,
|
||||
IsValid = result.IsValid,
|
||||
VerifiedAt = DateTimeOffset.UtcNow,
|
||||
VerifiedAt = timeProvider.GetUtcNow(),
|
||||
SignatureValid = result.SignatureValid,
|
||||
HashValid = result.HashValid,
|
||||
ChainValid = result.ChainValid,
|
||||
|
||||
@@ -11,13 +11,16 @@ public sealed class EvidenceGraphBuilder : IEvidenceGraphBuilder
|
||||
{
|
||||
private readonly IEvidenceRepository _evidenceRepo;
|
||||
private readonly IAttestationVerifier _attestationVerifier;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public EvidenceGraphBuilder(
|
||||
IEvidenceRepository evidenceRepo,
|
||||
IAttestationVerifier attestationVerifier)
|
||||
IAttestationVerifier attestationVerifier,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_evidenceRepo = evidenceRepo;
|
||||
_attestationVerifier = attestationVerifier;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<EvidenceGraphResponse?> BuildAsync(
|
||||
@@ -126,7 +129,7 @@ public sealed class EvidenceGraphBuilder : IEvidenceGraphBuilder
|
||||
Nodes = nodes,
|
||||
Edges = edges,
|
||||
RootNodeId = verdictNode.Id,
|
||||
GeneratedAt = DateTimeOffset.UtcNow
|
||||
GeneratedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ public sealed class FindingWorkflowService : IFindingWorkflowService
|
||||
|
||||
var payload = CreateBasePayload(request);
|
||||
payload["action"] = "assign";
|
||||
payload["assignee"] = BuildAssigneeNode(request.Assignee);
|
||||
payload["assignee"] = BuildAssigneeNode(request.Assignee!);
|
||||
AddComment(payload, request.Comment);
|
||||
ApplyStatus(payload, request.Status);
|
||||
ApplyAttachments(payload, request.Attachments);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
|
||||
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Persistence/StellaOps.Notify.Persistence.csproj" />
|
||||
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Queue/StellaOps.Notify.Queue.csproj" />
|
||||
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Engine/StellaOps.Notify.Engine.csproj" />
|
||||
<ProjectReference Include="../../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -9,6 +9,12 @@ namespace StellaOps.Notify.Storage.InMemory.Repositories;
|
||||
public sealed class NotifyChannelRepositoryAdapter : INotifyChannelRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, NotifyChannelDocument> _channels = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyChannelRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<NotifyChannelDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -34,7 +40,7 @@ public sealed class NotifyChannelRepositoryAdapter : INotifyChannelRepository
|
||||
|
||||
public Task<NotifyChannelDocument> UpsertAsync(NotifyChannelDocument channel, CancellationToken cancellationToken = default)
|
||||
{
|
||||
channel.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
channel.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
var key = $"{channel.TenantId}:{channel.Id}";
|
||||
_channels[key] = channel;
|
||||
return Task.FromResult(channel);
|
||||
@@ -59,6 +65,12 @@ public sealed class NotifyChannelRepositoryAdapter : INotifyChannelRepository
|
||||
public sealed class NotifyRuleRepositoryAdapter : INotifyRuleRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, NotifyRuleDocument> _rules = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyRuleRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<NotifyRuleDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -83,7 +95,7 @@ public sealed class NotifyRuleRepositoryAdapter : INotifyRuleRepository
|
||||
|
||||
public Task<NotifyRuleDocument> UpsertAsync(NotifyRuleDocument rule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
rule.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
rule.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
var key = $"{rule.TenantId}:{rule.Id}";
|
||||
_rules[key] = rule;
|
||||
return Task.FromResult(rule);
|
||||
@@ -108,6 +120,12 @@ public sealed class NotifyRuleRepositoryAdapter : INotifyRuleRepository
|
||||
public sealed class NotifyTemplateRepositoryAdapter : INotifyTemplateRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, NotifyTemplateDocument> _templates = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyTemplateRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<NotifyTemplateDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -130,7 +148,7 @@ public sealed class NotifyTemplateRepositoryAdapter : INotifyTemplateRepository
|
||||
|
||||
public Task<NotifyTemplateDocument> UpsertAsync(NotifyTemplateDocument template, CancellationToken cancellationToken = default)
|
||||
{
|
||||
template.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
template.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
var key = $"{template.TenantId}:{template.Id}";
|
||||
_templates[key] = template;
|
||||
return Task.FromResult(template);
|
||||
@@ -149,6 +167,12 @@ public sealed class NotifyTemplateRepositoryAdapter : INotifyTemplateRepository
|
||||
public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, NotifyDeliveryDocument> _deliveries = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyDeliveryRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<NotifyDeliveryDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -166,7 +190,7 @@ public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository
|
||||
|
||||
public Task<NotifyDeliveryDocument> UpsertAsync(NotifyDeliveryDocument delivery, CancellationToken cancellationToken = default)
|
||||
{
|
||||
delivery.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
delivery.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
var key = $"{delivery.TenantId}:{delivery.Id}";
|
||||
_deliveries[key] = delivery;
|
||||
return Task.FromResult(delivery);
|
||||
@@ -179,7 +203,7 @@ public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository
|
||||
{
|
||||
doc.Status = status;
|
||||
doc.Error = error;
|
||||
doc.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
doc.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
return Task.FromResult(false);
|
||||
@@ -199,6 +223,12 @@ public sealed class NotifyDeliveryRepositoryAdapter : INotifyDeliveryRepository
|
||||
public sealed class NotifyDigestRepositoryAdapter : INotifyDigestRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, NotifyDigestDocument> _digests = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyDigestRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<NotifyDigestDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -209,7 +239,7 @@ public sealed class NotifyDigestRepositoryAdapter : INotifyDigestRepository
|
||||
|
||||
public Task<NotifyDigestDocument> UpsertAsync(NotifyDigestDocument digest, CancellationToken cancellationToken = default)
|
||||
{
|
||||
digest.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
digest.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
var key = $"{digest.TenantId}:{digest.Id}";
|
||||
_digests[key] = digest;
|
||||
return Task.FromResult(digest);
|
||||
@@ -257,10 +287,16 @@ public sealed class NotifyAuditRepositoryAdapter : INotifyAuditRepository
|
||||
public sealed class NotifyLockRepositoryAdapter : INotifyLockRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, (string Owner, DateTimeOffset ExpiresAt)> _locks = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyLockRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<bool> TryAcquireAsync(string lockKey, string owner, TimeSpan ttl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Clean up expired locks
|
||||
foreach (var key in _locks.Keys.ToList())
|
||||
@@ -288,7 +324,7 @@ public sealed class NotifyLockRepositoryAdapter : INotifyLockRepository
|
||||
{
|
||||
if (_locks.TryGetValue(lockKey, out var value) && value.Owner == owner)
|
||||
{
|
||||
var newExpiry = DateTimeOffset.UtcNow + ttl;
|
||||
var newExpiry = _timeProvider.GetUtcNow() + ttl;
|
||||
_locks[lockKey] = (owner, newExpiry);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
@@ -302,6 +338,12 @@ public sealed class NotifyLockRepositoryAdapter : INotifyLockRepository
|
||||
public sealed class NotifyEscalationPolicyRepositoryAdapter : INotifyEscalationPolicyRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, NotifyEscalationPolicyDocument> _policies = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyEscalationPolicyRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<NotifyEscalationPolicyDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -318,7 +360,7 @@ public sealed class NotifyEscalationPolicyRepositoryAdapter : INotifyEscalationP
|
||||
|
||||
public Task<NotifyEscalationPolicyDocument> UpsertAsync(NotifyEscalationPolicyDocument policy, CancellationToken cancellationToken = default)
|
||||
{
|
||||
policy.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
policy.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
var key = $"{policy.TenantId}:{policy.Id}";
|
||||
_policies[key] = policy;
|
||||
return Task.FromResult(policy);
|
||||
@@ -331,6 +373,12 @@ public sealed class NotifyEscalationPolicyRepositoryAdapter : INotifyEscalationP
|
||||
public sealed class NotifyEscalationStateRepositoryAdapter : INotifyEscalationStateRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, NotifyEscalationStateDocument> _states = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyEscalationStateRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<NotifyEscalationStateDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -341,7 +389,7 @@ public sealed class NotifyEscalationStateRepositoryAdapter : INotifyEscalationSt
|
||||
|
||||
public Task<NotifyEscalationStateDocument> UpsertAsync(NotifyEscalationStateDocument state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
state.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
state.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
var key = $"{state.TenantId}:{state.Id}";
|
||||
_states[key] = state;
|
||||
return Task.FromResult(state);
|
||||
@@ -360,6 +408,12 @@ public sealed class NotifyEscalationStateRepositoryAdapter : INotifyEscalationSt
|
||||
public sealed class NotifyOnCallScheduleRepositoryAdapter : INotifyOnCallScheduleRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, NotifyOnCallScheduleDocument> _schedules = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyOnCallScheduleRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<NotifyOnCallScheduleDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -376,7 +430,7 @@ public sealed class NotifyOnCallScheduleRepositoryAdapter : INotifyOnCallSchedul
|
||||
|
||||
public Task<NotifyOnCallScheduleDocument> UpsertAsync(NotifyOnCallScheduleDocument schedule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
schedule.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
schedule.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
var key = $"{schedule.TenantId}:{schedule.Id}";
|
||||
_schedules[key] = schedule;
|
||||
return Task.FromResult(schedule);
|
||||
@@ -397,6 +451,12 @@ public sealed class NotifyOnCallScheduleRepositoryAdapter : INotifyOnCallSchedul
|
||||
public sealed class NotifyQuietHoursRepositoryAdapter : INotifyQuietHoursRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, NotifyQuietHoursDocument> _quietHours = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyQuietHoursRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<NotifyQuietHoursDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -413,7 +473,7 @@ public sealed class NotifyQuietHoursRepositoryAdapter : INotifyQuietHoursReposit
|
||||
|
||||
public Task<NotifyQuietHoursDocument> UpsertAsync(NotifyQuietHoursDocument quietHours, CancellationToken cancellationToken = default)
|
||||
{
|
||||
quietHours.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
quietHours.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
var key = $"{quietHours.TenantId}:{quietHours.Id}";
|
||||
_quietHours[key] = quietHours;
|
||||
return Task.FromResult(quietHours);
|
||||
@@ -432,6 +492,12 @@ public sealed class NotifyQuietHoursRepositoryAdapter : INotifyQuietHoursReposit
|
||||
public sealed class NotifyMaintenanceWindowRepositoryAdapter : INotifyMaintenanceWindowRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, NotifyMaintenanceWindowDocument> _windows = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyMaintenanceWindowRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<NotifyMaintenanceWindowDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -454,7 +520,7 @@ public sealed class NotifyMaintenanceWindowRepositoryAdapter : INotifyMaintenanc
|
||||
|
||||
public Task<NotifyMaintenanceWindowDocument> UpsertAsync(NotifyMaintenanceWindowDocument window, CancellationToken cancellationToken = default)
|
||||
{
|
||||
window.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
window.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
var key = $"{window.TenantId}:{window.Id}";
|
||||
_windows[key] = window;
|
||||
return Task.FromResult(window);
|
||||
@@ -473,6 +539,12 @@ public sealed class NotifyMaintenanceWindowRepositoryAdapter : INotifyMaintenanc
|
||||
public sealed class NotifyInboxRepositoryAdapter : INotifyInboxRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, NotifyInboxDocument> _inbox = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotifyInboxRepositoryAdapter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<NotifyInboxDocument?> GetByIdAsync(string tenantId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -502,7 +574,7 @@ public sealed class NotifyInboxRepositoryAdapter : INotifyInboxRepository
|
||||
if (_inbox.TryGetValue(key, out var doc))
|
||||
{
|
||||
doc.Read = true;
|
||||
doc.ReadAt = DateTimeOffset.UtcNow;
|
||||
doc.ReadAt = _timeProvider.GetUtcNow();
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
return Task.FromResult(false);
|
||||
|
||||
@@ -16,6 +16,7 @@ public sealed class LedgerExporter : ILedgerExporter
|
||||
private readonly ILedgerRepository _ledgerRepository;
|
||||
private readonly ILedgerExportRepository _exportRepository;
|
||||
private readonly ILogger<LedgerExporter> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
@@ -32,11 +33,13 @@ public sealed class LedgerExporter : ILedgerExporter
|
||||
public LedgerExporter(
|
||||
ILedgerRepository ledgerRepository,
|
||||
ILedgerExportRepository exportRepository,
|
||||
ILogger<LedgerExporter> logger)
|
||||
ILogger<LedgerExporter> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_ledgerRepository = ledgerRepository;
|
||||
_exportRepository = exportRepository;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -44,7 +47,7 @@ public sealed class LedgerExporter : ILedgerExporter
|
||||
LedgerExport export,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var startTime = DateTimeOffset.UtcNow;
|
||||
var startTime = _timeProvider.GetUtcNow();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -83,7 +86,7 @@ public sealed class LedgerExporter : ILedgerExporter
|
||||
export = export.Complete(outputUri, digest, sizeBytes, entries.Count);
|
||||
export = await _exportRepository.UpdateAsync(export, cancellationToken);
|
||||
|
||||
var duration = DateTimeOffset.UtcNow - startTime;
|
||||
var duration = _timeProvider.GetUtcNow() - startTime;
|
||||
OrchestratorMetrics.LedgerExportCompleted(export.TenantId, export.Format);
|
||||
OrchestratorMetrics.RecordLedgerExportDuration(export.TenantId, export.Format, duration.TotalSeconds);
|
||||
OrchestratorMetrics.RecordLedgerExportSize(export.TenantId, export.Format, sizeBytes);
|
||||
@@ -165,12 +168,12 @@ public sealed class LedgerExporter : ILedgerExporter
|
||||
return (content, digest);
|
||||
}
|
||||
|
||||
private static string GenerateJson(IReadOnlyList<RunLedgerEntry> entries)
|
||||
private string GenerateJson(IReadOnlyList<RunLedgerEntry> entries)
|
||||
{
|
||||
var exportData = new LedgerExportData
|
||||
{
|
||||
SchemaVersion = "1.0.0",
|
||||
ExportedAt = DateTimeOffset.UtcNow,
|
||||
ExportedAt = _timeProvider.GetUtcNow(),
|
||||
EntryCount = entries.Count,
|
||||
Entries = entries.Select(MapEntry).ToList()
|
||||
};
|
||||
|
||||
@@ -56,15 +56,18 @@ public sealed class PostgresDuplicateSuppressor : IDuplicateSuppressor
|
||||
private readonly OrchestratorDataSource _dataSource;
|
||||
private readonly string _tenantId;
|
||||
private readonly ILogger<PostgresDuplicateSuppressor> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public PostgresDuplicateSuppressor(
|
||||
OrchestratorDataSource dataSource,
|
||||
string tenantId,
|
||||
ILogger<PostgresDuplicateSuppressor> logger)
|
||||
ILogger<PostgresDuplicateSuppressor> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<bool> HasProcessedAsync(string scopeKey, string eventKey, CancellationToken cancellationToken)
|
||||
@@ -125,7 +128,7 @@ public sealed class PostgresDuplicateSuppressor : IDuplicateSuppressor
|
||||
command.Parameters.AddWithValue("event_key", eventKey);
|
||||
command.Parameters.AddWithValue("event_time", eventTime);
|
||||
command.Parameters.AddWithValue("batch_id", (object?)batchId ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("expires_at", DateTimeOffset.UtcNow + ttl);
|
||||
command.Parameters.AddWithValue("expires_at", _timeProvider.GetUtcNow() + ttl);
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -143,7 +146,7 @@ public sealed class PostgresDuplicateSuppressor : IDuplicateSuppressor
|
||||
return;
|
||||
}
|
||||
|
||||
var expiresAt = DateTimeOffset.UtcNow + ttl;
|
||||
var expiresAt = _timeProvider.GetUtcNow() + ttl;
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(_tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -108,13 +108,16 @@ public sealed class PostgresJobRepository : IJobRepository
|
||||
|
||||
private readonly OrchestratorDataSource _dataSource;
|
||||
private readonly ILogger<PostgresJobRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public PostgresJobRepository(
|
||||
OrchestratorDataSource dataSource,
|
||||
ILogger<PostgresJobRepository> logger)
|
||||
ILogger<PostgresJobRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<Job?> GetByIdAsync(string tenantId, Guid jobId, CancellationToken cancellationToken)
|
||||
@@ -228,8 +231,8 @@ public sealed class PostgresJobRepository : IJobRepository
|
||||
command.Parameters.AddWithValue("lease_id", leaseId);
|
||||
command.Parameters.AddWithValue("worker_id", workerId);
|
||||
command.Parameters.AddWithValue("lease_until", leaseUntil);
|
||||
command.Parameters.AddWithValue("leased_at", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("leased_at", _timeProvider.GetUtcNow());
|
||||
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||
|
||||
if (jobType != null)
|
||||
{
|
||||
@@ -263,7 +266,7 @@ public sealed class PostgresJobRepository : IJobRepository
|
||||
command.Parameters.AddWithValue("job_id", jobId);
|
||||
command.Parameters.AddWithValue("lease_id", leaseId);
|
||||
command.Parameters.AddWithValue("new_lease_until", newLeaseUntil);
|
||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||
|
||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
|
||||
@@ -13,6 +13,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository
|
||||
{
|
||||
private readonly OrchestratorDataSource _dataSource;
|
||||
private readonly ILogger<PostgresPackRegistryRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private const string PackColumns = """
|
||||
pack_id, tenant_id, project_id, name, display_name, description,
|
||||
@@ -33,10 +34,12 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository
|
||||
|
||||
public PostgresPackRegistryRepository(
|
||||
OrchestratorDataSource dataSource,
|
||||
ILogger<PostgresPackRegistryRepository> logger)
|
||||
ILogger<PostgresPackRegistryRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
// Pack CRUD
|
||||
@@ -264,7 +267,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("pack_id", packId);
|
||||
command.Parameters.AddWithValue("status", status.ToString().ToLowerInvariant());
|
||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow.UtcDateTime);
|
||||
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow().UtcDateTime);
|
||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||
command.Parameters.AddWithValue("published_at", (object?)publishedAt?.UtcDateTime ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("published_by", (object?)publishedBy ?? DBNull.Value);
|
||||
@@ -534,7 +537,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("pack_version_id", packVersionId);
|
||||
command.Parameters.AddWithValue("status", status.ToString().ToLowerInvariant());
|
||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow.UtcDateTime);
|
||||
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow().UtcDateTime);
|
||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||
command.Parameters.AddWithValue("published_at", (object?)publishedAt?.UtcDateTime ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("published_by", (object?)publishedBy ?? DBNull.Value);
|
||||
@@ -574,7 +577,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository
|
||||
command.Parameters.AddWithValue("signature_algorithm", signatureAlgorithm);
|
||||
command.Parameters.AddWithValue("signed_by", signedBy);
|
||||
command.Parameters.AddWithValue("signed_at", signedAt.UtcDateTime);
|
||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow.UtcDateTime);
|
||||
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow().UtcDateTime);
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -128,11 +128,16 @@ public sealed class PostgresPackRunRepository : IPackRunRepository
|
||||
|
||||
private readonly OrchestratorDataSource _dataSource;
|
||||
private readonly ILogger<PostgresPackRunRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public PostgresPackRunRepository(OrchestratorDataSource dataSource, ILogger<PostgresPackRunRepository> logger)
|
||||
public PostgresPackRunRepository(
|
||||
OrchestratorDataSource dataSource,
|
||||
ILogger<PostgresPackRunRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<PackRun?> GetByIdAsync(string tenantId, Guid packRunId, CancellationToken cancellationToken)
|
||||
@@ -244,7 +249,7 @@ public sealed class PostgresPackRunRepository : IPackRunRepository
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(sql, connection);
|
||||
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("lease_id", leaseId);
|
||||
command.Parameters.AddWithValue("task_runner_id", taskRunnerId);
|
||||
@@ -275,7 +280,7 @@ public sealed class PostgresPackRunRepository : IPackRunRepository
|
||||
command.Parameters.AddWithValue("pack_run_id", packRunId);
|
||||
command.Parameters.AddWithValue("lease_id", leaseId);
|
||||
command.Parameters.AddWithValue("new_lease_until", newLeaseUntil);
|
||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||
|
||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
@@ -292,7 +297,7 @@ public sealed class PostgresPackRunRepository : IPackRunRepository
|
||||
command.Parameters.AddWithValue("lease_id", leaseId);
|
||||
command.Parameters.AddWithValue("status", StatusToString(newStatus));
|
||||
command.Parameters.AddWithValue("reason", (object?)reason ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("completed_at", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("completed_at", _timeProvider.GetUtcNow());
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -113,13 +113,16 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
||||
|
||||
private readonly OrchestratorDataSource _dataSource;
|
||||
private readonly ILogger<PostgresQuotaRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public PostgresQuotaRepository(
|
||||
OrchestratorDataSource dataSource,
|
||||
ILogger<PostgresQuotaRepository> logger)
|
||||
ILogger<PostgresQuotaRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<Quota?> GetByIdAsync(string tenantId, Guid quotaId, CancellationToken cancellationToken)
|
||||
@@ -229,7 +232,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
||||
command.Parameters.AddWithValue("current_active", currentActive);
|
||||
command.Parameters.AddWithValue("current_hour_count", currentHourCount);
|
||||
command.Parameters.AddWithValue("current_hour_start", currentHourStart);
|
||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -245,7 +248,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
||||
command.Parameters.AddWithValue("quota_id", quotaId);
|
||||
command.Parameters.AddWithValue("pause_reason", reason);
|
||||
command.Parameters.AddWithValue("quota_ticket", (object?)ticket ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||
|
||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -263,7 +266,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
||||
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("quota_id", quotaId);
|
||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||
|
||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -281,7 +284,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
||||
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("quota_id", quotaId);
|
||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -294,7 +297,7 @@ public sealed class PostgresQuotaRepository : IQuotaRepository
|
||||
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("quota_id", quotaId);
|
||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -69,13 +69,16 @@ public sealed class PostgresRunRepository : IRunRepository
|
||||
|
||||
private readonly OrchestratorDataSource _dataSource;
|
||||
private readonly ILogger<PostgresRunRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public PostgresRunRepository(
|
||||
OrchestratorDataSource dataSource,
|
||||
ILogger<PostgresRunRepository> logger)
|
||||
ILogger<PostgresRunRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<Run?> GetByIdAsync(string tenantId, Guid runId, CancellationToken cancellationToken)
|
||||
@@ -149,7 +152,7 @@ public sealed class PostgresRunRepository : IRunRepository
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("run_id", runId);
|
||||
command.Parameters.AddWithValue("succeeded", succeeded);
|
||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
|
||||
@@ -74,13 +74,16 @@ public sealed class PostgresSourceRepository : ISourceRepository
|
||||
|
||||
private readonly OrchestratorDataSource _dataSource;
|
||||
private readonly ILogger<PostgresSourceRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public PostgresSourceRepository(
|
||||
OrchestratorDataSource dataSource,
|
||||
ILogger<PostgresSourceRepository> logger)
|
||||
ILogger<PostgresSourceRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<Source?> GetByIdAsync(string tenantId, Guid sourceId, CancellationToken cancellationToken)
|
||||
@@ -175,7 +178,7 @@ public sealed class PostgresSourceRepository : ISourceRepository
|
||||
command.Parameters.AddWithValue("source_id", sourceId);
|
||||
command.Parameters.AddWithValue("pause_reason", reason);
|
||||
command.Parameters.AddWithValue("pause_ticket", (object?)ticket ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||
|
||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -193,7 +196,7 @@ public sealed class PostgresSourceRepository : ISourceRepository
|
||||
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("source_id", sourceId);
|
||||
command.Parameters.AddWithValue("updated_at", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("updated_at", _timeProvider.GetUtcNow());
|
||||
command.Parameters.AddWithValue("updated_by", updatedBy);
|
||||
|
||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -77,13 +77,16 @@ public sealed class PostgresThrottleRepository : IThrottleRepository
|
||||
|
||||
private readonly OrchestratorDataSource _dataSource;
|
||||
private readonly ILogger<PostgresThrottleRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public PostgresThrottleRepository(
|
||||
OrchestratorDataSource dataSource,
|
||||
ILogger<PostgresThrottleRepository> logger)
|
||||
ILogger<PostgresThrottleRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<Throttle?> GetByIdAsync(string tenantId, Guid throttleId, CancellationToken cancellationToken)
|
||||
@@ -110,7 +113,7 @@ public sealed class PostgresThrottleRepository : IThrottleRepository
|
||||
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("source_id", sourceId);
|
||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
var throttles = new List<Throttle>();
|
||||
@@ -128,7 +131,7 @@ public sealed class PostgresThrottleRepository : IThrottleRepository
|
||||
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("job_type", jobType);
|
||||
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
|
||||
command.Parameters.AddWithValue("now", _timeProvider.GetUtcNow());
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
var throttles = new List<Throttle>();
|
||||
|
||||
@@ -100,13 +100,16 @@ public sealed class PostgresWatermarkRepository : IWatermarkRepository
|
||||
|
||||
private readonly OrchestratorDataSource _dataSource;
|
||||
private readonly ILogger<PostgresWatermarkRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public PostgresWatermarkRepository(
|
||||
OrchestratorDataSource dataSource,
|
||||
ILogger<PostgresWatermarkRepository> logger)
|
||||
ILogger<PostgresWatermarkRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<Watermark?> GetByScopeKeyAsync(string tenantId, string scopeKey, CancellationToken cancellationToken)
|
||||
@@ -271,7 +274,7 @@ public sealed class PostgresWatermarkRepository : IWatermarkRepository
|
||||
int limit,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var thresholdTime = DateTimeOffset.UtcNow - lagThreshold;
|
||||
var thresholdTime = _timeProvider.GetUtcNow() - lagThreshold;
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(SelectLaggingSql, connection);
|
||||
|
||||
@@ -152,7 +152,8 @@ public sealed record BackfillCheckpoint(
|
||||
int batchNumber,
|
||||
DateTimeOffset batchStart,
|
||||
DateTimeOffset batchEnd,
|
||||
int eventsInBatch)
|
||||
int eventsInBatch,
|
||||
DateTimeOffset startedAt)
|
||||
{
|
||||
return new BackfillCheckpoint(
|
||||
CheckpointId: Guid.NewGuid(),
|
||||
@@ -166,7 +167,7 @@ public sealed record BackfillCheckpoint(
|
||||
EventsSkipped: 0,
|
||||
EventsFailed: 0,
|
||||
BatchHash: null,
|
||||
StartedAt: DateTimeOffset.UtcNow,
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: null,
|
||||
ErrorMessage: null);
|
||||
}
|
||||
@@ -174,7 +175,7 @@ public sealed record BackfillCheckpoint(
|
||||
/// <summary>
|
||||
/// Marks the checkpoint as complete.
|
||||
/// </summary>
|
||||
public BackfillCheckpoint Complete(int processed, int skipped, int failed, string? batchHash)
|
||||
public BackfillCheckpoint Complete(int processed, int skipped, int failed, string? batchHash, DateTimeOffset completedAt)
|
||||
{
|
||||
return this with
|
||||
{
|
||||
@@ -182,18 +183,18 @@ public sealed record BackfillCheckpoint(
|
||||
EventsSkipped = skipped,
|
||||
EventsFailed = failed,
|
||||
BatchHash = batchHash,
|
||||
CompletedAt = DateTimeOffset.UtcNow
|
||||
CompletedAt = completedAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the checkpoint as failed.
|
||||
/// </summary>
|
||||
public BackfillCheckpoint Fail(string error)
|
||||
public BackfillCheckpoint Fail(string error, DateTimeOffset completedAt)
|
||||
{
|
||||
return this with
|
||||
{
|
||||
CompletedAt = DateTimeOffset.UtcNow,
|
||||
CompletedAt = completedAt,
|
||||
ErrorMessage = error
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,15 +14,18 @@ public sealed class FirstSignalSnapshotWriter : BackgroundService
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly FirstSignalSnapshotWriterOptions _options;
|
||||
private readonly ILogger<FirstSignalSnapshotWriter> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public FirstSignalSnapshotWriter(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<FirstSignalOptions> options,
|
||||
ILogger<FirstSignalSnapshotWriter> logger)
|
||||
ILogger<FirstSignalSnapshotWriter> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value.SnapshotWriter;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
@@ -76,7 +79,7 @@ public sealed class FirstSignalSnapshotWriter : BackgroundService
|
||||
var runRepository = scope.ServiceProvider.GetRequiredService<IRunRepository>();
|
||||
var firstSignalService = scope.ServiceProvider.GetRequiredService<CoreServices.IFirstSignalService>();
|
||||
|
||||
var createdAfter = DateTimeOffset.UtcNow.Subtract(lookback);
|
||||
var createdAfter = _timeProvider.GetUtcNow().Subtract(lookback);
|
||||
|
||||
var pending = await runRepository.ListAsync(
|
||||
tenantId,
|
||||
|
||||
@@ -36,13 +36,14 @@ public static class HealthEndpoints
|
||||
return app;
|
||||
}
|
||||
|
||||
private static IResult GetHealth()
|
||||
private static IResult GetHealth([FromServices] TimeProvider timeProvider)
|
||||
{
|
||||
return Results.Ok(new HealthResponse("ok", DateTimeOffset.UtcNow));
|
||||
return Results.Ok(new HealthResponse("ok", timeProvider.GetUtcNow()));
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetReadiness(
|
||||
[FromServices] OrchestratorDataSource dataSource,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -53,14 +54,14 @@ public static class HealthEndpoints
|
||||
if (!dbHealthy)
|
||||
{
|
||||
return Results.Json(
|
||||
new ReadinessResponse("not_ready", DateTimeOffset.UtcNow, new Dictionary<string, string>
|
||||
new ReadinessResponse("not_ready", timeProvider.GetUtcNow(), new Dictionary<string, string>
|
||||
{
|
||||
["database"] = "unhealthy"
|
||||
}),
|
||||
statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
return Results.Ok(new ReadinessResponse("ready", DateTimeOffset.UtcNow, new Dictionary<string, string>
|
||||
return Results.Ok(new ReadinessResponse("ready", timeProvider.GetUtcNow(), new Dictionary<string, string>
|
||||
{
|
||||
["database"] = "healthy"
|
||||
}));
|
||||
@@ -68,7 +69,7 @@ public static class HealthEndpoints
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.Json(
|
||||
new ReadinessResponse("not_ready", DateTimeOffset.UtcNow, new Dictionary<string, string>
|
||||
new ReadinessResponse("not_ready", timeProvider.GetUtcNow(), new Dictionary<string, string>
|
||||
{
|
||||
["database"] = $"error: {ex.Message}"
|
||||
}),
|
||||
@@ -76,14 +77,15 @@ public static class HealthEndpoints
|
||||
}
|
||||
}
|
||||
|
||||
private static IResult GetLiveness()
|
||||
private static IResult GetLiveness([FromServices] TimeProvider timeProvider)
|
||||
{
|
||||
// Liveness just checks the process is alive
|
||||
return Results.Ok(new HealthResponse("alive", DateTimeOffset.UtcNow));
|
||||
return Results.Ok(new HealthResponse("alive", timeProvider.GetUtcNow()));
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetHealthDetails(
|
||||
[FromServices] OrchestratorDataSource dataSource,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var checks = new Dictionary<string, HealthCheckResult>();
|
||||
@@ -96,12 +98,12 @@ public static class HealthEndpoints
|
||||
checks["database"] = new HealthCheckResult(
|
||||
dbHealthy ? "healthy" : "unhealthy",
|
||||
dbHealthy ? null : "Connection test failed",
|
||||
DateTimeOffset.UtcNow);
|
||||
timeProvider.GetUtcNow());
|
||||
overallHealthy &= dbHealthy;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
checks["database"] = new HealthCheckResult("unhealthy", ex.Message, DateTimeOffset.UtcNow);
|
||||
checks["database"] = new HealthCheckResult("unhealthy", ex.Message, timeProvider.GetUtcNow());
|
||||
overallHealthy = false;
|
||||
}
|
||||
|
||||
@@ -114,7 +116,7 @@ public static class HealthEndpoints
|
||||
checks["memory"] = new HealthCheckResult(
|
||||
memoryHealthy ? "healthy" : "degraded",
|
||||
$"Used: {memoryUsedMb:F2} MB",
|
||||
DateTimeOffset.UtcNow);
|
||||
timeProvider.GetUtcNow());
|
||||
|
||||
// Thread pool check
|
||||
ThreadPool.GetAvailableThreads(out var workerThreads, out var completionPortThreads);
|
||||
@@ -124,11 +126,11 @@ public static class HealthEndpoints
|
||||
checks["threadPool"] = new HealthCheckResult(
|
||||
threadPoolHealthy ? "healthy" : "degraded",
|
||||
$"Worker threads available: {workerThreads}/{maxWorkerThreads}",
|
||||
DateTimeOffset.UtcNow);
|
||||
timeProvider.GetUtcNow());
|
||||
|
||||
var response = new HealthDetailsResponse(
|
||||
overallHealthy ? "healthy" : "unhealthy",
|
||||
DateTimeOffset.UtcNow,
|
||||
timeProvider.GetUtcNow(),
|
||||
checks);
|
||||
|
||||
return overallHealthy
|
||||
|
||||
@@ -55,10 +55,12 @@ public static class KpiEndpoints
|
||||
[FromQuery] DateTimeOffset? to,
|
||||
[FromQuery] string? tenant,
|
||||
[FromServices] IKpiCollector collector,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var start = from ?? DateTimeOffset.UtcNow.AddDays(-7);
|
||||
var end = to ?? DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var start = from ?? now.AddDays(-7);
|
||||
var end = to ?? now;
|
||||
|
||||
var kpis = await collector.CollectAsync(start, end, tenant, ct);
|
||||
return Results.Ok(kpis);
|
||||
@@ -69,11 +71,13 @@ public static class KpiEndpoints
|
||||
[FromQuery] DateTimeOffset? to,
|
||||
[FromQuery] string? tenant,
|
||||
[FromServices] IKpiCollector collector,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var kpis = await collector.CollectAsync(
|
||||
from ?? DateTimeOffset.UtcNow.AddDays(-7),
|
||||
to ?? DateTimeOffset.UtcNow,
|
||||
from ?? now.AddDays(-7),
|
||||
to ?? now,
|
||||
tenant,
|
||||
ct);
|
||||
return Results.Ok(kpis.Reachability);
|
||||
@@ -84,11 +88,13 @@ public static class KpiEndpoints
|
||||
[FromQuery] DateTimeOffset? to,
|
||||
[FromQuery] string? tenant,
|
||||
[FromServices] IKpiCollector collector,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var kpis = await collector.CollectAsync(
|
||||
from ?? DateTimeOffset.UtcNow.AddDays(-7),
|
||||
to ?? DateTimeOffset.UtcNow,
|
||||
from ?? now.AddDays(-7),
|
||||
to ?? now,
|
||||
tenant,
|
||||
ct);
|
||||
return Results.Ok(kpis.Explainability);
|
||||
@@ -99,11 +105,13 @@ public static class KpiEndpoints
|
||||
[FromQuery] DateTimeOffset? to,
|
||||
[FromQuery] string? tenant,
|
||||
[FromServices] IKpiCollector collector,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var kpis = await collector.CollectAsync(
|
||||
from ?? DateTimeOffset.UtcNow.AddDays(-7),
|
||||
to ?? DateTimeOffset.UtcNow,
|
||||
from ?? now.AddDays(-7),
|
||||
to ?? now,
|
||||
tenant,
|
||||
ct);
|
||||
return Results.Ok(kpis.Runtime);
|
||||
@@ -114,11 +122,13 @@ public static class KpiEndpoints
|
||||
[FromQuery] DateTimeOffset? to,
|
||||
[FromQuery] string? tenant,
|
||||
[FromServices] IKpiCollector collector,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var kpis = await collector.CollectAsync(
|
||||
from ?? DateTimeOffset.UtcNow.AddDays(-7),
|
||||
to ?? DateTimeOffset.UtcNow,
|
||||
from ?? now.AddDays(-7),
|
||||
to ?? now,
|
||||
tenant,
|
||||
ct);
|
||||
return Results.Ok(kpis.Replay);
|
||||
|
||||
@@ -375,8 +375,8 @@ app.MapConflictsApi();
|
||||
|
||||
app.Run();
|
||||
|
||||
// Make Program class partial to allow integration testing while keeping it minimal
|
||||
// Make Program class internal to prevent type conflicts when referencing this assembly
|
||||
namespace StellaOps.Policy.Engine
|
||||
{
|
||||
public partial class Program { }
|
||||
internal partial class Program { }
|
||||
}
|
||||
|
||||
@@ -6,12 +6,18 @@ namespace StellaOps.Policy.Engine.Services;
|
||||
internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, PolicyPackRecord> packs = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryPolicyPackRepository(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<PolicyPackRecord> CreateAsync(string packId, string? displayName, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(packId);
|
||||
|
||||
var created = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, displayName, DateTimeOffset.UtcNow));
|
||||
var created = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, displayName, _timeProvider.GetUtcNow()));
|
||||
return Task.FromResult(created);
|
||||
}
|
||||
|
||||
@@ -25,15 +31,15 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
|
||||
|
||||
public Task<PolicyRevisionRecord> UpsertRevisionAsync(string packId, int version, bool requiresTwoPersonApproval, PolicyRevisionStatus initialStatus, CancellationToken cancellationToken)
|
||||
{
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, DateTimeOffset.UtcNow));
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, _timeProvider.GetUtcNow()));
|
||||
int revisionVersion = version > 0 ? version : pack.GetNextVersion();
|
||||
var revision = pack.GetOrAddRevision(
|
||||
revisionVersion,
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPersonApproval, initialStatus, DateTimeOffset.UtcNow));
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPersonApproval, initialStatus, _timeProvider.GetUtcNow()));
|
||||
|
||||
if (revision.Status != initialStatus)
|
||||
{
|
||||
revision.SetStatus(initialStatus, DateTimeOffset.UtcNow);
|
||||
revision.SetStatus(initialStatus, _timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
return Task.FromResult(revision);
|
||||
@@ -95,9 +101,9 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bundle);
|
||||
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, DateTimeOffset.UtcNow));
|
||||
var pack = packs.GetOrAdd(packId, id => new PolicyPackRecord(id, null, _timeProvider.GetUtcNow()));
|
||||
var revision = pack.GetOrAddRevision(version > 0 ? version : pack.GetNextVersion(),
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPerson: false, status: PolicyRevisionStatus.Draft, DateTimeOffset.UtcNow));
|
||||
v => new PolicyRevisionRecord(v, requiresTwoPerson: false, status: PolicyRevisionStatus.Draft, _timeProvider.GetUtcNow()));
|
||||
|
||||
revision.SetBundle(bundle);
|
||||
return Task.FromResult(bundle);
|
||||
|
||||
@@ -13,6 +13,12 @@ namespace StellaOps.Policy.Engine.Storage.InMemory;
|
||||
public sealed class InMemoryExceptionRepository : IExceptionRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<(string Tenant, Guid Id), ExceptionEntity> _exceptions = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryExceptionRepository(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<ExceptionEntity> CreateAsync(ExceptionEntity exception, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -123,7 +129,7 @@ public sealed class InMemoryExceptionRepository : IExceptionRepository
|
||||
_exceptions[key] = Copy(
|
||||
existing,
|
||||
statusOverride: ExceptionStatus.Revoked,
|
||||
revokedAtOverride: DateTimeOffset.UtcNow,
|
||||
revokedAtOverride: _timeProvider.GetUtcNow(),
|
||||
revokedByOverride: revokedBy);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
@@ -133,7 +139,7 @@ public sealed class InMemoryExceptionRepository : IExceptionRepository
|
||||
|
||||
public Task<int> ExpireAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var normalizedTenant = Normalize(tenantId);
|
||||
var expired = 0;
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ public static class ExceptionApprovalEndpoints
|
||||
CreateApprovalRequestDto request,
|
||||
IExceptionApprovalRepository repository,
|
||||
IExceptionApprovalRulesService rulesService,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<ExceptionApprovalRequestEntity> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -110,7 +111,7 @@ public static class ExceptionApprovalEndpoints
|
||||
}
|
||||
|
||||
// Generate request ID
|
||||
var requestId = $"EAR-{DateTimeOffset.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
var requestId = $"EAR-{timeProvider.GetUtcNow():yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
|
||||
// Parse gate level
|
||||
if (!Enum.TryParse<GateLevel>(request.GateLevel, ignoreCase: true, out var gateLevel))
|
||||
@@ -139,7 +140,7 @@ public static class ExceptionApprovalEndpoints
|
||||
});
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var entity = new ExceptionApprovalRequestEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
|
||||
@@ -134,6 +134,7 @@ public static class ExceptionEndpoints
|
||||
CreateExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (request is null)
|
||||
@@ -146,7 +147,7 @@ public static class ExceptionEndpoints
|
||||
}
|
||||
|
||||
// Validate expiry is in future
|
||||
if (request.ExpiresAt <= DateTimeOffset.UtcNow)
|
||||
if (request.ExpiresAt <= timeProvider.GetUtcNow())
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
@@ -157,7 +158,7 @@ public static class ExceptionEndpoints
|
||||
}
|
||||
|
||||
// Validate expiry is not more than 1 year
|
||||
if (request.ExpiresAt > DateTimeOffset.UtcNow.AddYears(1))
|
||||
if (request.ExpiresAt > timeProvider.GetUtcNow().AddYears(1))
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
@@ -188,8 +189,8 @@ public static class ExceptionEndpoints
|
||||
},
|
||||
OwnerId = request.OwnerId,
|
||||
RequesterId = actorId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
CreatedAt = timeProvider.GetUtcNow(),
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
ReasonCode = ParseReasonRequired(request.ReasonCode),
|
||||
Rationale = request.Rationale,
|
||||
@@ -210,6 +211,7 @@ public static class ExceptionEndpoints
|
||||
UpdateExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -238,7 +240,7 @@ public static class ExceptionEndpoints
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
Rationale = request.Rationale ?? existing.Rationale,
|
||||
EvidenceRefs = request.EvidenceRefs?.ToImmutableArray() ?? existing.EvidenceRefs,
|
||||
CompensatingControls = request.CompensatingControls?.ToImmutableArray() ?? existing.CompensatingControls,
|
||||
@@ -258,6 +260,7 @@ public static class ExceptionEndpoints
|
||||
ApproveExceptionRequest? request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -294,8 +297,8 @@ public static class ExceptionEndpoints
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Approved,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
ApprovedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
ApprovedAt = timeProvider.GetUtcNow(),
|
||||
ApproverIds = existing.ApproverIds.Add(actorId)
|
||||
};
|
||||
|
||||
@@ -310,6 +313,7 @@ public static class ExceptionEndpoints
|
||||
string id,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -335,7 +339,7 @@ public static class ExceptionEndpoints
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Active,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
UpdatedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
var result = await repository.UpdateAsync(
|
||||
@@ -350,6 +354,7 @@ public static class ExceptionEndpoints
|
||||
ExtendExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -384,7 +389,7 @@ public static class ExceptionEndpoints
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = timeProvider.GetUtcNow(),
|
||||
ExpiresAt = request.NewExpiresAt
|
||||
};
|
||||
|
||||
@@ -400,6 +405,7 @@ public static class ExceptionEndpoints
|
||||
[FromBody] RevokeExceptionRequest? request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
@@ -425,7 +431,7 @@ public static class ExceptionEndpoints
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Revoked,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
UpdatedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
var result = await repository.UpdateAsync(
|
||||
|
||||
@@ -39,6 +39,7 @@ public static class GateEndpoints
|
||||
IBaselineSelector baselineSelector,
|
||||
IGateBypassAuditor bypassAuditor,
|
||||
IMemoryCache cache,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<DriftGateEvaluator> logger,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
@@ -79,12 +80,12 @@ public static class GateEndpoints
|
||||
|
||||
return Results.Ok(new GateEvaluateResponse
|
||||
{
|
||||
DecisionId = $"gate:{DateTimeOffset.UtcNow:yyyyMMddHHmmss}:{Guid.NewGuid():N}",
|
||||
DecisionId = $"gate:{timeProvider.GetUtcNow():yyyyMMddHHmmss}:{Guid.NewGuid():N}",
|
||||
Status = GateStatus.Pass,
|
||||
ExitCode = GateExitCodes.Pass,
|
||||
ImageDigest = request.ImageDigest,
|
||||
BaselineRef = request.BaselineRef,
|
||||
DecidedAt = DateTimeOffset.UtcNow,
|
||||
DecidedAt = timeProvider.GetUtcNow(),
|
||||
Summary = "First build - no baseline for comparison",
|
||||
Advisory = "This appears to be a first build. Future builds will be compared against this baseline."
|
||||
});
|
||||
@@ -224,7 +225,8 @@ public static class GateEndpoints
|
||||
.WithDescription("Retrieve a previous gate evaluation decision by ID");
|
||||
|
||||
// GET /api/v1/policy/gate/health - Health check for gate service
|
||||
gates.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTimeOffset.UtcNow }))
|
||||
gates.MapGet("/health", ([FromServices] TimeProvider timeProvider) =>
|
||||
Results.Ok(new { status = "healthy", timestamp = timeProvider.GetUtcNow() }))
|
||||
.WithName("GateHealth")
|
||||
.WithDescription("Health check for the gate evaluation service");
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ public static class GovernanceEndpoints
|
||||
|
||||
private static Task<IResult> GetSealedModeStatusAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
[FromQuery] string? tenantId)
|
||||
{
|
||||
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
|
||||
@@ -118,7 +119,7 @@ public static class GovernanceEndpoints
|
||||
.Select(MapOverrideToResponse)
|
||||
.ToList(),
|
||||
VerificationStatus = "verified",
|
||||
LastVerifiedAt = DateTimeOffset.UtcNow.ToString("O")
|
||||
LastVerifiedAt = timeProvider.GetUtcNow().ToString("O")
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
@@ -140,11 +141,12 @@ public static class GovernanceEndpoints
|
||||
|
||||
private static Task<IResult> ToggleSealedModeAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
SealedModeToggleRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
|
||||
|
||||
@@ -173,7 +175,7 @@ public static class GovernanceEndpoints
|
||||
|
||||
// Audit
|
||||
RecordAudit(tenant, actor, "sealed_mode_toggled", "sealed-mode", "system_config",
|
||||
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}");
|
||||
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}", timeProvider);
|
||||
|
||||
var response = new SealedModeStatusResponse
|
||||
{
|
||||
@@ -193,11 +195,12 @@ public static class GovernanceEndpoints
|
||||
|
||||
private static Task<IResult> CreateSealedModeOverrideAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
SealedModeOverrideRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var overrideId = $"override-{Guid.NewGuid():N}";
|
||||
var entity = new SealedModeOverrideEntity
|
||||
@@ -217,13 +220,14 @@ public static class GovernanceEndpoints
|
||||
Overrides[overrideId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "sealed_mode_override_created", overrideId, "sealed_mode_override",
|
||||
$"Created override for {request.Target}: {request.Reason}");
|
||||
$"Created override for {request.Target}: {request.Reason}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapOverrideToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> RevokeSealedModeOverrideAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
string overrideId,
|
||||
RevokeOverrideRequest request)
|
||||
{
|
||||
@@ -243,7 +247,7 @@ public static class GovernanceEndpoints
|
||||
Overrides[overrideId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "sealed_mode_override_revoked", overrideId, "sealed_mode_override",
|
||||
$"Revoked override: {request.Reason}");
|
||||
$"Revoked override: {request.Reason}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.NoContent());
|
||||
}
|
||||
@@ -289,11 +293,12 @@ public static class GovernanceEndpoints
|
||||
|
||||
private static Task<IResult> CreateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CreateRiskProfileRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var profileId = $"profile-{Guid.NewGuid():N}";
|
||||
var entity = new RiskProfileEntity
|
||||
@@ -317,19 +322,20 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_created", profileId, "risk_profile",
|
||||
$"Created risk profile: {request.Name}");
|
||||
$"Created risk profile: {request.Name}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.Created($"/api/v1/governance/risk-profiles/{profileId}", MapProfileToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> UpdateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
string profileId,
|
||||
UpdateRiskProfileRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
@@ -354,13 +360,14 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_updated", profileId, "risk_profile",
|
||||
$"Updated risk profile: {entity.Name}");
|
||||
$"Updated risk profile: {entity.Name}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> DeleteRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
string profileId)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
@@ -376,18 +383,19 @@ public static class GovernanceEndpoints
|
||||
}
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_deleted", profileId, "risk_profile",
|
||||
$"Deleted risk profile: {removed.Name}");
|
||||
$"Deleted risk profile: {removed.Name}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.NoContent());
|
||||
}
|
||||
|
||||
private static Task<IResult> ActivateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
string profileId)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
@@ -408,19 +416,20 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_activated", profileId, "risk_profile",
|
||||
$"Activated risk profile: {entity.Name}");
|
||||
$"Activated risk profile: {entity.Name}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> DeprecateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
string profileId,
|
||||
DeprecateProfileRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
@@ -442,7 +451,7 @@ public static class GovernanceEndpoints
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_deprecated", profileId, "risk_profile",
|
||||
$"Deprecated risk profile: {entity.Name} - {request.Reason}");
|
||||
$"Deprecated risk profile: {entity.Name} - {request.Reason}", timeProvider);
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
@@ -582,7 +591,7 @@ public static class GovernanceEndpoints
|
||||
?? httpContext.Request.Headers["X-StellaOps-Actor"].FirstOrDefault();
|
||||
}
|
||||
|
||||
private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary)
|
||||
private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary, TimeProvider timeProvider)
|
||||
{
|
||||
var id = $"audit-{Guid.NewGuid():N}";
|
||||
AuditEntries[id] = new GovernanceAuditEntry
|
||||
@@ -590,7 +599,7 @@ public static class GovernanceEndpoints
|
||||
Id = id,
|
||||
TenantId = tenantId,
|
||||
Type = eventType,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
||||
Timestamp = timeProvider.GetUtcNow().ToString("O"),
|
||||
Actor = actor,
|
||||
ActorType = "user",
|
||||
TargetResource = targetId,
|
||||
|
||||
@@ -50,6 +50,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleDockerRegistryWebhook(
|
||||
[FromBody] DockerRegistryNotification notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -77,7 +78,7 @@ internal static class RegistryWebhookEndpoints
|
||||
Tag = evt.Target.Tag,
|
||||
RegistryUrl = evt.Request?.Host,
|
||||
Source = "docker-registry",
|
||||
Timestamp = evt.Timestamp ?? DateTimeOffset.UtcNow
|
||||
Timestamp = evt.Timestamp ?? timeProvider.GetUtcNow()
|
||||
}, ct);
|
||||
|
||||
jobs.Add(jobId);
|
||||
@@ -100,6 +101,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleHarborWebhook(
|
||||
[FromBody] HarborWebhookEvent notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -136,7 +138,7 @@ internal static class RegistryWebhookEndpoints
|
||||
Tag = resource.Tag,
|
||||
RegistryUrl = notification.EventData.Repository?.RepoFullName,
|
||||
Source = "harbor",
|
||||
Timestamp = notification.OccurAt ?? DateTimeOffset.UtcNow
|
||||
Timestamp = notification.OccurAt ?? timeProvider.GetUtcNow()
|
||||
}, ct);
|
||||
|
||||
jobs.Add(jobId);
|
||||
@@ -159,6 +161,7 @@ internal static class RegistryWebhookEndpoints
|
||||
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleGenericWebhook(
|
||||
[FromBody] GenericRegistryWebhook notification,
|
||||
IGateEvaluationQueue evaluationQueue,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
ILogger<RegistryWebhookEndpointMarker> logger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -177,7 +180,7 @@ internal static class RegistryWebhookEndpoints
|
||||
RegistryUrl = notification.RegistryUrl,
|
||||
BaselineRef = notification.BaselineRef,
|
||||
Source = notification.Source ?? "generic",
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
Timestamp = timeProvider.GetUtcNow()
|
||||
}, ct);
|
||||
|
||||
logger.LogInformation(
|
||||
|
||||
@@ -21,11 +21,15 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
{
|
||||
private readonly Channel<GateEvaluationJob> _channel;
|
||||
private readonly ILogger<InMemoryGateEvaluationQueue> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryGateEvaluationQueue(ILogger<InMemoryGateEvaluationQueue> logger)
|
||||
public InMemoryGateEvaluationQueue(
|
||||
ILogger<InMemoryGateEvaluationQueue> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
|
||||
// Bounded channel to prevent unbounded memory growth
|
||||
_channel = Channel.CreateBounded<GateEvaluationJob>(new BoundedChannelOptions(1000)
|
||||
@@ -46,7 +50,7 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
{
|
||||
JobId = jobId,
|
||||
Request = request,
|
||||
QueuedAt = DateTimeOffset.UtcNow
|
||||
QueuedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
await _channel.Writer.WriteAsync(job, cancellationToken).ConfigureAwait(false);
|
||||
@@ -65,10 +69,10 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
/// </summary>
|
||||
public ChannelReader<GateEvaluationJob> Reader => _channel.Reader;
|
||||
|
||||
private static string GenerateJobId()
|
||||
private string GenerateJobId()
|
||||
{
|
||||
// Format: gate-{timestamp}-{random}
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var timestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
|
||||
var random = Guid.NewGuid().ToString("N")[..8];
|
||||
return $"gate-{timestamp}-{random}";
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
public sealed class InMemoryOverrideStore : IOverrideStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid OverrideId), OverrideEntity> _overrides = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryOverrideStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<OverrideEntity> CreateAsync(
|
||||
Guid tenantId,
|
||||
@@ -16,7 +22,7 @@ public sealed class InMemoryOverrideStore : IOverrideStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var overrideId = Guid.NewGuid();
|
||||
|
||||
var entity = new OverrideEntity
|
||||
@@ -73,7 +79,7 @@ public sealed class InMemoryOverrideStore : IOverrideStore
|
||||
return Task.FromResult<OverrideEntity?>(null);
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var updated = existing with
|
||||
{
|
||||
Status = OverrideStatus.Approved,
|
||||
|
||||
@@ -13,6 +13,12 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), PolicyPackEntity> _packs = new();
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), List<PolicyPackHistoryEntry>> _history = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryPolicyPackStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<PolicyPackEntity> CreateAsync(
|
||||
Guid tenantId,
|
||||
@@ -20,7 +26,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var packId = Guid.NewGuid();
|
||||
|
||||
var entity = new PolicyPackEntity
|
||||
@@ -130,7 +136,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
Description = request.Description ?? existing.Description,
|
||||
Rules = request.Rules ?? existing.Rules,
|
||||
Metadata = request.Metadata ?? existing.Metadata,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = _timeProvider.GetUtcNow(),
|
||||
UpdatedBy = updatedBy
|
||||
};
|
||||
|
||||
@@ -178,7 +184,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
return Task.FromResult<PolicyPackEntity?>(null);
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var updated = existing with
|
||||
{
|
||||
Status = newStatus,
|
||||
@@ -228,7 +234,7 @@ public sealed class InMemoryPolicyPackStore : IPolicyPackStore
|
||||
{
|
||||
PackId = packId,
|
||||
Action = action,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Timestamp = _timeProvider.GetUtcNow(),
|
||||
PerformedBy = performedBy,
|
||||
PreviousStatus = previousStatus,
|
||||
NewStatus = newStatus,
|
||||
|
||||
@@ -12,6 +12,12 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
public sealed class InMemorySnapshotStore : ISnapshotStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid SnapshotId), SnapshotEntity> _snapshots = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemorySnapshotStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<SnapshotEntity> CreateAsync(
|
||||
Guid tenantId,
|
||||
@@ -19,7 +25,7 @@ public sealed class InMemorySnapshotStore : ISnapshotStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var snapshotId = Guid.NewGuid();
|
||||
|
||||
// Compute digest from pack IDs and timestamp for uniqueness
|
||||
|
||||
@@ -9,6 +9,12 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, string PolicyId), VerificationPolicyEntity> _policies = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryVerificationPolicyStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<VerificationPolicyEntity> CreateAsync(
|
||||
Guid tenantId,
|
||||
@@ -16,7 +22,7 @@ public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore
|
||||
string? createdBy = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
var entity = new VerificationPolicyEntity
|
||||
{
|
||||
@@ -102,7 +108,7 @@ public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore
|
||||
SignerRequirements = request.SignerRequirements ?? existing.SignerRequirements,
|
||||
ValidityWindow = request.ValidityWindow ?? existing.ValidityWindow,
|
||||
Metadata = request.Metadata ?? existing.Metadata,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = _timeProvider.GetUtcNow(),
|
||||
UpdatedBy = updatedBy
|
||||
};
|
||||
|
||||
|
||||
@@ -9,13 +9,19 @@ namespace StellaOps.Policy.Registry.Storage;
|
||||
public sealed class InMemoryViolationStore : IViolationStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid ViolationId), ViolationEntity> _violations = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryViolationStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<ViolationEntity> AppendAsync(
|
||||
Guid tenantId,
|
||||
CreateViolationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var violationId = Guid.NewGuid();
|
||||
|
||||
var entity = new ViolationEntity
|
||||
@@ -42,7 +48,7 @@ public sealed class InMemoryViolationStore : IViolationStore
|
||||
IReadOnlyList<CreateViolationRequest> requests,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
int created = 0;
|
||||
int failed = 0;
|
||||
var errors = new List<BatchError>();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy.Persistence;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
@@ -35,7 +36,7 @@ public sealed class UnknownsRepositoryTests : IAsyncLifetime
|
||||
public async Task CreateAndGetById_RoundTripsReasonCodeAndEvidence()
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(_tenantId.ToString());
|
||||
var repository = new UnknownsRepository(connection);
|
||||
var repository = new UnknownsRepository(connection, TimeProvider.System, SystemGuidProvider.Instance);
|
||||
var now = new DateTimeOffset(2025, 1, 2, 3, 4, 5, TimeSpan.Zero);
|
||||
|
||||
var unknown = CreateUnknown(
|
||||
@@ -65,7 +66,7 @@ public sealed class UnknownsRepositoryTests : IAsyncLifetime
|
||||
public async Task UpdateAsync_PersistsReasonCodeAndAssumptions()
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(_tenantId.ToString());
|
||||
var repository = new UnknownsRepository(connection);
|
||||
var repository = new UnknownsRepository(connection, TimeProvider.System, SystemGuidProvider.Instance);
|
||||
var now = new DateTimeOffset(2025, 2, 3, 4, 5, 6, TimeSpan.Zero);
|
||||
|
||||
var unknown = CreateUnknown(
|
||||
|
||||
@@ -33,7 +33,7 @@ rules:
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, null), CancellationToken.None);
|
||||
|
||||
@@ -80,7 +80,7 @@ rules:
|
||||
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
|
||||
|
||||
var findings = ImmutableArray.Create(
|
||||
@@ -111,7 +111,7 @@ rules:
|
||||
{
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
|
||||
|
||||
const string invalid = "version: 1.0";
|
||||
@@ -159,7 +159,7 @@ rules:
|
||||
Assert.False(binding.Document.Rules[0].Metadata.ContainsKey("quiet"));
|
||||
Assert.True(binding.Document.Rules[0].Action.Quiet);
|
||||
|
||||
var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "quiet test"), CancellationToken.None);
|
||||
var snapshot = await store.GetLatestAsync();
|
||||
Assert.NotNull(snapshot);
|
||||
|
||||
@@ -25,7 +25,7 @@ rules:
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero));
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null);
|
||||
|
||||
@@ -56,7 +56,7 @@ rules:
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero));
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null);
|
||||
var first = await store.SaveAsync(content, CancellationToken.None);
|
||||
@@ -81,7 +81,7 @@ rules:
|
||||
{
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
const string invalidYaml = "version: '1.0'\nrules: []";
|
||||
var content = new PolicySnapshotContent(invalidYaml, PolicyDocumentFormat.Yaml, null, null, null);
|
||||
|
||||
@@ -32,6 +32,7 @@ public sealed class ReplayEngineTests
|
||||
_snapshotService,
|
||||
sourceResolver,
|
||||
verdictComparer,
|
||||
null,
|
||||
NullLogger<ReplayEngine>.Instance);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ using StellaOps.SbomService.Models;
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.SbomService.Tests;
|
||||
|
||||
public class EntrypointEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
public class EntrypointEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
|
||||
|
||||
public EntrypointEndpointsTests(WebApplicationFactory<Program> factory)
|
||||
public EntrypointEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ using Xunit;
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.SbomService.Tests;
|
||||
|
||||
public class OrchestratorEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
public class OrchestratorEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
|
||||
|
||||
public OrchestratorEndpointsTests(WebApplicationFactory<Program> factory)
|
||||
public OrchestratorEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ using Xunit;
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.SbomService.Tests;
|
||||
|
||||
public class ProjectionEndpointTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
public class ProjectionEndpointTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
|
||||
|
||||
public ProjectionEndpointTests(WebApplicationFactory<Program> factory)
|
||||
public ProjectionEndpointTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
|
||||
{
|
||||
var contentRoot = ResolveContentRoot();
|
||||
_factory = factory.WithWebHostBuilder(builder =>
|
||||
|
||||
@@ -8,11 +8,11 @@ using Xunit;
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.SbomService.Tests;
|
||||
|
||||
public class ResolverFeedExportTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
public class ResolverFeedExportTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
|
||||
|
||||
public ResolverFeedExportTests(WebApplicationFactory<Program> factory)
|
||||
public ResolverFeedExportTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ using Xunit;
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.SbomService.Tests;
|
||||
|
||||
public class SbomAssetEventsTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
public class SbomAssetEventsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
|
||||
|
||||
public SbomAssetEventsTests(WebApplicationFactory<Program> factory)
|
||||
public SbomAssetEventsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ using Xunit;
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.SbomService.Tests;
|
||||
|
||||
public class SbomEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
public class SbomEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
|
||||
|
||||
public SbomEndpointsTests(WebApplicationFactory<Program> factory)
|
||||
public SbomEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(_ => { });
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ using Xunit;
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.SbomService.Tests;
|
||||
|
||||
public class SbomEventEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
public class SbomEventEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
|
||||
|
||||
public SbomEventEndpointsTests(WebApplicationFactory<Program> factory)
|
||||
public SbomEventEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(_ => { });
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ using Xunit;
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.SbomService.Tests;
|
||||
|
||||
public class SbomInventoryEventsTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
public class SbomInventoryEventsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
|
||||
|
||||
public SbomInventoryEventsTests(WebApplicationFactory<Program> factory)
|
||||
public SbomInventoryEventsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ using Xunit;
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.SbomService.Tests;
|
||||
|
||||
public sealed class SbomLedgerEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
public sealed class SbomLedgerEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
|
||||
|
||||
public SbomLedgerEndpointsTests(WebApplicationFactory<Program> factory)
|
||||
public SbomLedgerEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(_ => { });
|
||||
}
|
||||
|
||||
@@ -1311,5 +1311,8 @@ app.MapPost("/internal/orchestrator/watermarks", async Task<IResult> (
|
||||
|
||||
app.Run();
|
||||
|
||||
// Program class public for WebApplicationFactory<Program>
|
||||
public partial class Program;
|
||||
// Program class in namespace to avoid conflicts with other assemblies
|
||||
namespace StellaOps.SbomService
|
||||
{
|
||||
public partial class Program;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ namespace StellaOps.SbomService.Repositories;
|
||||
internal sealed class InMemoryOrchestratorRepository : IOrchestratorRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, List<OrchestratorSource>> _sources = new(StringComparer.Ordinal);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryOrchestratorRepository()
|
||||
public InMemoryOrchestratorRepository(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
Seed();
|
||||
}
|
||||
|
||||
@@ -37,7 +39,7 @@ internal sealed class InMemoryOrchestratorRepository : IOrchestratorRepository
|
||||
sourceId,
|
||||
request.ArtifactDigest.Trim(),
|
||||
request.SourceType.Trim(),
|
||||
DateTimeOffset.UtcNow,
|
||||
_timeProvider.GetUtcNow(),
|
||||
request.Metadata.Trim());
|
||||
|
||||
// Idempotent on (tenant, artifactDigest, sourceType)
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.SbomService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the current time abstraction - delegates to TimeProvider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deprecated: Prefer injecting TimeProvider directly. This interface is kept
|
||||
/// for backward compatibility during migration.
|
||||
/// </remarks>
|
||||
public interface IClock
|
||||
{
|
||||
DateTimeOffset UtcNow { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default IClock implementation that delegates to TimeProvider.
|
||||
/// </summary>
|
||||
public sealed class SystemClock : IClock
|
||||
{
|
||||
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public SystemClock(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public DateTimeOffset UtcNow => _timeProvider.GetUtcNow();
|
||||
}
|
||||
|
||||
@@ -136,9 +136,9 @@ public sealed record ReplayVerificationResult
|
||||
public ImmutableArray<ReplayFieldDrift> Drifts { get; init; } = ImmutableArray<ReplayFieldDrift>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When the verification was performed.
|
||||
/// When the verification was performed. Must be explicitly set by calling code.
|
||||
/// </summary>
|
||||
public DateTimeOffset VerifiedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public required DateTimeOffset VerifiedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional message with additional context.
|
||||
@@ -249,7 +249,7 @@ public sealed record ReplayDriftAnalysis
|
||||
public required string DriftSummary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the analysis was performed.
|
||||
/// When the analysis was performed. Must be explicitly set by calling code.
|
||||
/// </summary>
|
||||
public DateTimeOffset AnalyzedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public required DateTimeOffset AnalyzedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -26,19 +26,22 @@ internal sealed class LineageCompareService : ILineageCompareService
|
||||
private readonly IVexDeltaRepository? _vexDeltaRepository;
|
||||
private readonly ILineageCompareCache? _cache;
|
||||
private readonly ILogger<LineageCompareService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public LineageCompareService(
|
||||
ISbomLineageGraphService lineageService,
|
||||
ISbomLedgerService ledgerService,
|
||||
ILogger<LineageCompareService> logger,
|
||||
IVexDeltaRepository? vexDeltaRepository = null,
|
||||
ILineageCompareCache? cache = null)
|
||||
ILineageCompareCache? cache = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_lineageService = lineageService ?? throw new ArgumentNullException(nameof(lineageService));
|
||||
_ledgerService = ledgerService ?? throw new ArgumentNullException(nameof(ledgerService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_vexDeltaRepository = vexDeltaRepository;
|
||||
_cache = cache;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -139,7 +142,7 @@ internal sealed class LineageCompareService : ILineageCompareService
|
||||
FromDigest = fromDigest,
|
||||
ToDigest = toDigest,
|
||||
TenantId = tenantId,
|
||||
ComputedAt = DateTimeOffset.UtcNow,
|
||||
ComputedAt = _timeProvider.GetUtcNow(),
|
||||
FromArtifact = fromArtifact,
|
||||
ToArtifact = toArtifact,
|
||||
Summary = summary,
|
||||
|
||||
@@ -21,16 +21,19 @@ internal sealed class LineageExportService : ILineageExportService
|
||||
private readonly ISbomLineageGraphService _lineageService;
|
||||
private readonly IReplayHashService? _replayHashService;
|
||||
private readonly ILogger<LineageExportService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private const long MaxExportSizeBytes = 50 * 1024 * 1024; // 50MB limit
|
||||
|
||||
public LineageExportService(
|
||||
ISbomLineageGraphService lineageService,
|
||||
ILogger<LineageExportService> logger,
|
||||
IReplayHashService? replayHashService = null)
|
||||
IReplayHashService? replayHashService = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_lineageService = lineageService;
|
||||
_logger = logger;
|
||||
_replayHashService = replayHashService;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<LineageExportResponse?> ExportAsync(
|
||||
@@ -59,7 +62,7 @@ internal sealed class LineageExportService : ILineageExportService
|
||||
Version = "1.0",
|
||||
FromDigest = request.FromDigest,
|
||||
ToDigest = request.ToDigest,
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
GeneratedAt = _timeProvider.GetUtcNow(),
|
||||
ReplayHash = diff.ReplayHash ?? ComputeFallbackHash(request.FromDigest, request.ToDigest),
|
||||
SbomDiff = request.IncludeSbomDiff ? diff.SbomDiff?.Summary : null,
|
||||
VexDeltas = request.IncludeVexDeltas ? diff.VexDiff : null,
|
||||
@@ -91,7 +94,7 @@ internal sealed class LineageExportService : ILineageExportService
|
||||
// Generate export ID and URL
|
||||
var exportId = Guid.NewGuid().ToString("N");
|
||||
var downloadUrl = $"/api/v1/lineage/export/{exportId}/download";
|
||||
var expiresAt = DateTimeOffset.UtcNow.AddHours(24);
|
||||
var expiresAt = _timeProvider.GetUtcNow().AddHours(24);
|
||||
|
||||
// TODO: Store evidence pack for retrieval (file system, blob storage, etc.)
|
||||
// For now, return metadata only
|
||||
@@ -114,9 +117,9 @@ internal sealed class LineageExportService : ILineageExportService
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeFallbackHash(string fromDigest, string toDigest)
|
||||
private string ComputeFallbackHash(string fromDigest, string toDigest)
|
||||
{
|
||||
var input = $"{fromDigest}:{toDigest}:{DateTimeOffset.UtcNow:O}";
|
||||
var input = $"{fromDigest}:{toDigest}:{_timeProvider.GetUtcNow():O}";
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
var hashBytes = SHA256.HashData(bytes);
|
||||
return Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||
|
||||
@@ -221,11 +221,13 @@ internal sealed class InMemoryLineageHoverCache : ILineageHoverCache
|
||||
{
|
||||
private readonly Dictionary<string, (SbomLineageHoverCard Card, DateTimeOffset ExpiresAt)> _cache = new();
|
||||
private readonly LineageHoverCacheOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public InMemoryLineageHoverCache(LineageHoverCacheOptions? options = null)
|
||||
public InMemoryLineageHoverCache(LineageHoverCacheOptions? options = null, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_options = options ?? new LineageHoverCacheOptions();
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<SbomLineageHoverCard?> GetAsync(string fromDigest, string toDigest, string tenantId, CancellationToken ct = default)
|
||||
@@ -240,7 +242,7 @@ internal sealed class InMemoryLineageHoverCache : ILineageHoverCache
|
||||
{
|
||||
if (_cache.TryGetValue(key, out var entry))
|
||||
{
|
||||
if (entry.ExpiresAt > DateTimeOffset.UtcNow)
|
||||
if (entry.ExpiresAt > _timeProvider.GetUtcNow())
|
||||
{
|
||||
return Task.FromResult<SbomLineageHoverCard?>(entry.Card);
|
||||
}
|
||||
@@ -260,7 +262,7 @@ internal sealed class InMemoryLineageHoverCache : ILineageHoverCache
|
||||
}
|
||||
|
||||
var key = BuildKey(fromDigest, toDigest, tenantId);
|
||||
var expiresAt = DateTimeOffset.UtcNow.Add(_options.Ttl);
|
||||
var expiresAt = _timeProvider.GetUtcNow().Add(_options.Ttl);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
|
||||
@@ -11,8 +11,8 @@ public sealed record OrchestratorControlState(
|
||||
string Backpressure,
|
||||
DateTimeOffset UpdatedAtUtc)
|
||||
{
|
||||
public static OrchestratorControlState Default(string tenantId) =>
|
||||
new(tenantId, false, 0, "normal", DateTimeOffset.UtcNow);
|
||||
public static OrchestratorControlState Default(string tenantId, TimeProvider? timeProvider = null) =>
|
||||
new(tenantId, false, 0, "normal", (timeProvider ?? TimeProvider.System).GetUtcNow());
|
||||
}
|
||||
|
||||
public sealed record OrchestratorControlRequest(
|
||||
@@ -34,13 +34,18 @@ internal sealed class OrchestratorControlService : IOrchestratorControlService
|
||||
private readonly Counter<long> _controlUpdates;
|
||||
private readonly ObservableGauge<int> _throttleGauge;
|
||||
private readonly ObservableGauge<int> _pausedGauge;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private readonly ConcurrentDictionary<string, OrchestratorControlState> _cache = new(StringComparer.Ordinal);
|
||||
|
||||
public OrchestratorControlService(IOrchestratorControlRepository repository, Meter meter)
|
||||
public OrchestratorControlService(
|
||||
IOrchestratorControlRepository repository,
|
||||
Meter meter,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_repository = repository;
|
||||
_meter = meter;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_controlUpdates = meter.CreateCounter<long>("sbom_orchestrator_control_updates");
|
||||
_throttleGauge = meter.CreateObservableGauge("sbom_orchestrator_throttle_percent", ObserveThrottle);
|
||||
_pausedGauge = meter.CreateObservableGauge("sbom_orchestrator_paused", ObservePaused);
|
||||
@@ -66,7 +71,7 @@ internal sealed class OrchestratorControlService : IOrchestratorControlService
|
||||
Paused: request.Paused ?? current.Paused,
|
||||
ThrottlePercent: throttle,
|
||||
Backpressure: string.IsNullOrWhiteSpace(request.Backpressure) ? current.Backpressure : request.Backpressure!.Trim().ToLowerInvariant(),
|
||||
UpdatedAtUtc: DateTimeOffset.UtcNow);
|
||||
UpdatedAtUtc: _timeProvider.GetUtcNow());
|
||||
|
||||
await _repository.SetAsync(updated, cancellationToken);
|
||||
_cache[updated.TenantId] = updated;
|
||||
|
||||
@@ -30,15 +30,18 @@ public sealed class RegistrySourceService : IRegistrySourceService
|
||||
private readonly IRegistrySourceRepository _sourceRepository;
|
||||
private readonly IRegistrySourceRunRepository _runRepository;
|
||||
private readonly ILogger<RegistrySourceService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public RegistrySourceService(
|
||||
IRegistrySourceRepository sourceRepository,
|
||||
IRegistrySourceRunRepository runRepository,
|
||||
ILogger<RegistrySourceService> logger)
|
||||
ILogger<RegistrySourceService> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_sourceRepository = sourceRepository;
|
||||
_runRepository = runRepository;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -125,7 +128,7 @@ public sealed class RegistrySourceService : IRegistrySourceService
|
||||
if (request.Status.HasValue) source.Status = request.Status.Value;
|
||||
if (request.Tags is not null) source.Tags = request.Tags.ToList();
|
||||
|
||||
source.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
source.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
source.UpdatedBy = userId;
|
||||
|
||||
var updated = await _sourceRepository.UpdateAsync(source, cancellationToken);
|
||||
@@ -156,23 +159,23 @@ public sealed class RegistrySourceService : IRegistrySourceService
|
||||
var source = await _sourceRepository.GetByIdAsync(id, cancellationToken);
|
||||
if (source is null)
|
||||
{
|
||||
return new TestRegistrySourceResponse(id, false, "Registry source not found", null, TimeSpan.Zero, DateTimeOffset.UtcNow);
|
||||
return new TestRegistrySourceResponse(id, false, "Registry source not found", null, TimeSpan.Zero, _timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
var startTime = DateTimeOffset.UtcNow;
|
||||
var startTime = _timeProvider.GetUtcNow();
|
||||
|
||||
// TODO: Implement actual registry connection test
|
||||
// For now, simulate a successful test
|
||||
await Task.Delay(100, cancellationToken);
|
||||
|
||||
var duration = DateTimeOffset.UtcNow - startTime;
|
||||
var duration = _timeProvider.GetUtcNow() - startTime;
|
||||
|
||||
// Update source status
|
||||
var newStatus = RegistrySourceStatus.Active;
|
||||
if (source.Status != newStatus)
|
||||
{
|
||||
source.Status = newStatus;
|
||||
source.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
source.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
await _sourceRepository.UpdateAsync(source, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -188,7 +191,7 @@ public sealed class RegistrySourceService : IRegistrySourceService
|
||||
["type"] = source.Type.ToString()
|
||||
},
|
||||
duration,
|
||||
DateTimeOffset.UtcNow);
|
||||
_timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -209,7 +212,7 @@ public sealed class RegistrySourceService : IRegistrySourceService
|
||||
Status = RegistryRunStatus.Queued,
|
||||
TriggerType = triggerType,
|
||||
TriggerMetadata = triggerMetadata,
|
||||
StartedAt = DateTimeOffset.UtcNow
|
||||
StartedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
var created = await _runRepository.CreateAsync(run, cancellationToken);
|
||||
@@ -229,7 +232,7 @@ public sealed class RegistrySourceService : IRegistrySourceService
|
||||
if (source is null) return null;
|
||||
|
||||
source.Status = RegistrySourceStatus.Paused;
|
||||
source.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
source.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
source.UpdatedBy = userId;
|
||||
|
||||
var updated = await _sourceRepository.UpdateAsync(source, cancellationToken);
|
||||
@@ -252,7 +255,7 @@ public sealed class RegistrySourceService : IRegistrySourceService
|
||||
}
|
||||
|
||||
source.Status = RegistrySourceStatus.Active;
|
||||
source.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
source.UpdatedAt = _timeProvider.GetUtcNow();
|
||||
source.UpdatedBy = userId;
|
||||
|
||||
var updated = await _sourceRepository.UpdateAsync(source, cancellationToken);
|
||||
|
||||
@@ -74,6 +74,7 @@ internal sealed class ReplayVerificationService : IReplayVerificationService
|
||||
ExpectedHash = request.ReplayHash,
|
||||
ComputedHash = string.Empty,
|
||||
Status = ReplayVerificationStatus.InputsNotFound,
|
||||
VerifiedAt = _clock.UtcNow,
|
||||
Error = "Unable to determine verification inputs. Provide explicit inputs or ensure hash is stored."
|
||||
};
|
||||
}
|
||||
@@ -119,6 +120,7 @@ internal sealed class ReplayVerificationService : IReplayVerificationService
|
||||
ExpectedHash = request.ReplayHash,
|
||||
ComputedHash = string.Empty,
|
||||
Status = ReplayVerificationStatus.Error,
|
||||
VerifiedAt = _clock.UtcNow,
|
||||
Error = ex.Message
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ public interface IWatermarkService
|
||||
internal sealed class InMemoryWatermarkService : IWatermarkService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, WatermarkState> _watermarks = new(StringComparer.Ordinal);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryWatermarkService(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<WatermarkState> GetAsync(string tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -24,14 +30,14 @@ internal sealed class InMemoryWatermarkService : IWatermarkService
|
||||
return Task.FromResult(state);
|
||||
}
|
||||
|
||||
var created = new WatermarkState(tenantId, string.Empty, DateTimeOffset.UtcNow);
|
||||
var created = new WatermarkState(tenantId, string.Empty, _timeProvider.GetUtcNow());
|
||||
_watermarks[tenantId] = created;
|
||||
return Task.FromResult(created);
|
||||
}
|
||||
|
||||
public Task<WatermarkState> SetAsync(string tenantId, string watermark, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = new WatermarkState(tenantId, watermark, DateTimeOffset.UtcNow);
|
||||
var state = new WatermarkState(tenantId, watermark, _timeProvider.GetUtcNow());
|
||||
_watermarks[tenantId] = state;
|
||||
return Task.FromResult(state);
|
||||
}
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="StellaOps.SbomService.Tests" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -13,12 +13,15 @@ public sealed class SbomLineageEdgeRepository : RepositoryBase<LineageDataSource
|
||||
private const string Schema = "sbom";
|
||||
private const string Table = "sbom_lineage_edges";
|
||||
private const string FullTable = $"{Schema}.{Table}";
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public SbomLineageEdgeRepository(
|
||||
LineageDataSource dataSource,
|
||||
ILogger<SbomLineageEdgeRepository> logger)
|
||||
ILogger<SbomLineageEdgeRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async ValueTask<LineageGraph> GetGraphAsync(
|
||||
@@ -260,7 +263,7 @@ public sealed class SbomLineageEdgeRepository : RepositoryBase<LineageDataSource
|
||||
ArtifactDigest: artifactDigest,
|
||||
SbomVersionId: null,
|
||||
SequenceNumber: 0,
|
||||
CreatedAt: DateTimeOffset.UtcNow,
|
||||
CreatedAt: _timeProvider.GetUtcNow(),
|
||||
Metadata: null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ public sealed class LineageGraphService : ILineageGraphService
|
||||
private readonly ISbomVerdictLinkRepository _verdictRepository;
|
||||
private readonly IDistributedCache? _cache;
|
||||
private readonly ILogger<LineageGraphService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(10);
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
@@ -25,13 +26,15 @@ public sealed class LineageGraphService : ILineageGraphService
|
||||
IVexDeltaRepository deltaRepository,
|
||||
ISbomVerdictLinkRepository verdictRepository,
|
||||
ILogger<LineageGraphService> logger,
|
||||
IDistributedCache? cache = null)
|
||||
IDistributedCache? cache = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_edgeRepository = edgeRepository;
|
||||
_deltaRepository = deltaRepository;
|
||||
_verdictRepository = verdictRepository;
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async ValueTask<LineageGraphResponse> GetLineageAsync(
|
||||
@@ -184,7 +187,7 @@ public sealed class LineageGraphService : ILineageGraphService
|
||||
|
||||
// Placeholder implementation
|
||||
var downloadUrl = $"https://evidence.stellaops.example/exports/{Guid.NewGuid()}.tar.gz";
|
||||
var expiresAt = DateTimeOffset.UtcNow.AddHours(24);
|
||||
var expiresAt = _timeProvider.GetUtcNow().AddHours(24);
|
||||
|
||||
return new ExportResult(
|
||||
DownloadUrl: downloadUrl,
|
||||
|
||||
@@ -11,11 +11,16 @@ namespace StellaOps.SbomService.Persistence.Postgres.Repositories;
|
||||
/// </summary>
|
||||
public sealed class PostgresOrchestratorRepository : RepositoryBase<SbomServiceDataSource>, IOrchestratorRepository
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private bool _tableInitialized;
|
||||
|
||||
public PostgresOrchestratorRepository(SbomServiceDataSource dataSource, ILogger<PostgresOrchestratorRepository> logger)
|
||||
public PostgresOrchestratorRepository(
|
||||
SbomServiceDataSource dataSource,
|
||||
ILogger<PostgresOrchestratorRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<OrchestratorSource>> ListAsync(string tenantId, CancellationToken cancellationToken)
|
||||
@@ -79,7 +84,7 @@ public sealed class PostgresOrchestratorRepository : RepositoryBase<SbomServiceD
|
||||
var count = Convert.ToInt32(await countCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false));
|
||||
var sourceId = $"src-{count + 1:D3}";
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
const string insertSql = @"
|
||||
INSERT INTO sbom.orchestrator_sources (tenant_id, source_id, artifact_digest, source_type, created_at, metadata)
|
||||
|
||||
@@ -78,9 +78,9 @@ public sealed record LineageEdge
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the edge was created.
|
||||
/// When the edge was created. Must be explicitly set by calling code.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional metadata about the relationship.
|
||||
|
||||
@@ -94,9 +94,9 @@ public sealed record SbomVerdictLink
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the link was created.
|
||||
/// When the link was created. Must be explicitly set by calling code.
|
||||
/// </summary>
|
||||
public DateTimeOffset LinkedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public required DateTimeOffset LinkedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SBOM artifact digest for cross-reference.
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class AdvisoryClient : IAdvisoryClient
|
||||
|
||||
var normalized = cveId.Trim().ToUpperInvariant();
|
||||
var cacheKey = $"advisory:cve:{normalized}";
|
||||
if (_cache.TryGetValue(cacheKey, out AdvisorySymbolMapping cached))
|
||||
if (_cache.TryGetValue(cacheKey, out AdvisorySymbolMapping? cached) && cached is not null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ internal static class PythonDistributionLoader
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void AddFileEvidence(LanguageAnalyzerContext context, string path, string source, ICollection<LanguageComponentEvidence> evidence)
|
||||
private static void AddFileEvidence(LanguageAnalyzerContext context, string? path, string source, ICollection<LanguageComponentEvidence> evidence)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ public sealed class RiskScoreTests
|
||||
[Fact]
|
||||
public void RiskScore_Zero_ReturnsNegligibleLevel()
|
||||
{
|
||||
var score = RiskScore.Zero;
|
||||
var score = RiskScore.Zero();
|
||||
|
||||
Assert.Equal(0.0f, score.OverallScore);
|
||||
Assert.Equal(RiskCategory.Unknown, score.Category);
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class SurfaceValidatorRunnerTests
|
||||
Array.Empty<string>(),
|
||||
new SurfaceSecretsConfiguration("kubernetes", "", null, null, null, false),
|
||||
string.Empty,
|
||||
new SurfaceTlsConfiguration(null, null, null));
|
||||
new SurfaceTlsConfiguration(null, null, null)) { CreatedAtUtc = DateTimeOffset.UtcNow };
|
||||
|
||||
var context = SurfaceValidationContext.Create(services, "TestComponent", environment);
|
||||
|
||||
@@ -60,7 +60,7 @@ public sealed class SurfaceValidatorRunnerTests
|
||||
Array.Empty<string>(),
|
||||
new SurfaceSecretsConfiguration("kubernetes", "tenant-a", null, "stellaops", null, false),
|
||||
"tenant-a",
|
||||
new SurfaceTlsConfiguration(null, null, null));
|
||||
new SurfaceTlsConfiguration(null, null, null)) { CreatedAtUtc = DateTimeOffset.UtcNow };
|
||||
|
||||
var services = CreateServices();
|
||||
var runner = services.GetRequiredService<ISurfaceValidatorRunner>();
|
||||
@@ -86,7 +86,7 @@ public sealed class SurfaceValidatorRunnerTests
|
||||
Array.Empty<string>(),
|
||||
new SurfaceSecretsConfiguration("inline", "tenant-a", Root: null, Namespace: null, FallbackProvider: null, AllowInline: false),
|
||||
"tenant-a",
|
||||
new SurfaceTlsConfiguration(null, null, null));
|
||||
new SurfaceTlsConfiguration(null, null, null)) { CreatedAtUtc = DateTimeOffset.UtcNow };
|
||||
|
||||
var services = CreateServices();
|
||||
var runner = services.GetRequiredService<ISurfaceValidatorRunner>();
|
||||
@@ -118,7 +118,7 @@ public sealed class SurfaceValidatorRunnerTests
|
||||
Array.Empty<string>(),
|
||||
new SurfaceSecretsConfiguration("file", "tenant-a", Root: missingRoot, Namespace: null, FallbackProvider: null, AllowInline: false),
|
||||
"tenant-a",
|
||||
new SurfaceTlsConfiguration(null, null, null));
|
||||
new SurfaceTlsConfiguration(null, null, null)) { CreatedAtUtc = DateTimeOffset.UtcNow };
|
||||
|
||||
var services = CreateServices();
|
||||
var runner = services.GetRequiredService<ISurfaceValidatorRunner>();
|
||||
|
||||
@@ -49,7 +49,7 @@ builder.Services.AddOpenApi();
|
||||
var routerOptions = builder.Configuration.GetSection("VexHub:Router").Get<StellaRouterOptionsBase>();
|
||||
builder.Services.TryAddStellaRouter(
|
||||
serviceName: "vexhub",
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
version: System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
var app = builder.Build();
|
||||
@@ -83,3 +83,9 @@ app.MapGet("/health", () => Results.Ok(new { Status = "Healthy", Service = "VexH
|
||||
app.TryRefreshStellaRouterEndpoints(routerOptions);
|
||||
|
||||
app.Run();
|
||||
|
||||
// Make Program class explicit to avoid conflicts with imported types
|
||||
namespace StellaOps.VexHub.WebService
|
||||
{
|
||||
public partial class Program { }
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ namespace StellaOps.VexHub.WebService.Tests.Integration;
|
||||
/// Integration tests verifying VexHub API compatibility with Trivy and Grype.
|
||||
/// These tests ensure the API endpoints return valid OpenVEX format that can be consumed by scanning tools.
|
||||
/// </summary>
|
||||
public sealed class VexExportCompatibilityTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
public sealed class VexExportCompatibilityTests : IClassFixture<WebApplicationFactory<StellaOps.VexHub.WebService.Program>>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public VexExportCompatibilityTests(WebApplicationFactory<Program> factory)
|
||||
public VexExportCompatibilityTests(WebApplicationFactory<StellaOps.VexHub.WebService.Program> factory)
|
||||
{
|
||||
_client = factory.CreateClient();
|
||||
}
|
||||
|
||||
@@ -165,14 +165,14 @@ internal static class NoiseGatingApiMapper
|
||||
VulnerabilityId: entry.VulnerabilityId,
|
||||
ProductKey: entry.ProductKey,
|
||||
FromStatus: entry.FromStatus?.ToString(),
|
||||
ToStatus: entry.ToStatus?.ToString(),
|
||||
ToStatus: entry.ToStatus.ToString(),
|
||||
FromConfidence: entry.FromConfidence,
|
||||
ToConfidence: entry.ToConfidence,
|
||||
Justification: entry.Justification?.ToString(),
|
||||
FromRationaleClass: entry.FromRationaleClass,
|
||||
ToRationaleClass: entry.ToRationaleClass,
|
||||
Summary: entry.Summary,
|
||||
ContributingSources: entry.ContributingSources?.ToList(),
|
||||
ContributingSources: entry.ContributingSources.IsDefaultOrEmpty ? null : entry.ContributingSources.ToList(),
|
||||
CreatedAt: entry.Timestamp);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,16 +57,27 @@ public sealed class NoiseGateService : INoiseGate
|
||||
if (!opts.EdgeDeduplicationEnabled || !opts.Enabled)
|
||||
{
|
||||
// Return edges without deduplication - create minimal deduplicated wrappers
|
||||
var passthrough = edges.Select(e => new DeduplicatedEdgeBuilder(
|
||||
e.From, e.To, e.Why.Type, e.Why.Loc)
|
||||
.WithConfidence(e.Why.Confidence)
|
||||
.Build())
|
||||
.ToList();
|
||||
var passthrough = edges.Select(e =>
|
||||
{
|
||||
var key = new EdgeSemanticKey(e.From, e.To);
|
||||
var builder = new DeduplicatedEdgeBuilder(key, e.From, e.To);
|
||||
builder.AddSource(
|
||||
"passthrough",
|
||||
e.Why,
|
||||
e.Why.Confidence,
|
||||
_timeProvider.GetUtcNow());
|
||||
return builder.Build();
|
||||
}).ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<DeduplicatedEdge>>(passthrough);
|
||||
}
|
||||
|
||||
var result = _edgeDeduplicator.Deduplicate(edges);
|
||||
var result = _edgeDeduplicator.Deduplicate(
|
||||
edges,
|
||||
e => new EdgeSemanticKey(e.From, e.To),
|
||||
e => "deduplicated",
|
||||
e => e.Why.Confidence,
|
||||
e => _timeProvider.GetUtcNow());
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
@@ -438,13 +449,13 @@ public sealed class NoiseGateService : INoiseGate
|
||||
|
||||
// Add sorted edges
|
||||
var sortedEdges = edges
|
||||
.OrderBy(e => e.SemanticKey, StringComparer.Ordinal)
|
||||
.OrderBy(e => e.Key.ComputeKey(), StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
foreach (var edge in sortedEdges)
|
||||
{
|
||||
sb.Append('|');
|
||||
sb.Append(edge.SemanticKey);
|
||||
sb.Append(edge.Key.ComputeKey());
|
||||
}
|
||||
|
||||
// Add sorted verdicts
|
||||
|
||||
@@ -26,7 +26,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { inject } from '@angular/core';
|
||||
|
||||
import { SecretAlertSettings, SecretAlertDestination } from '../../models';
|
||||
import { SecretAlertSettings, SecretAlertDestination, SecretSeverity } from '../../models';
|
||||
import { SecretDetectionSettingsService } from '../../services';
|
||||
|
||||
@Component({
|
||||
@@ -403,7 +403,16 @@ export class AlertDestinationConfigComponent {
|
||||
private readonly settingsService = inject(SecretDetectionSettingsService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
|
||||
@Input() settings: SecretAlertSettings = { enabled: false, destinations: [] };
|
||||
@Input() settings: SecretAlertSettings = {
|
||||
enabled: false,
|
||||
destinations: [],
|
||||
minimumAlertSeverity: 'High',
|
||||
maxAlertsPerScan: 100,
|
||||
deduplicationWindowHours: 24,
|
||||
includeFilePath: true,
|
||||
includeMaskedValue: false,
|
||||
includeImageRef: true,
|
||||
};
|
||||
@Input() tenantId = '';
|
||||
@Output() settingsChange = new EventEmitter<SecretAlertSettings>();
|
||||
|
||||
@@ -421,7 +430,7 @@ export class AlertDestinationConfigComponent {
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
onMinSeverityChange(severity: string): void {
|
||||
onMinSeverityChange(severity: SecretSeverity): void {
|
||||
this.settings = { ...this.settings, minimumSeverity: severity };
|
||||
this.emitChange();
|
||||
}
|
||||
@@ -448,6 +457,8 @@ export class AlertDestinationConfigComponent {
|
||||
const newDest: SecretAlertDestination = {
|
||||
id: crypto.randomUUID(),
|
||||
name: '',
|
||||
channelType: 'Webhook',
|
||||
channelId: '',
|
||||
type: 'webhook',
|
||||
enabled: true,
|
||||
config: {},
|
||||
@@ -466,7 +477,7 @@ export class AlertDestinationConfigComponent {
|
||||
this.updateDestination(index, { name: input.value });
|
||||
}
|
||||
|
||||
onDestTypeChange(index: number, type: string): void {
|
||||
onDestTypeChange(index: number, type: 'webhook' | 'slack' | 'email' | 'teams' | 'pagerduty'): void {
|
||||
this.updateDestination(index, { type, config: {} });
|
||||
}
|
||||
|
||||
@@ -478,7 +489,7 @@ export class AlertDestinationConfigComponent {
|
||||
});
|
||||
}
|
||||
|
||||
onDestSeverityChange(index: number, severities: string[]): void {
|
||||
onDestSeverityChange(index: number, severities: SecretSeverity[]): void {
|
||||
this.updateDestination(index, { severityFilter: severities });
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import { MatCardModule } from '@angular/material/card';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
|
||||
import { SecretExceptionService } from '../../services';
|
||||
import { SecretExceptionPattern } from '../../models';
|
||||
import { SecretExceptionPattern, CreateExceptionRequest } from '../../models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-exception-manager',
|
||||
@@ -398,7 +398,7 @@ export class ExceptionManagerComponent implements OnInit {
|
||||
}
|
||||
|
||||
loadExceptions(): void {
|
||||
this.exceptionService.loadExceptions(this.tenantId).subscribe();
|
||||
this.exceptionService.listExceptions(this.tenantId).subscribe();
|
||||
}
|
||||
|
||||
openCreateDialog(): void {
|
||||
@@ -436,17 +436,18 @@ export class ExceptionManagerComponent implements OnInit {
|
||||
const formValue = this.exceptionForm.value;
|
||||
const existing = this.editingException();
|
||||
|
||||
const payload: Partial<SecretExceptionPattern> = {
|
||||
name: formValue.name,
|
||||
const payload: CreateExceptionRequest = {
|
||||
name: formValue.name ?? '',
|
||||
pattern: formValue.pattern ?? '',
|
||||
reason: formValue.description ?? 'Exception created via UI',
|
||||
matchType: formValue.matchType ?? 'literal',
|
||||
target: formValue.target ?? 'value',
|
||||
enabled: formValue.enabled ?? true,
|
||||
description: formValue.description || undefined,
|
||||
matchType: formValue.matchType,
|
||||
pattern: formValue.pattern,
|
||||
target: formValue.target,
|
||||
enabled: formValue.enabled,
|
||||
};
|
||||
|
||||
const operation = existing
|
||||
? this.exceptionService.updateException(existing.id, payload)
|
||||
? this.exceptionService.updateException(this.tenantId, existing.id, payload)
|
||||
: this.exceptionService.createException(this.tenantId, payload);
|
||||
|
||||
operation.subscribe({
|
||||
@@ -464,7 +465,7 @@ export class ExceptionManagerComponent implements OnInit {
|
||||
}
|
||||
|
||||
toggleEnabled(exception: SecretExceptionPattern): void {
|
||||
this.exceptionService.updateException(exception.id, {
|
||||
this.exceptionService.updateException(this.tenantId, exception.id, {
|
||||
enabled: !exception.enabled,
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
@@ -477,7 +478,7 @@ export class ExceptionManagerComponent implements OnInit {
|
||||
deleteException(exception: SecretExceptionPattern): void {
|
||||
if (!confirm(`Delete exception "${exception.name}"?`)) return;
|
||||
|
||||
this.exceptionService.deleteException(exception.id).subscribe({
|
||||
this.exceptionService.deleteException(this.tenantId, exception.id).subscribe({
|
||||
next: () => {
|
||||
this.showSuccess('Exception deleted');
|
||||
this.loadExceptions();
|
||||
|
||||
@@ -121,6 +121,7 @@ export class MaskedValueDisplayComponent {
|
||||
|
||||
@Input() value = '';
|
||||
@Input() findingId = '';
|
||||
@Input() tenantId = '';
|
||||
@Input() canReveal = false;
|
||||
|
||||
@Output() revealed = new EventEmitter<string>();
|
||||
@@ -138,12 +139,12 @@ export class MaskedValueDisplayComponent {
|
||||
};
|
||||
|
||||
reveal(): void {
|
||||
if (!this.canReveal || !this.findingId) return;
|
||||
if (!this.canReveal || !this.findingId || !this.tenantId) return;
|
||||
|
||||
this.revealing.set(true);
|
||||
|
||||
this.findingsService.revealValue(this.findingId).subscribe({
|
||||
next: (value) => {
|
||||
this.findingsService.revealValue(this.tenantId, this.findingId).subscribe({
|
||||
next: (value: string) => {
|
||||
this.revealedValue.set(value);
|
||||
this.isRevealed.set(true);
|
||||
this.revealing.set(false);
|
||||
|
||||
@@ -26,8 +26,8 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
|
||||
import { SecretFindingsService } from '../../services';
|
||||
import { SecretFinding } from '../../models';
|
||||
import { MaskedValueDisplayComponent } from './masked-value-display.component';
|
||||
import { SecretFinding, SecretSeverity, SecretFindingStatus } from '../../models';
|
||||
import { MaskedValueDisplayComponent } from './';
|
||||
|
||||
@Component({
|
||||
selector: 'app-secret-findings-list',
|
||||
@@ -426,7 +426,9 @@ export class SecretFindingsListComponent implements OnInit {
|
||||
|
||||
// Expose service signals
|
||||
readonly findings = this.findingsService.findings;
|
||||
readonly pagination = this.findingsService.pagination;
|
||||
readonly page = this.findingsService.page;
|
||||
readonly pageSize = this.findingsService.pageSize;
|
||||
readonly total = this.findingsService.total;
|
||||
readonly loading = this.findingsService.loading;
|
||||
readonly error = this.findingsService.error;
|
||||
|
||||
@@ -459,14 +461,11 @@ export class SecretFindingsListComponent implements OnInit {
|
||||
reload(): void {
|
||||
const tid = this.tenantId();
|
||||
if (tid) {
|
||||
this.findingsService.loadFindings(tid, {
|
||||
this.findingsService.listFindings(tid, {
|
||||
page: 1,
|
||||
pageSize: 25,
|
||||
search: this.searchQuery(),
|
||||
severity: this.severityFilter(),
|
||||
status: this.statusFilter(),
|
||||
sortBy: this.sortField(),
|
||||
sortDirection: this.sortDirection(),
|
||||
severity: this.severityFilter() as SecretSeverity[],
|
||||
status: this.statusFilter() ? [this.statusFilter() as SecretFindingStatus] : undefined,
|
||||
}).subscribe();
|
||||
}
|
||||
}
|
||||
@@ -496,14 +495,11 @@ export class SecretFindingsListComponent implements OnInit {
|
||||
onPageChange(event: PageEvent): void {
|
||||
const tid = this.tenantId();
|
||||
if (tid) {
|
||||
this.findingsService.loadFindings(tid, {
|
||||
this.findingsService.listFindings(tid, {
|
||||
page: event.pageIndex + 1,
|
||||
pageSize: event.pageSize,
|
||||
search: this.searchQuery(),
|
||||
severity: this.severityFilter(),
|
||||
status: this.statusFilter(),
|
||||
sortBy: this.sortField(),
|
||||
sortDirection: this.sortDirection(),
|
||||
severity: this.severityFilter() as SecretSeverity[],
|
||||
status: this.statusFilter() ? [this.statusFilter() as SecretFindingStatus] : undefined,
|
||||
}).subscribe();
|
||||
}
|
||||
}
|
||||
@@ -527,7 +523,8 @@ export class SecretFindingsListComponent implements OnInit {
|
||||
}
|
||||
|
||||
markResolved(finding: SecretFinding): void {
|
||||
this.findingsService.updateStatus(finding.id, 'resolved').subscribe({
|
||||
const tid = this.tenantId();
|
||||
this.findingsService.updateStatus(tid, finding.id, 'Resolved').subscribe({
|
||||
next: () => this.reload(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -260,7 +260,14 @@ type RevelationMode = 'masked' | 'partial' | 'full' | 'redacted';
|
||||
`]
|
||||
})
|
||||
export class RevelationPolicySelectorComponent {
|
||||
@Input() config: RevelationPolicyConfig = { mode: 'masked' };
|
||||
@Input() config: RevelationPolicyConfig = {
|
||||
defaultPolicy: 'FullMask',
|
||||
exportPolicy: 'FullMask',
|
||||
logPolicy: 'FullMask',
|
||||
fullRevealRoles: [],
|
||||
partialRevealChars: 4,
|
||||
mode: 'masked',
|
||||
};
|
||||
@Output() configChange = new EventEmitter<RevelationPolicyConfig>();
|
||||
|
||||
private readonly sampleSecret = 'ghp_abc123XYZ789secret';
|
||||
|
||||
@@ -19,10 +19,10 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { SecretDetectionSettingsService } from '../../services';
|
||||
import { RevelationPolicySelectorComponent } from '../settings/revelation-policy-selector.component';
|
||||
import { RuleCategoryTogglesComponent } from '../settings/rule-category-toggles.component';
|
||||
import { ExceptionManagerComponent } from '../exceptions/exception-manager.component';
|
||||
import { AlertDestinationConfigComponent } from '../alerts/alert-destination-config.component';
|
||||
import { RevelationPolicySelectorComponent } from './revelation-policy-selector.component';
|
||||
import { RuleCategoryTogglesComponent } from './rule-category-toggles.component';
|
||||
import { ExceptionManagerComponent } from '../exceptions';
|
||||
import { AlertDestinationConfigComponent } from '../alerts';
|
||||
import { RevelationPolicyConfig, SecretAlertSettings } from '../../models';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -24,6 +24,11 @@ export interface SecretDetectionSettings {
|
||||
*/
|
||||
export type SecretRevelationPolicy = 'FullMask' | 'PartialReveal' | 'FullReveal';
|
||||
|
||||
/**
|
||||
* Revelation mode for display settings.
|
||||
*/
|
||||
export type RevelationMode = 'masked' | 'partial' | 'full' | 'redacted';
|
||||
|
||||
/**
|
||||
* Configuration for revelation policies by context.
|
||||
*/
|
||||
@@ -33,6 +38,18 @@ export interface RevelationPolicyConfig {
|
||||
logPolicy: SecretRevelationPolicy;
|
||||
fullRevealRoles: string[];
|
||||
partialRevealChars: number;
|
||||
/** Display mode for UI presentation */
|
||||
mode?: RevelationMode;
|
||||
/** Character used for masking */
|
||||
maskChar?: string;
|
||||
/** Length of the mask */
|
||||
maskLength?: number;
|
||||
/** Number of characters to reveal at start */
|
||||
revealFirst?: number;
|
||||
/** Number of characters to reveal at end */
|
||||
revealLast?: number;
|
||||
/** Required permission for full reveal */
|
||||
requiredPermission?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,6 +64,16 @@ export interface SecretExceptionPattern {
|
||||
expiresAt?: string;
|
||||
ruleIds?: string[];
|
||||
pathFilter?: string;
|
||||
/** Display name for the exception */
|
||||
name?: string;
|
||||
/** Description of why exception exists */
|
||||
description?: string;
|
||||
/** How the pattern is matched */
|
||||
matchType?: 'literal' | 'regex' | 'glob' | 'prefix' | 'suffix';
|
||||
/** What the pattern applies to */
|
||||
target?: 'value' | 'path' | 'filename';
|
||||
/** Whether the exception is active */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,6 +89,12 @@ export interface SecretAlertSettings {
|
||||
includeMaskedValue: boolean;
|
||||
includeImageRef: boolean;
|
||||
alertMessagePrefix?: string;
|
||||
/** Minimum severity to trigger alert (UI variant) */
|
||||
minimumSeverity?: SecretSeverity;
|
||||
/** Rate limit per hour */
|
||||
rateLimitPerHour?: number;
|
||||
/** Deduplication window in minutes (UI variant) */
|
||||
deduplicationWindowMinutes?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,6 +107,12 @@ export interface SecretAlertDestination {
|
||||
channelId: string;
|
||||
severityFilter?: SecretSeverity[];
|
||||
ruleCategoryFilter?: string[];
|
||||
/** Destination type (UI variant) */
|
||||
type?: 'webhook' | 'slack' | 'email' | 'teams' | 'pagerduty';
|
||||
/** Configuration options */
|
||||
config?: Record<string, unknown>;
|
||||
/** Whether destination is enabled */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,6 +159,8 @@ export interface SecretRuleCategory {
|
||||
description: string;
|
||||
ruleCount: number;
|
||||
enabled: boolean;
|
||||
/** Group for categorization */
|
||||
group?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,6 +172,16 @@ export interface CreateExceptionRequest {
|
||||
expiresAt?: string;
|
||||
ruleIds?: string[];
|
||||
pathFilter?: string;
|
||||
/** Display name for the exception */
|
||||
name?: string;
|
||||
/** Description of why exception exists */
|
||||
description?: string;
|
||||
/** How the pattern is matched */
|
||||
matchType?: 'literal' | 'regex' | 'glob' | 'prefix' | 'suffix';
|
||||
/** What the pattern applies to */
|
||||
target?: 'value' | 'path' | 'filename';
|
||||
/** Whether the exception is active */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
import { catchError, tap, map } from 'rxjs/operators';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import {
|
||||
SecretExceptionPattern,
|
||||
@@ -47,12 +47,10 @@ export class SecretExceptionService {
|
||||
catchError(err => {
|
||||
this._error.set(err.message || 'Failed to load exceptions');
|
||||
this._loading.set(false);
|
||||
return of({ items: [] });
|
||||
return of({ items: [] as SecretExceptionPattern[] });
|
||||
}),
|
||||
// Transform response
|
||||
tap(() => {}),
|
||||
// Return just the items array
|
||||
tap(response => this._exceptions.set(response.items))
|
||||
// Map to array
|
||||
map(response => response.items)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { Injectable, inject, signal, computed } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
import { catchError, tap, map } from 'rxjs/operators';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import {
|
||||
SecretFinding,
|
||||
@@ -157,6 +157,21 @@ export class SecretFindingsService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveals the secret value for a finding (requires appropriate permissions).
|
||||
*/
|
||||
revealValue(tenantId: string, findingId: string): Observable<string> {
|
||||
return this.http.get<{ value: string }>(
|
||||
`${this.baseUrl}/${tenantId}/${findingId}/reveal`
|
||||
).pipe(
|
||||
map(response => response.value),
|
||||
catchError(err => {
|
||||
this._error.set(err.message || 'Failed to reveal secret value');
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a finding for detail view.
|
||||
*/
|
||||
|
||||
@@ -249,7 +249,7 @@ public sealed class RuntimeAdmissionPolicyServiceTests
|
||||
new[] { "admission" },
|
||||
new SurfaceSecretsConfiguration("inline", "default", null, null, null, true),
|
||||
"default",
|
||||
new SurfaceTlsConfiguration(null, null, null));
|
||||
new SurfaceTlsConfiguration(null, null, null)) { CreatedAtUtc = DateTimeOffset.UtcNow };
|
||||
|
||||
private sealed class StubSurfaceFsClient : IWebhookSurfaceFsClient
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace StellaOps.Cryptography.Kms;
|
||||
public sealed class AwsKmsClient : IKmsClient, IDisposable
|
||||
{
|
||||
private readonly IAwsKmsFacade _facade;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly TimeSpan _metadataCacheDuration;
|
||||
private readonly TimeSpan _publicKeyCacheDuration;
|
||||
|
||||
@@ -18,11 +19,12 @@ public sealed class AwsKmsClient : IKmsClient, IDisposable
|
||||
private readonly ConcurrentDictionary<string, CachedPublicKey> _publicKeyCache = new(StringComparer.Ordinal);
|
||||
private bool _disposed;
|
||||
|
||||
public AwsKmsClient(IAwsKmsFacade facade, AwsKmsOptions options)
|
||||
public AwsKmsClient(IAwsKmsFacade facade, AwsKmsOptions options, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_facade = facade ?? throw new ArgumentNullException(nameof(facade));
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_metadataCacheDuration = options.MetadataCacheDuration;
|
||||
_publicKeyCacheDuration = options.PublicKeyCacheDuration;
|
||||
}
|
||||
@@ -156,7 +158,7 @@ public sealed class AwsKmsClient : IKmsClient, IDisposable
|
||||
|
||||
private async Task<AwsKeyMetadata> GetCachedMetadataAsync(string keyId, CancellationToken cancellationToken)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
if (_metadataCache.TryGetValue(keyId, out var cached) && cached.ExpiresAt > now)
|
||||
{
|
||||
return cached.Metadata;
|
||||
@@ -170,7 +172,7 @@ public sealed class AwsKmsClient : IKmsClient, IDisposable
|
||||
|
||||
private async Task<AwsPublicKeyMaterial> GetCachedPublicKeyAsync(string resource, CancellationToken cancellationToken)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
if (_publicKeyCache.TryGetValue(resource, out var cached) && cached.ExpiresAt > now)
|
||||
{
|
||||
return cached.Material;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user