save checkpoint: save features
This commit is contained in:
332
src/Replay/StellaOps.Replay.WebService/FeedSnapshotSupport.cs
Normal file
332
src/Replay/StellaOps.Replay.WebService/FeedSnapshotSupport.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
// <copyright file="FeedSnapshotSupport.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Replay.Core.FeedSnapshots;
|
||||
|
||||
namespace StellaOps.Replay.WebService;
|
||||
|
||||
internal sealed class InMemoryFeedSnapshotBlobStore : IFeedSnapshotBlobStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, FeedSnapshotBlob> _blobs = new(StringComparer.Ordinal);
|
||||
|
||||
public Task StoreAsync(FeedSnapshotBlob blob, CancellationToken ct = default)
|
||||
{
|
||||
_blobs[blob.Digest] = blob;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<FeedSnapshotBlob?> GetByDigestAsync(string digest, CancellationToken ct = default)
|
||||
{
|
||||
_blobs.TryGetValue(digest, out var blob);
|
||||
return Task.FromResult(blob);
|
||||
}
|
||||
|
||||
public Task<bool> ExistsAsync(string digest, CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(_blobs.ContainsKey(digest));
|
||||
}
|
||||
|
||||
public Task DeleteAsync(string digest, CancellationToken ct = default)
|
||||
{
|
||||
_blobs.TryRemove(digest, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class InMemoryFeedSnapshotIndexStore : IFeedSnapshotIndexStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, List<FeedSnapshotIndexEntry>> _index = new(StringComparer.Ordinal);
|
||||
|
||||
public Task IndexSnapshotAsync(FeedSnapshotIndexEntry entry, CancellationToken ct = default)
|
||||
{
|
||||
var entries = _index.GetOrAdd(entry.ProviderId, static _ => []);
|
||||
lock (entries)
|
||||
{
|
||||
entries.Add(entry);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<FeedSnapshotIndexEntry?> FindSnapshotAtTimeAsync(
|
||||
string providerId,
|
||||
DateTimeOffset pointInTime,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!_index.TryGetValue(providerId, out var entries))
|
||||
{
|
||||
return Task.FromResult<FeedSnapshotIndexEntry?>(null);
|
||||
}
|
||||
|
||||
lock (entries)
|
||||
{
|
||||
var found = entries
|
||||
.Where(e => e.CapturedAt <= pointInTime)
|
||||
.OrderByDescending(e => e.CapturedAt)
|
||||
.FirstOrDefault();
|
||||
return Task.FromResult(found);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<ImmutableArray<FeedSnapshotIndexEntry>> ListSnapshotsAsync(
|
||||
string providerId,
|
||||
DateTimeOffset from,
|
||||
DateTimeOffset to,
|
||||
int limit,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!_index.TryGetValue(providerId, out var entries))
|
||||
{
|
||||
return Task.FromResult(ImmutableArray<FeedSnapshotIndexEntry>.Empty);
|
||||
}
|
||||
|
||||
lock (entries)
|
||||
{
|
||||
IEnumerable<FeedSnapshotIndexEntry> query = entries
|
||||
.Where(e => e.CapturedAt >= from && e.CapturedAt <= to)
|
||||
.OrderBy(e => e.CapturedAt);
|
||||
|
||||
if (limit > 0)
|
||||
{
|
||||
query = query.Take(limit);
|
||||
}
|
||||
|
||||
return Task.FromResult(query.ToImmutableArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class JsonAdvisoryExtractor : IAdvisoryExtractor
|
||||
{
|
||||
public Task<AdvisoryData?> ExtractAdvisoryAsync(
|
||||
string cveId,
|
||||
byte[] content,
|
||||
FeedSnapshotFormat format,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cveId) || content.Length == 0)
|
||||
{
|
||||
return Task.FromResult<AdvisoryData?>(null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
var advisory = TryExtract(cveId, doc.RootElement);
|
||||
return Task.FromResult(advisory);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return Task.FromResult<AdvisoryData?>(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static AdvisoryData? TryExtract(string cveId, JsonElement root)
|
||||
{
|
||||
if (root.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (TryParseAdvisoryObject(cveId, root, out var direct))
|
||||
{
|
||||
return direct;
|
||||
}
|
||||
|
||||
if (TryGetPropertyIgnoreCase(root, "advisories", out var advisories) &&
|
||||
advisories.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in advisories.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.Object &&
|
||||
TryParseAdvisoryObject(cveId, item, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TryGetPropertyIgnoreCase(root, "cves", out var cves))
|
||||
{
|
||||
if (cves.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var prop in cves.EnumerateObject())
|
||||
{
|
||||
if (string.Equals(prop.Name, cveId, StringComparison.OrdinalIgnoreCase) &&
|
||||
prop.Value.ValueKind == JsonValueKind.Object &&
|
||||
TryParseAdvisoryObject(cveId, prop.Value, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cves.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in cves.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.Object &&
|
||||
TryParseAdvisoryObject(cveId, item, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (root.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in root.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.Object &&
|
||||
TryParseAdvisoryObject(cveId, item, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryParseAdvisoryObject(string requestedCveId, JsonElement obj, out AdvisoryData? advisory)
|
||||
{
|
||||
advisory = null;
|
||||
var cveId = GetString(obj, "cveId", "cve", "cve_id", "id");
|
||||
if (string.IsNullOrWhiteSpace(cveId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(cveId, requestedCveId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
advisory = new AdvisoryData
|
||||
{
|
||||
CveId = cveId,
|
||||
Severity = GetString(obj, "severity"),
|
||||
CvssScore = GetDecimal(obj, "cvssScore", "cvss", "score"),
|
||||
CvssVector = GetString(obj, "cvssVector", "vector"),
|
||||
Description = GetString(obj, "description", "summary"),
|
||||
FixStatus = GetString(obj, "fixStatus", "fix_state", "status"),
|
||||
AffectedProducts = GetStringArray(obj, "affectedProducts", "affected_products", "packages"),
|
||||
References = GetStringArray(obj, "references", "refs", "links"),
|
||||
PublishedAt = GetDateTimeOffset(obj, "publishedAt", "published_at"),
|
||||
LastModifiedAt = GetDateTimeOffset(obj, "lastModifiedAt", "last_modified_at")
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? GetString(JsonElement obj, params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (TryGetPropertyIgnoreCase(obj, name, out var value))
|
||||
{
|
||||
if (value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return value.GetString();
|
||||
}
|
||||
|
||||
if (value.ValueKind != JsonValueKind.Null && value.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static decimal? GetDecimal(JsonElement obj, params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (!TryGetPropertyIgnoreCase(obj, name, out var value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.ValueKind == JsonValueKind.Number && value.TryGetDecimal(out var num))
|
||||
{
|
||||
return num;
|
||||
}
|
||||
|
||||
if (value.ValueKind == JsonValueKind.String &&
|
||||
decimal.TryParse(value.GetString(), out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? GetDateTimeOffset(JsonElement obj, params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (!TryGetPropertyIgnoreCase(obj, name, out var value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.ValueKind == JsonValueKind.String &&
|
||||
DateTimeOffset.TryParse(value.GetString(), out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> GetStringArray(JsonElement obj, params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (!TryGetPropertyIgnoreCase(obj, name, out var value) ||
|
||||
value.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var values = new List<string>();
|
||||
foreach (var item in value.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var str = item.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(str))
|
||||
{
|
||||
values.Add(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values.ToImmutableArray();
|
||||
}
|
||||
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
private static bool TryGetPropertyIgnoreCase(JsonElement obj, string name, out JsonElement value)
|
||||
{
|
||||
foreach (var prop in obj.EnumerateObject())
|
||||
{
|
||||
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = prop.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,12 @@ using Serilog.Events;
|
||||
using StellaOps.Audit.ReplayToken;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.AuditPack.Services;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.DependencyInjection;
|
||||
using StellaOps.Replay.Core.FeedSnapshots;
|
||||
using StellaOps.Replay.WebService;
|
||||
using StellaOps.Telemetry.Core;
|
||||
|
||||
const string ReplayReadPolicy = "replay.token.read";
|
||||
@@ -48,6 +51,17 @@ builder.Services.AddOptions<ReplayServiceOptions>()
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
builder.Services.AddSingleton<ICryptoHash, DefaultCryptoHash>();
|
||||
builder.Services.AddSingleton<IReplayTokenGenerator, Sha256ReplayTokenGenerator>();
|
||||
builder.Services.AddSingleton<IReplayExecutor, ReplayExecutor>();
|
||||
builder.Services.AddSingleton<IAuditBundleReader, AuditBundleReader>();
|
||||
builder.Services.AddSingleton<IVerdictReplayPredicate, VerdictReplayPredicate>();
|
||||
|
||||
builder.Services.AddSingleton<IFeedSnapshotBlobStore, InMemoryFeedSnapshotBlobStore>();
|
||||
builder.Services.AddSingleton<IFeedSnapshotIndexStore, InMemoryFeedSnapshotIndexStore>();
|
||||
builder.Services.AddSingleton(new FeedSnapshotServiceOptions());
|
||||
builder.Services.AddSingleton<FeedSnapshotService>();
|
||||
builder.Services.AddSingleton<IAdvisoryExtractor, JsonAdvisoryExtractor>();
|
||||
builder.Services.AddSingleton<PointInTimeAdvisoryResolver>();
|
||||
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddHealthChecks();
|
||||
@@ -125,6 +139,8 @@ app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapHealthChecks("/healthz");
|
||||
app.MapVerdictReplayEndpoints();
|
||||
app.MapPointInTimeQueryEndpoints();
|
||||
|
||||
// POST /v1/replay/tokens - Generate a new replay token
|
||||
app.MapPost("/v1/replay/tokens", Task<Results<Created<GenerateTokenResponse>, ProblemHttpResult>> (
|
||||
@@ -484,3 +500,5 @@ public class AuthorityConfig
|
||||
public List<string> Audiences { get; set; } = new() { "stella-ops-api" };
|
||||
public List<string> RequiredScopes { get; set; } = new() { "vuln.operate" };
|
||||
}
|
||||
|
||||
public partial class Program;
|
||||
|
||||
Reference in New Issue
Block a user