Files
git.stella-ops.org/src/Excititor/StellaOps.Excititor.WebService/Program.Helpers.cs
StellaOps Bot 150b3730ef
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
up
2025-11-24 07:52:25 +02:00

269 lines
8.5 KiB
C#

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<string, string> ReadMetadata(BsonValue value)
{
if (value is not BsonDocument doc || doc.ElementCount == 0)
{
return new Dictionary<string, string>(StringComparer.Ordinal);
}
var result = new Dictionary<string, string>(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<string, object?>
{
["violations"] = violations.ToArray(),
["primaryCode"] = exception.PrimaryErrorCode,
});
}
private static ImmutableHashSet<string> BuildStringFilterSet(StringValues values)
{
if (values.Count == 0)
{
return ImmutableHashSet<string>.Empty;
}
var builder = ImmutableHashSet.CreateBuilder<string>(StringComparer.OrdinalIgnoreCase);
foreach (var value in values)
{
if (!string.IsNullOrWhiteSpace(value))
{
builder.Add(value.Trim());
}
}
return builder.ToImmutable();
}
private static ImmutableHashSet<VexClaimStatus> BuildStatusFilter(StringValues values)
{
if (values.Count == 0)
{
return ImmutableHashSet<VexClaimStatus>.Empty;
}
var builder = ImmutableHashSet.CreateBuilder<VexClaimStatus>();
foreach (var value in values)
{
if (Enum.TryParse<VexClaimStatus>(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<string> NormalizePurls(string[]? purls)
{
if (purls is null || purls.Length == 0)
{
return Array.Empty<string>();
}
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var ordered = new List<string>(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<GraphOverlayItem> Items,
DateTimeOffset CachedAt);
}