314 lines
9.5 KiB
C#
314 lines
9.5 KiB
C#
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);
|
|
}
|