audit, advisories and doctors/setup work

This commit is contained in:
master
2026-01-13 18:53:39 +02:00
parent 9ca7cb183e
commit d7be6ba34b
811 changed files with 54242 additions and 4056 deletions

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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))

View File

@@ -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();

View File

@@ -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!,