consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
252
src/Concelier/StellaOps.Excititor.WebService/Program.Helpers.cs
Normal file
252
src/Concelier/StellaOps.Excititor.WebService/Program.Helpers.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user