Update
This commit is contained in:
313
src/StellaOps.Concelier.Core/Unknown/UnknownStateLedger.cs
Normal file
313
src/StellaOps.Concelier.Core/Unknown/UnknownStateLedger.cs
Normal file
@@ -0,0 +1,313 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Core.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation that derives unknown-state markers from canonical advisories.
|
||||
/// </summary>
|
||||
public sealed class UnknownStateLedger : IUnknownStateLedger
|
||||
{
|
||||
private static readonly ImmutableHashSet<string> ImpactStatuses = ImmutableHashSet.Create(
|
||||
StringComparer.Ordinal,
|
||||
AffectedPackageStatusCatalog.KnownAffected,
|
||||
AffectedPackageStatusCatalog.Affected,
|
||||
AffectedPackageStatusCatalog.UnderInvestigation,
|
||||
AffectedPackageStatusCatalog.Pending,
|
||||
AffectedPackageStatusCatalog.Unknown);
|
||||
|
||||
private static readonly ImmutableHashSet<string> FixStatuses = ImmutableHashSet.Create(
|
||||
StringComparer.Ordinal,
|
||||
AffectedPackageStatusCatalog.Fixed,
|
||||
AffectedPackageStatusCatalog.FirstFixed,
|
||||
AffectedPackageStatusCatalog.Mitigated);
|
||||
|
||||
private static readonly ImmutableDictionary<string, UnknownMarkerSeed> MarkerSeeds = new Dictionary<string, UnknownMarkerSeed>(StringComparer.Ordinal)
|
||||
{
|
||||
[UnknownStateMarkerKinds.UnknownVulnerabilityRange] = new UnknownMarkerSeed(0.8, "high"),
|
||||
[UnknownStateMarkerKinds.UnknownOrigin] = new UnknownMarkerSeed(0.6, "medium"),
|
||||
[UnknownStateMarkerKinds.AmbiguousFix] = new UnknownMarkerSeed(0.45, "medium"),
|
||||
}.ToImmutableDictionary(StringComparer.Ordinal);
|
||||
|
||||
private readonly IUnknownStateRepository _repository;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public UnknownStateLedger(IUnknownStateRepository repository, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async ValueTask<UnknownStateLedgerResult> RecordAsync(
|
||||
UnknownStateLedgerRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var recordedAt = _timeProvider.GetUtcNow();
|
||||
var markers = EvaluateMarkers(request.Advisory, request.AsOf, recordedAt);
|
||||
|
||||
await _repository.UpsertAsync(request.VulnerabilityKey, markers, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new UnknownStateLedgerResult(request.VulnerabilityKey, request.AsOf, markers);
|
||||
}
|
||||
|
||||
public ValueTask<IReadOnlyList<UnknownStateSnapshot>> GetByVulnerabilityAsync(
|
||||
string vulnerabilityKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(vulnerabilityKey);
|
||||
var normalizedKey = vulnerabilityKey.Trim().ToLowerInvariant();
|
||||
return _repository.GetByVulnerabilityAsync(normalizedKey, cancellationToken);
|
||||
}
|
||||
|
||||
private static ImmutableArray<UnknownStateSnapshot> EvaluateMarkers(
|
||||
Advisory advisory,
|
||||
DateTimeOffset observedAt,
|
||||
DateTimeOffset recordedAt)
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<UnknownStateSnapshot>(initialCapacity: 3);
|
||||
|
||||
if (advisory is not null)
|
||||
{
|
||||
if (TryCreateUnknownVulnerabilityRangeMarker(advisory, observedAt, recordedAt, out var unknownRange))
|
||||
{
|
||||
builder.Add(unknownRange);
|
||||
}
|
||||
|
||||
if (TryCreateUnknownOriginMarker(advisory, observedAt, recordedAt, out var unknownOrigin))
|
||||
{
|
||||
builder.Add(unknownOrigin);
|
||||
}
|
||||
|
||||
if (TryCreateAmbiguousFixMarker(advisory, observedAt, recordedAt, out var ambiguousFix))
|
||||
{
|
||||
builder.Add(ambiguousFix);
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.Count == 0)
|
||||
{
|
||||
return ImmutableArray<UnknownStateSnapshot>.Empty;
|
||||
}
|
||||
|
||||
builder.Sort(static (left, right) => StringComparer.Ordinal.Compare(left.Marker, right.Marker));
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static bool TryCreateUnknownVulnerabilityRangeMarker(
|
||||
Advisory advisory,
|
||||
DateTimeOffset observedAt,
|
||||
DateTimeOffset recordedAt,
|
||||
out UnknownStateSnapshot snapshot)
|
||||
{
|
||||
snapshot = null!;
|
||||
|
||||
if (advisory.AffectedPackages.IsDefaultOrEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lackingPackages = 0;
|
||||
|
||||
foreach (var package in advisory.AffectedPackages)
|
||||
{
|
||||
if (package is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!HasImpactStatus(package))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!HasConcreteRange(package))
|
||||
{
|
||||
lackingPackages++;
|
||||
}
|
||||
}
|
||||
|
||||
if (lackingPackages == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var seed = MarkerSeeds[UnknownStateMarkerKinds.UnknownVulnerabilityRange];
|
||||
var evidence = lackingPackages == 1
|
||||
? "1 affected package lacks explicit version ranges."
|
||||
: $"{lackingPackages} affected packages lack explicit version ranges.";
|
||||
|
||||
snapshot = new UnknownStateSnapshot(
|
||||
UnknownStateMarkerKinds.UnknownVulnerabilityRange,
|
||||
seed.Confidence,
|
||||
seed.Band,
|
||||
observedAt,
|
||||
recordedAt,
|
||||
evidence);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCreateUnknownOriginMarker(
|
||||
Advisory advisory,
|
||||
DateTimeOffset observedAt,
|
||||
DateTimeOffset recordedAt,
|
||||
out UnknownStateSnapshot snapshot)
|
||||
{
|
||||
snapshot = null!;
|
||||
|
||||
if (ContainsKnownProvenance(advisory.Provenance))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var seed = MarkerSeeds[UnknownStateMarkerKinds.UnknownOrigin];
|
||||
var evidence = advisory.Provenance.IsDefaultOrEmpty
|
||||
? "Advisory provenance is missing; falling back to inferred sources."
|
||||
: "All advisory provenance sources resolve to 'unknown'.";
|
||||
|
||||
snapshot = new UnknownStateSnapshot(
|
||||
UnknownStateMarkerKinds.UnknownOrigin,
|
||||
seed.Confidence,
|
||||
seed.Band,
|
||||
observedAt,
|
||||
recordedAt,
|
||||
evidence);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCreateAmbiguousFixMarker(
|
||||
Advisory advisory,
|
||||
DateTimeOffset observedAt,
|
||||
DateTimeOffset recordedAt,
|
||||
out UnknownStateSnapshot snapshot)
|
||||
{
|
||||
snapshot = null!;
|
||||
|
||||
if (advisory.AffectedPackages.IsDefaultOrEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ambiguousPackages = 0;
|
||||
|
||||
foreach (var package in advisory.AffectedPackages)
|
||||
{
|
||||
if (package is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!package.Statuses.IsDefaultOrEmpty && package.Statuses.Any(status => FixStatuses.Contains(status.Status)))
|
||||
{
|
||||
var hasFixedRange = package.VersionRanges.Any(static range => !string.IsNullOrWhiteSpace(range.FixedVersion));
|
||||
if (!hasFixedRange)
|
||||
{
|
||||
ambiguousPackages++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ambiguousPackages == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var seed = MarkerSeeds[UnknownStateMarkerKinds.AmbiguousFix];
|
||||
var evidence = ambiguousPackages == 1
|
||||
? "Fix status published without explicit fixed version details."
|
||||
: $"Fix status published without explicit fixed versions for {ambiguousPackages} packages.";
|
||||
|
||||
snapshot = new UnknownStateSnapshot(
|
||||
UnknownStateMarkerKinds.AmbiguousFix,
|
||||
seed.Confidence,
|
||||
seed.Band,
|
||||
observedAt,
|
||||
recordedAt,
|
||||
evidence);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HasImpactStatus(AffectedPackage package)
|
||||
{
|
||||
if (package.Statuses.IsDefaultOrEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var status in package.Statuses)
|
||||
{
|
||||
if (status is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ImpactStatuses.Contains(status.Status))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasConcreteRange(AffectedPackage package)
|
||||
{
|
||||
if (package.VersionRanges.IsDefaultOrEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var range in package.VersionRanges)
|
||||
{
|
||||
if (range is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(range.IntroducedVersion) ||
|
||||
!string.IsNullOrWhiteSpace(range.FixedVersion) ||
|
||||
!string.IsNullOrWhiteSpace(range.LastAffectedVersion) ||
|
||||
!string.IsNullOrWhiteSpace(range.RangeExpression))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ContainsKnownProvenance(ImmutableArray<AdvisoryProvenance> provenance)
|
||||
{
|
||||
if (provenance.IsDefaultOrEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var entry in provenance)
|
||||
{
|
||||
if (entry is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsKnownSource(entry.Source))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsKnownSource(string? source)
|
||||
=> !string.IsNullOrWhiteSpace(source) &&
|
||||
!string.Equals(source, "unknown", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private readonly record struct UnknownMarkerSeed(double Confidence, string Band);
|
||||
}
|
||||
Reference in New Issue
Block a user