using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using MongoDB.Bson; using StellaOps.Excititor.Core; using StellaOps.Excititor.Core.Aoc; using StellaOps.Excititor.Storage.Mongo; using StellaOps.Excititor.WebService.Contracts; using StellaOps.Excititor.WebService.Services; public partial class Program { private const string TenantHeaderName = "X-Stella-Tenant"; private static bool TryResolveTenant(HttpContext context, VexMongoStorageOptions options, bool requireHeader, out string tenant, out IResult? problem) { tenant = options.DefaultTenant; problem = null; if (context.Request.Headers.TryGetValue(TenantHeaderName, out var headerValues) && headerValues.Count > 0) { var requestedTenant = headerValues[0]?.Trim(); if (string.IsNullOrEmpty(requestedTenant)) { problem = Results.Problem(detail: "X-Stella-Tenant header must not be empty.", statusCode: StatusCodes.Status400BadRequest, title: "Validation error"); return false; } if (!string.Equals(requestedTenant, options.DefaultTenant, StringComparison.OrdinalIgnoreCase)) { var detail = string.Format(CultureInfo.InvariantCulture, "Tenant '{0}' is not allowed for this Excititor deployment.", requestedTenant); problem = Results.Problem(detail: detail, statusCode: StatusCodes.Status403Forbidden, title: "Forbidden"); return false; } tenant = requestedTenant; return true; } if (requireHeader) { var detail = string.Format(CultureInfo.InvariantCulture, "{0} header is required.", TenantHeaderName); problem = Results.Problem(detail: detail, statusCode: StatusCodes.Status400BadRequest, title: "Validation error"); return false; } return true; } private static IReadOnlyDictionary ReadMetadata(BsonValue value) { if (value is not BsonDocument doc || doc.ElementCount == 0) { return new Dictionary(StringComparer.Ordinal); } var result = new Dictionary(StringComparer.Ordinal); foreach (var element in doc.Elements) { if (string.IsNullOrWhiteSpace(element.Name)) { continue; } result[element.Name] = element.Value?.ToString() ?? string.Empty; } return result; } private static bool TryDecodeCursor(string? cursor, out DateTimeOffset timestamp, out string digest) { timestamp = default; digest = string.Empty; if (string.IsNullOrWhiteSpace(cursor)) { return false; } try { var payload = Encoding.UTF8.GetString(Convert.FromBase64String(cursor)); var parts = payload.Split('|'); if (parts.Length != 2) { return false; } if (!DateTimeOffset.TryParse(parts[0], CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out timestamp)) { return false; } digest = parts[1]; return true; } catch { return false; } } private static string EncodeCursor(DateTime timestamp, string digest) { var payload = string.Format(CultureInfo.InvariantCulture, "{0:O}|{1}", timestamp, digest); return Convert.ToBase64String(Encoding.UTF8.GetBytes(payload)); } private static IResult ValidationProblem(string message) => Results.Problem(detail: message, statusCode: StatusCodes.Status400BadRequest, title: "Validation error"); private static IResult MapGuardException(ExcititorAocGuardException exception) { var violations = exception.Violations.Select(violation => new { code = violation.ErrorCode, path = violation.Path, message = violation.Message }); return Results.Problem( detail: "VEX document failed Aggregation-Only Contract validation.", statusCode: StatusCodes.Status400BadRequest, title: "AOC violation", extensions: new Dictionary { ["violations"] = violations.ToArray(), ["primaryCode"] = exception.PrimaryErrorCode, }); } private static ImmutableHashSet BuildStringFilterSet(StringValues values) { if (values.Count == 0) { return ImmutableHashSet.Empty; } var builder = ImmutableHashSet.CreateBuilder(StringComparer.OrdinalIgnoreCase); foreach (var value in values) { if (!string.IsNullOrWhiteSpace(value)) { builder.Add(value.Trim()); } } return builder.ToImmutable(); } private static ImmutableHashSet BuildStatusFilter(StringValues values) { if (values.Count == 0) { return ImmutableHashSet.Empty; } var builder = ImmutableHashSet.CreateBuilder(); foreach (var value in values) { if (Enum.TryParse(value, ignoreCase: true, out var status)) { builder.Add(status); } } return builder.ToImmutable(); } private static DateTimeOffset? ParseSinceTimestamp(StringValues values) { if (values.Count == 0) { return null; } var candidate = values[0]; return DateTimeOffset.TryParse(candidate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed) ? parsed : null; } private static int ResolveLimit(StringValues values, int defaultValue, int min, int max) { if (values.Count == 0) { return defaultValue; } if (!int.TryParse(values[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)) { return defaultValue; } return Math.Clamp(parsed, min, max); } private static IReadOnlyList NormalizePurls(string[]? purls) { if (purls is null || purls.Length == 0) { return Array.Empty(); } var seen = new HashSet(StringComparer.OrdinalIgnoreCase); var ordered = new List(purls.Length); foreach (var purl in purls) { var trimmed = purl?.Trim(); if (string.IsNullOrWhiteSpace(trimmed)) { continue; } var normalized = trimmed.ToLowerInvariant(); if (seen.Add(normalized)) { ordered.Add(normalized); } } return ordered; } private static VexObservationStatementResponse ToResponse(VexObservationStatementProjection projection) { var scope = projection.Scope; var document = projection.Document; var signature = projection.Signature; return new VexObservationStatementResponse( projection.ObservationId, projection.ProviderId, projection.Status.ToString().ToLowerInvariant(), projection.Justification?.ToString().ToLowerInvariant(), projection.Detail, projection.FirstSeen, projection.LastSeen, new VexObservationScopeResponse( scope.Key, scope.Name, scope.Version, scope.Purl, scope.Cpe, scope.ComponentIdentifiers), projection.Anchors, new VexObservationDocumentResponse( document.Digest, document.Format.ToString().ToLowerInvariant(), document.Revision, document.SourceUri.ToString()), signature is null ? null : new VexObservationSignatureResponse( signature.Type, signature.KeyId, signature.Issuer, signature.VerifiedAt)); } private sealed record CachedGraphOverlay( IReadOnlyList Items, DateTimeOffset CachedAt); }