audit, advisories and doctors/setup work
This commit is contained in:
@@ -12,11 +12,14 @@ namespace StellaOps.Excititor.Worker.Auth;
|
||||
/// </summary>
|
||||
public sealed class TenantAuthorityClientFactory : ITenantAuthorityClientFactory
|
||||
{
|
||||
internal const string AuthorityClientName = "StellaOps.Excititor.Worker.Authority";
|
||||
private readonly TenantAuthorityOptions _options;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public TenantAuthorityClientFactory(IOptions<TenantAuthorityOptions> options)
|
||||
public TenantAuthorityClientFactory(IOptions<TenantAuthorityOptions> options, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
||||
}
|
||||
|
||||
public HttpClient Create(string tenant)
|
||||
@@ -31,10 +34,8 @@ public sealed class TenantAuthorityClientFactory : ITenantAuthorityClientFactory
|
||||
throw new InvalidOperationException($"Authority base URL not configured for tenant '{tenant}'.");
|
||||
}
|
||||
|
||||
var client = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(baseUrl, UriKind.Absolute),
|
||||
};
|
||||
var client = _httpClientFactory.CreateClient(AuthorityClientName);
|
||||
client.BaseAddress = new Uri(baseUrl, UriKind.Absolute);
|
||||
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
client.DefaultRequestHeaders.Add("X-Tenant", tenant);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Excititor.Worker.Orchestration;
|
||||
|
||||
internal interface IGuidGenerator
|
||||
{
|
||||
Guid NewGuid();
|
||||
}
|
||||
|
||||
internal sealed class DefaultGuidGenerator : IGuidGenerator
|
||||
{
|
||||
public Guid NewGuid() => Guid.NewGuid();
|
||||
}
|
||||
@@ -28,6 +28,7 @@ internal sealed class VexWorkerOrchestratorClient : IVexWorkerOrchestratorClient
|
||||
private readonly IVexConnectorStateRepository _stateRepository;
|
||||
private readonly IAppendOnlyCheckpointStore? _checkpointStore;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidGenerator _guidGenerator;
|
||||
private readonly IOptions<VexWorkerOrchestratorOptions> _options;
|
||||
private readonly ILogger<VexWorkerOrchestratorClient> _logger;
|
||||
private readonly HttpClient? _httpClient;
|
||||
@@ -38,6 +39,7 @@ internal sealed class VexWorkerOrchestratorClient : IVexWorkerOrchestratorClient
|
||||
public VexWorkerOrchestratorClient(
|
||||
IVexConnectorStateRepository stateRepository,
|
||||
TimeProvider timeProvider,
|
||||
IGuidGenerator guidGenerator,
|
||||
IOptions<VexWorkerOrchestratorOptions> options,
|
||||
ILogger<VexWorkerOrchestratorClient> logger,
|
||||
HttpClient? httpClient = null,
|
||||
@@ -46,6 +48,7 @@ internal sealed class VexWorkerOrchestratorClient : IVexWorkerOrchestratorClient
|
||||
_stateRepository = stateRepository ?? throw new ArgumentNullException(nameof(stateRepository));
|
||||
_checkpointStore = checkpointStore;
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidGenerator = guidGenerator ?? throw new ArgumentNullException(nameof(guidGenerator));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_httpClient = httpClient;
|
||||
@@ -63,7 +66,7 @@ internal sealed class VexWorkerOrchestratorClient : IVexWorkerOrchestratorClient
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var startedAt = _timeProvider.GetUtcNow();
|
||||
var fallbackContext = new VexWorkerJobContext(tenant, connectorId, Guid.NewGuid(), checkpoint, startedAt);
|
||||
var fallbackContext = new VexWorkerJobContext(tenant, connectorId, _guidGenerator.NewGuid(), checkpoint, startedAt);
|
||||
|
||||
if (!CanUseRemote())
|
||||
{
|
||||
@@ -479,7 +482,7 @@ internal sealed class VexWorkerOrchestratorClient : IVexWorkerOrchestratorClient
|
||||
return null;
|
||||
}
|
||||
|
||||
return DateTimeOffset.TryParse(checkpoint, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed)
|
||||
return DateTimeOffset.TryParse(checkpoint, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ using StellaOps.Excititor.Persistence.Extensions;
|
||||
using StellaOps.Excititor.Worker.Auth;
|
||||
using StellaOps.Excititor.Worker.Options;
|
||||
using StellaOps.Excititor.Worker.Orchestration;
|
||||
using StellaOps.Excititor.Worker.Plugins;
|
||||
using StellaOps.Excititor.Worker.Scheduling;
|
||||
using StellaOps.Excititor.Worker.Signature;
|
||||
using StellaOps.Excititor.Attestation.Extensions;
|
||||
@@ -51,9 +52,9 @@ services.AddOptions<VexStorageOptions>()
|
||||
.ValidateOnStart();
|
||||
|
||||
services.AddExcititorPersistence(configuration);
|
||||
services.AddSingleton<IVexProviderStore, InMemoryVexProviderStore>();
|
||||
services.TryAddSingleton<IVexProviderStore, InMemoryVexProviderStore>();
|
||||
services.TryAddScoped<IVexConnectorStateRepository, InMemoryVexConnectorStateRepository>();
|
||||
services.AddSingleton<IVexClaimStore, InMemoryVexClaimStore>();
|
||||
services.TryAddSingleton<IVexClaimStore, InMemoryVexClaimStore>();
|
||||
services.AddCsafNormalizer();
|
||||
services.AddCycloneDxNormalizer();
|
||||
services.AddOpenVexNormalizer();
|
||||
@@ -82,6 +83,7 @@ services.AddExcititorAocGuards();
|
||||
|
||||
services.AddSingleton<IValidateOptions<VexWorkerOptions>, VexWorkerOptionsValidator>();
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
services.TryAddSingleton<IGuidGenerator, DefaultGuidGenerator>();
|
||||
services.PostConfigure<VexWorkerOptions>(options =>
|
||||
{
|
||||
if (!options.Providers.Any(provider => string.Equals(provider.ProviderId, "excititor:redhat", StringComparison.OrdinalIgnoreCase)))
|
||||
@@ -93,40 +95,15 @@ services.PostConfigure<VexWorkerOptions>(options =>
|
||||
}
|
||||
});
|
||||
// Load VEX connector plugins
|
||||
services.AddSingleton<VexWorkerPluginCatalogLoader>();
|
||||
services.AddSingleton<PluginCatalog>(provider =>
|
||||
{
|
||||
var opts = provider.GetRequiredService<IOptions<VexWorkerPluginOptions>>().Value;
|
||||
var catalog = new PluginCatalog();
|
||||
|
||||
var directory = opts.ResolveDirectory();
|
||||
if (Directory.Exists(directory))
|
||||
{
|
||||
catalog.AddFromDirectory(directory, opts.ResolveSearchPattern());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: try loading from plugins/excititor directory
|
||||
var fallbackPath = Path.Combine(AppContext.BaseDirectory, "plugins", "excititor");
|
||||
if (Directory.Exists(fallbackPath))
|
||||
{
|
||||
catalog.AddFromDirectory(fallbackPath, "StellaOps.Excititor.Connectors.*.dll");
|
||||
}
|
||||
else
|
||||
{
|
||||
var logger = provider.GetRequiredService<ILogger<Program>>();
|
||||
logger.LogWarning(
|
||||
"Excititor worker plugin directory '{Directory}' does not exist; proceeding without external connectors.",
|
||||
directory);
|
||||
}
|
||||
}
|
||||
|
||||
return catalog;
|
||||
});
|
||||
provider.GetRequiredService<VexWorkerPluginCatalogLoader>().Load().Catalog);
|
||||
|
||||
// Orchestrator worker SDK integration
|
||||
services.AddOptions<VexWorkerOrchestratorOptions>()
|
||||
.Bind(configuration.GetSection("Excititor:Worker:Orchestrator"))
|
||||
.ValidateOnStart();
|
||||
services.AddHttpClient(TenantAuthorityClientFactory.AuthorityClientName);
|
||||
services.AddHttpClient<IVexWorkerOrchestratorClient, VexWorkerOrchestratorClient>((provider, client) =>
|
||||
{
|
||||
var opts = provider.GetRequiredService<IOptions<VexWorkerOrchestratorOptions>>().Value;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -206,7 +208,13 @@ internal sealed class DefaultVexProviderRunner : IVexProviderRunner
|
||||
await SafeWaitForTaskAsync(heartbeatTask).ConfigureAwait(false);
|
||||
|
||||
var error = VexWorkerError.Cancelled("Operation cancelled by host");
|
||||
await _orchestratorClient.FailJobAsync(jobContext, error, CancellationToken.None).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await _orchestratorClient.FailJobAsync(jobContext, error, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
@@ -221,7 +229,7 @@ internal sealed class DefaultVexProviderRunner : IVexProviderRunner
|
||||
|
||||
// Apply backoff delay for retryable errors
|
||||
var retryDelay = classifiedError.Retryable
|
||||
? (int)CalculateDelayWithJitter(1).TotalSeconds
|
||||
? (int)CalculateDelayWithJitter(connector.Id, (stateBeforeRun?.FailureCount ?? 0) + 1).TotalSeconds
|
||||
: (int?)null;
|
||||
|
||||
var errorWithRetry = classifiedError.Retryable && retryDelay.HasValue
|
||||
@@ -235,7 +243,13 @@ internal sealed class DefaultVexProviderRunner : IVexProviderRunner
|
||||
classifiedError.Details)
|
||||
: classifiedError;
|
||||
|
||||
await _orchestratorClient.FailJobAsync(jobContext, errorWithRetry, CancellationToken.None).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await _orchestratorClient.FailJobAsync(jobContext, errorWithRetry, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
|
||||
await UpdateFailureStateAsync(stateRepository, descriptor.Id, _timeProvider.GetUtcNow(), ex, classifiedError.Retryable, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
@@ -291,7 +305,7 @@ internal sealed class DefaultVexProviderRunner : IVexProviderRunner
|
||||
if (retryable)
|
||||
{
|
||||
// Apply exponential backoff for retryable errors
|
||||
var delay = CalculateDelayWithJitter(failureCount);
|
||||
var delay = CalculateDelayWithJitter(connectorId, failureCount);
|
||||
nextEligible = failureTime + delay;
|
||||
|
||||
if (failureCount >= _retryOptions.FailureThreshold)
|
||||
@@ -338,7 +352,7 @@ internal sealed class DefaultVexProviderRunner : IVexProviderRunner
|
||||
nextEligible);
|
||||
}
|
||||
|
||||
private TimeSpan CalculateDelayWithJitter(int failureCount)
|
||||
internal TimeSpan CalculateDelayWithJitter(string connectorId, int failureCount)
|
||||
{
|
||||
var exponent = Math.Max(0, failureCount - 1);
|
||||
var factor = Math.Pow(2, exponent);
|
||||
@@ -351,25 +365,24 @@ internal sealed class DefaultVexProviderRunner : IVexProviderRunner
|
||||
|
||||
var minFactor = 1.0 - _retryOptions.JitterRatio;
|
||||
var maxFactor = 1.0 + _retryOptions.JitterRatio;
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
RandomNumberGenerator.Fill(buffer);
|
||||
var sample = BitConverter.ToUInt64(buffer) / (double)ulong.MaxValue;
|
||||
var sample = ComputeDeterministicSample(connectorId, failureCount);
|
||||
var jitterFactor = minFactor + (maxFactor - minFactor) * sample;
|
||||
var jitteredTicks = (long)Math.Round(baselineTicks * jitterFactor);
|
||||
var jitteredTicks = (long)Math.Round(baselineTicks * jitterFactor, MidpointRounding.AwayFromZero);
|
||||
|
||||
if (jitteredTicks < _retryOptions.BaseDelay.Ticks)
|
||||
{
|
||||
jitteredTicks = _retryOptions.BaseDelay.Ticks;
|
||||
}
|
||||
|
||||
if (jitteredTicks > _retryOptions.MaxDelay.Ticks)
|
||||
{
|
||||
jitteredTicks = _retryOptions.MaxDelay.Ticks;
|
||||
}
|
||||
jitteredTicks = Math.Clamp(jitteredTicks, _retryOptions.BaseDelay.Ticks, _retryOptions.MaxDelay.Ticks);
|
||||
|
||||
return TimeSpan.FromTicks(jitteredTicks);
|
||||
}
|
||||
|
||||
internal static double ComputeDeterministicSample(string connectorId, int failureCount)
|
||||
{
|
||||
var normalizedId = string.IsNullOrWhiteSpace(connectorId) ? "unknown" : connectorId.Trim();
|
||||
var input = string.Concat(normalizedId, ":", failureCount.ToString(CultureInfo.InvariantCulture));
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
var value = BinaryPrimitives.ReadUInt64BigEndian(hash.AsSpan(0, 8));
|
||||
return value / (double)ulong.MaxValue;
|
||||
}
|
||||
|
||||
private static string Truncate(string? value, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
|
||||
@@ -63,7 +63,7 @@ internal sealed class VexWorkerHostedService : BackgroundService
|
||||
await Task.Delay(schedule.InitialDelay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
using var timer = new PeriodicTimer(schedule.Interval);
|
||||
using var timer = new PeriodicTimer(schedule.Interval, _timeProvider);
|
||||
do
|
||||
{
|
||||
var startedAt = _timeProvider.GetUtcNow();
|
||||
|
||||
@@ -153,7 +153,7 @@ internal sealed class WorkerSignatureVerifier : IVexSignatureVerifier
|
||||
|
||||
var predicate = statement.Predicate ?? throw new InvalidOperationException("DSSE predicate is missing.");
|
||||
var request = BuildAttestationRequest(statement, predicate);
|
||||
var attestationMetadata = BuildAttestationMetadata(statement, envelope, metadata);
|
||||
var attestationMetadata = BuildAttestationMetadata(statement, envelope, metadata, document.RetrievedAt);
|
||||
|
||||
var verificationRequest = new VexAttestationVerificationRequest(
|
||||
request,
|
||||
@@ -251,7 +251,8 @@ internal sealed class WorkerSignatureVerifier : IVexSignatureVerifier
|
||||
private VexAttestationMetadata BuildAttestationMetadata(
|
||||
VexInTotoStatement statement,
|
||||
DsseEnvelope envelope,
|
||||
ImmutableDictionary<string, string> metadata)
|
||||
ImmutableDictionary<string, string> metadata,
|
||||
DateTimeOffset fallbackSignedAt)
|
||||
{
|
||||
VexRekorReference? rekor = null;
|
||||
if (metadata.TryGetValue("vex.signature.transparencyLogReference", out var rekorValue) && !string.IsNullOrWhiteSpace(rekorValue))
|
||||
@@ -259,7 +260,7 @@ internal sealed class WorkerSignatureVerifier : IVexSignatureVerifier
|
||||
rekor = new VexRekorReference("0.1", rekorValue);
|
||||
}
|
||||
|
||||
DateTimeOffset signedAt;
|
||||
DateTimeOffset? signedAt = null;
|
||||
if (metadata.TryGetValue("vex.signature.verifiedAt", out var signedAtRaw)
|
||||
&& DateTimeOffset.TryParse(signedAtRaw, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsedSignedAt))
|
||||
{
|
||||
@@ -267,7 +268,7 @@ internal sealed class WorkerSignatureVerifier : IVexSignatureVerifier
|
||||
}
|
||||
else
|
||||
{
|
||||
signedAt = _timeProvider.GetUtcNow();
|
||||
signedAt = fallbackSignedAt;
|
||||
}
|
||||
|
||||
return new VexAttestationMetadata(
|
||||
@@ -320,7 +321,7 @@ internal sealed class WorkerSignatureVerifier : IVexSignatureVerifier
|
||||
keyId = diagnosticKeyId;
|
||||
}
|
||||
|
||||
var verifiedAt = attestationMetadata.SignedAt ?? _timeProvider.GetUtcNow();
|
||||
var verifiedAt = attestationMetadata.SignedAt;
|
||||
|
||||
return new VexSignatureMetadata(
|
||||
type!,
|
||||
|
||||
Reference in New Issue
Block a user