using System.Collections.Generic; using System.Linq; using System.Collections.Immutable; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using StellaOps.Excititor.Attestation.Extensions; using StellaOps.Excititor.Attestation; using StellaOps.Excititor.Attestation.Transparency; using StellaOps.Excititor.ArtifactStores.S3.Extensions; using StellaOps.Excititor.Connectors.RedHat.CSAF.DependencyInjection; using StellaOps.Excititor.Core; using StellaOps.Excititor.Export; using StellaOps.Excititor.Formats.CSAF; using StellaOps.Excititor.Formats.CycloneDX; using StellaOps.Excititor.Formats.OpenVEX; using StellaOps.Excititor.Policy; using StellaOps.Excititor.Storage.Mongo; using StellaOps.Excititor.WebService.Endpoints; using StellaOps.Excititor.WebService.Services; using StellaOps.Excititor.Core; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; var services = builder.Services; services.AddOptions() .Bind(configuration.GetSection("Excititor:Storage:Mongo")) .ValidateOnStart(); services.AddExcititorMongoStorage(); services.AddCsafNormalizer(); services.AddCycloneDxNormalizer(); services.AddOpenVexNormalizer(); services.AddSingleton(); services.AddScoped(); services.AddVexExportEngine(); services.AddVexExportCacheServices(); services.AddVexAttestation(); services.Configure(configuration.GetSection("Excititor:Attestation:Client")); services.AddVexPolicy(); services.AddRedHatCsafConnector(); services.Configure(configuration.GetSection(MirrorDistributionOptions.SectionName)); services.AddSingleton(); var rekorSection = configuration.GetSection("Excititor:Attestation:Rekor"); if (rekorSection.Exists()) { services.AddVexRekorClient(opts => rekorSection.Bind(opts)); } var fileSystemSection = configuration.GetSection("Excititor:Artifacts:FileSystem"); if (fileSystemSection.Exists()) { services.AddVexFileSystemArtifactStore(opts => fileSystemSection.Bind(opts)); } else { services.AddVexFileSystemArtifactStore(_ => { }); } var s3Section = configuration.GetSection("Excititor:Artifacts:S3"); if (s3Section.Exists()) { services.AddVexS3ArtifactClient(opts => s3Section.GetSection("Client").Bind(opts)); services.AddSingleton(provider => { var options = new S3ArtifactStoreOptions(); s3Section.GetSection("Store").Bind(options); return new S3ArtifactStore( provider.GetRequiredService(), Microsoft.Extensions.Options.Options.Create(options), provider.GetRequiredService>()); }); } var offlineSection = configuration.GetSection("Excititor:Artifacts:OfflineBundle"); if (offlineSection.Exists()) { services.AddVexOfflineBundleArtifactStore(opts => offlineSection.Bind(opts)); } services.AddEndpointsApiExplorer(); services.AddHealthChecks(); services.AddSingleton(TimeProvider.System); services.AddMemoryCache(); services.AddAuthentication(); services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapGet("/excititor/status", async (HttpContext context, IEnumerable artifactStores, IOptions mongoOptions, TimeProvider timeProvider) => { var payload = new StatusResponse( timeProvider.GetUtcNow(), mongoOptions.Value.RawBucketName, mongoOptions.Value.GridFsInlineThresholdBytes, artifactStores.Select(store => store.GetType().Name).ToArray()); context.Response.ContentType = "application/json"; await System.Text.Json.JsonSerializer.SerializeAsync(context.Response.Body, payload); }); app.MapHealthChecks("/excititor/health"); app.MapPost("/excititor/statements", async ( VexStatementIngestRequest request, IVexClaimStore claimStore, TimeProvider timeProvider, CancellationToken cancellationToken) => { if (request?.Statements is null || request.Statements.Count == 0) { return Results.BadRequest("At least one statement must be provided."); } var claims = request.Statements.Select(statement => statement.ToDomainClaim()); await claimStore.AppendAsync(claims, timeProvider.GetUtcNow(), cancellationToken).ConfigureAwait(false); return Results.Accepted(); }); app.MapGet("/excititor/statements/{vulnerabilityId}/{productKey}", async ( string vulnerabilityId, string productKey, DateTimeOffset? since, IVexClaimStore claimStore, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(vulnerabilityId) || string.IsNullOrWhiteSpace(productKey)) { return Results.BadRequest("vulnerabilityId and productKey are required."); } var claims = await claimStore.FindAsync(vulnerabilityId.Trim(), productKey.Trim(), since, cancellationToken).ConfigureAwait(false); return Results.Ok(claims); }); app.MapPost("/excititor/admin/backfill-statements", async ( VexStatementBackfillRequest? request, VexStatementBackfillService backfillService, CancellationToken cancellationToken) => { request ??= new VexStatementBackfillRequest(); var result = await backfillService.RunAsync(request, cancellationToken).ConfigureAwait(false); var message = FormattableString.Invariant( $"Backfill completed: evaluated {result.DocumentsEvaluated}, backfilled {result.DocumentsBackfilled}, claims written {result.ClaimsWritten}, skipped {result.SkippedExisting}, failures {result.NormalizationFailures}."); return Results.Ok(new { message, summary = result }); }); IngestEndpoints.MapIngestEndpoints(app); ResolveEndpoint.MapResolveEndpoint(app); MirrorEndpoints.MapMirrorEndpoints(app); app.Run(); public partial class Program; internal sealed record StatusResponse(DateTimeOffset UtcNow, string MongoBucket, int InlineThreshold, string[] ArtifactStores); internal sealed record VexStatementIngestRequest(IReadOnlyList Statements); internal sealed record VexStatementEntry( string VulnerabilityId, string ProviderId, string ProductKey, string? ProductName, string? ProductVersion, string? ProductPurl, string? ProductCpe, IReadOnlyList? ComponentIdentifiers, VexClaimStatus Status, VexJustification? Justification, string? Detail, DateTimeOffset FirstSeen, DateTimeOffset LastSeen, VexDocumentFormat DocumentFormat, string DocumentDigest, string DocumentUri, string? DocumentRevision, VexSignatureMetadataRequest? Signature, VexConfidenceRequest? Confidence, VexSignalRequest? Signals, IReadOnlyDictionary? Metadata) { public VexClaim ToDomainClaim() { var product = new VexProduct( ProductKey, ProductName, ProductVersion, ProductPurl, ProductCpe, ComponentIdentifiers ?? Array.Empty()); if (!Uri.TryCreate(DocumentUri, UriKind.Absolute, out var uri)) { throw new InvalidOperationException($"DocumentUri '{DocumentUri}' is not a valid absolute URI."); } var document = new VexClaimDocument( DocumentFormat, DocumentDigest, uri, DocumentRevision, Signature?.ToDomain()); var additionalMetadata = Metadata is null ? ImmutableDictionary.Empty : Metadata.ToImmutableDictionary(StringComparer.Ordinal); return new VexClaim( VulnerabilityId, ProviderId, product, Status, document, FirstSeen, LastSeen, Justification, Detail, Confidence?.ToDomain(), Signals?.ToDomain(), additionalMetadata); } } internal sealed record VexSignatureMetadataRequest( string Type, string? Subject, string? Issuer, string? KeyId, DateTimeOffset? VerifiedAt, string? TransparencyLogReference) { public VexSignatureMetadata ToDomain() => new(Type, Subject, Issuer, KeyId, VerifiedAt, TransparencyLogReference); } internal sealed record VexConfidenceRequest(string Level, double? Score, string? Method) { public VexConfidence ToDomain() => new(Level, Score, Method); } internal sealed record VexSignalRequest(VexSeveritySignalRequest? Severity, bool? Kev, double? Epss) { public VexSignalSnapshot ToDomain() => new(Severity?.ToDomain(), Kev, Epss); } internal sealed record VexSeveritySignalRequest(string Scheme, double? Score, string? Label, string? Vector) { public VexSeveritySignal ToDomain() => new(Scheme, Score, Label, Vector); }