Files
git.stella-ops.org/src/Replay/StellaOps.Replay.WebService/FeedSnapshotSupport.cs
2026-02-12 10:27:23 +02:00

333 lines
10 KiB
C#

// <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;
}
}