Files
git.stella-ops.org/src/Concelier/StellaOps.Excititor.WebService/Program.Helpers.cs

253 lines
8.1 KiB
C#

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Aoc;
using StellaOps.Excititor.Core.Storage;
using StellaOps.Excititor.WebService.Contracts;
using StellaOps.Excititor.WebService.Services;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
public partial class Program
{
private const string TenantHeaderName = "X-Stella-Tenant";
internal static bool TryResolveTenant(HttpContext context, VexStorageOptions 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 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();
}
internal 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 CachedGraphStatus(
IReadOnlyList<GraphStatusItem> Items,
DateTimeOffset CachedAt);
internal static string[] NormalizeValues(StringValues values) =>
values.Where(static v => !string.IsNullOrWhiteSpace(v))
.Select(static v => v!.Trim())
.ToArray();
}