up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,44 +1,44 @@
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.ImpactIndex;
/// <summary>
/// Provides read access to the scheduler impact index.
/// </summary>
public interface IImpactIndex
{
/// <summary>
/// Resolves the impacted image set for the provided package URLs.
/// </summary>
/// <param name="purls">Package URLs to look up.</param>
/// <param name="usageOnly">When true, restricts results to components marked as runtime/entrypoint usage.</param>
/// <param name="selector">Selector scoping the query.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask<ImpactSet> ResolveByPurlsAsync(
IEnumerable<string> purls,
bool usageOnly,
Selector selector,
CancellationToken cancellationToken = default);
/// <summary>
/// Resolves impacted images by vulnerability identifiers if the index has the mapping available.
/// </summary>
/// <param name="vulnerabilityIds">Vulnerability identifiers to look up.</param>
/// <param name="usageOnly">When true, restricts results to components marked as runtime/entrypoint usage.</param>
/// <param name="selector">Selector scoping the query.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask<ImpactSet> ResolveByVulnerabilitiesAsync(
IEnumerable<string> vulnerabilityIds,
bool usageOnly,
Selector selector,
CancellationToken cancellationToken = default);
/// <summary>
/// Resolves all tracked images for the provided selector.
/// </summary>
/// <param name="selector">Selector scoping the query.</param>
/// <param name="usageOnly">When true, restricts results to images with entrypoint usage.</param>
/// <param name="cancellationToken">Cancellation token.</param>
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.ImpactIndex;
/// <summary>
/// Provides read access to the scheduler impact index.
/// </summary>
public interface IImpactIndex
{
/// <summary>
/// Resolves the impacted image set for the provided package URLs.
/// </summary>
/// <param name="purls">Package URLs to look up.</param>
/// <param name="usageOnly">When true, restricts results to components marked as runtime/entrypoint usage.</param>
/// <param name="selector">Selector scoping the query.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask<ImpactSet> ResolveByPurlsAsync(
IEnumerable<string> purls,
bool usageOnly,
Selector selector,
CancellationToken cancellationToken = default);
/// <summary>
/// Resolves impacted images by vulnerability identifiers if the index has the mapping available.
/// </summary>
/// <param name="vulnerabilityIds">Vulnerability identifiers to look up.</param>
/// <param name="usageOnly">When true, restricts results to components marked as runtime/entrypoint usage.</param>
/// <param name="selector">Selector scoping the query.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask<ImpactSet> ResolveByVulnerabilitiesAsync(
IEnumerable<string> vulnerabilityIds,
bool usageOnly,
Selector selector,
CancellationToken cancellationToken = default);
/// <summary>
/// Resolves all tracked images for the provided selector.
/// </summary>
/// <param name="selector">Selector scoping the query.</param>
/// <param name="usageOnly">When true, restricts results to images with entrypoint usage.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask<ImpactSet> ResolveAllAsync(
Selector selector,
bool usageOnly,

View File

@@ -1,17 +1,17 @@
using System;
using System.Collections.Immutable;
namespace StellaOps.Scheduler.ImpactIndex;
using System;
using System.Collections.Immutable;
namespace StellaOps.Scheduler.ImpactIndex;
public sealed record ImpactImageRecord(
int ImageId,
string TenantId,
string Digest,
string Registry,
string Repository,
ImmutableArray<string> Namespaces,
ImmutableArray<string> Tags,
ImmutableSortedDictionary<string, string> Labels,
DateTimeOffset GeneratedAt,
ImmutableArray<string> Components,
ImmutableArray<string> EntrypointComponents);
int ImageId,
string TenantId,
string Digest,
string Registry,
string Repository,
ImmutableArray<string> Namespaces,
ImmutableArray<string> Tags,
ImmutableSortedDictionary<string, string> Labels,
DateTimeOffset GeneratedAt,
ImmutableArray<string> Components,
ImmutableArray<string> EntrypointComponents);

View File

@@ -1,26 +1,26 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace StellaOps.Scheduler.ImpactIndex;
/// <summary>
/// ServiceCollection helpers for wiring the fixture-backed impact index.
/// </summary>
public static class ImpactIndexServiceCollectionExtensions
{
public static IServiceCollection AddImpactIndexStub(
this IServiceCollection services,
Action<ImpactIndexStubOptions>? configure = null)
{
ArgumentNullException.ThrowIfNull(services);
var options = new ImpactIndexStubOptions();
configure?.Invoke(options);
services.TryAddSingleton(TimeProvider.System);
services.AddSingleton(options);
services.TryAddSingleton<IImpactIndex, FixtureImpactIndex>();
return services;
}
}
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace StellaOps.Scheduler.ImpactIndex;
/// <summary>
/// ServiceCollection helpers for wiring the fixture-backed impact index.
/// </summary>
public static class ImpactIndexServiceCollectionExtensions
{
public static IServiceCollection AddImpactIndexStub(
this IServiceCollection services,
Action<ImpactIndexStubOptions>? configure = null)
{
ArgumentNullException.ThrowIfNull(services);
var options = new ImpactIndexStubOptions();
configure?.Invoke(options);
services.TryAddSingleton(TimeProvider.System);
services.AddSingleton(options);
services.TryAddSingleton<IImpactIndex, FixtureImpactIndex>();
return services;
}
}

View File

@@ -1,19 +1,19 @@
namespace StellaOps.Scheduler.ImpactIndex;
/// <summary>
/// Options controlling the fixture-backed impact index stub.
/// </summary>
public sealed class ImpactIndexStubOptions
{
/// <summary>
/// Optional absolute or relative directory containing BOM-Index JSON fixtures.
/// When not supplied or not found, embedded fixtures ship with the assembly are used instead.
/// </summary>
public string? FixtureDirectory { get; set; }
/// <summary>
/// Snapshot identifier reported in the generated <see cref="StellaOps.Scheduler.Models.ImpactSet"/>.
/// Defaults to <c>samples/impact-index-stub</c>.
/// </summary>
public string SnapshotId { get; set; } = "samples/impact-index-stub";
}
namespace StellaOps.Scheduler.ImpactIndex;
/// <summary>
/// Options controlling the fixture-backed impact index stub.
/// </summary>
public sealed class ImpactIndexStubOptions
{
/// <summary>
/// Optional absolute or relative directory containing BOM-Index JSON fixtures.
/// When not supplied or not found, embedded fixtures ship with the assembly are used instead.
/// </summary>
public string? FixtureDirectory { get; set; }
/// <summary>
/// Snapshot identifier reported in the generated <see cref="StellaOps.Scheduler.Models.ImpactSet"/>.
/// Defaults to <c>samples/impact-index-stub</c>.
/// </summary>
public string SnapshotId { get; set; } = "samples/impact-index-stub";
}

View File

@@ -1,119 +1,119 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Globalization;
using System.Text;
using Collections.Special;
namespace StellaOps.Scheduler.ImpactIndex.Ingestion;
internal sealed record BomIndexComponent(string Key, bool UsedByEntrypoint);
internal sealed record BomIndexDocument(string ImageDigest, DateTimeOffset GeneratedAt, ImmutableArray<BomIndexComponent> Components);
internal static class BomIndexReader
{
private const int HeaderMagicLength = 7;
private static readonly byte[] Magic = Encoding.ASCII.GetBytes("BOMIDX1");
public static BomIndexDocument Read(Stream stream)
{
ArgumentNullException.ThrowIfNull(stream);
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
Span<byte> magicBuffer = stackalloc byte[HeaderMagicLength];
if (reader.Read(magicBuffer) != HeaderMagicLength || !magicBuffer.SequenceEqual(Magic))
{
throw new InvalidOperationException("Invalid BOM index header magic.");
}
var version = reader.ReadUInt16();
if (version != 1)
{
throw new NotSupportedException($"Unsupported BOM index version '{version}'.");
}
var flags = reader.ReadUInt16();
var hasEntrypoints = (flags & 0x1) == 1;
var digestLength = reader.ReadUInt16();
var digestBytes = reader.ReadBytes(digestLength);
var imageDigest = Encoding.UTF8.GetString(digestBytes);
var generatedAtMicros = reader.ReadInt64();
var generatedAt = DateTimeOffset.FromUnixTimeMilliseconds(generatedAtMicros / 1000)
.AddTicks((generatedAtMicros % 1000) * TimeSpan.TicksPerMillisecond / 1000);
var layerCount = checked((int)reader.ReadUInt32());
var componentCount = checked((int)reader.ReadUInt32());
var entrypointCount = checked((int)reader.ReadUInt32());
// Layer table (we only need to skip entries but validate length)
for (var i = 0; i < layerCount; i++)
{
_ = ReadUtf8String(reader);
}
var componentKeys = new string[componentCount];
for (var i = 0; i < componentCount; i++)
{
componentKeys[i] = ReadUtf8String(reader);
}
for (var i = 0; i < componentCount; i++)
{
var length = reader.ReadUInt32();
if (length > 0)
{
var payload = reader.ReadBytes(checked((int)length));
using var bitmapStream = new MemoryStream(payload, writable: false);
_ = RoaringBitmap.Deserialize(bitmapStream);
}
}
var entrypointPresence = new bool[componentCount];
if (hasEntrypoints && entrypointCount > 0)
{
// Entrypoint table (skip strings)
for (var i = 0; i < entrypointCount; i++)
{
_ = ReadUtf8String(reader);
}
for (var i = 0; i < componentCount; i++)
{
var length = reader.ReadUInt32();
if (length == 0)
{
entrypointPresence[i] = false;
continue;
}
var payload = reader.ReadBytes(checked((int)length));
using var bitmapStream = new MemoryStream(payload, writable: false);
var bitmap = RoaringBitmap.Deserialize(bitmapStream);
entrypointPresence[i] = bitmap.Any();
}
}
var builder = ImmutableArray.CreateBuilder<BomIndexComponent>(componentCount);
for (var i = 0; i < componentCount; i++)
{
var key = componentKeys[i];
builder.Add(new BomIndexComponent(key, entrypointPresence[i]));
}
return new BomIndexDocument(imageDigest, generatedAt, builder.MoveToImmutable());
}
private static string ReadUtf8String(BinaryReader reader)
{
var length = reader.ReadUInt16();
if (length == 0)
{
return string.Empty;
}
var bytes = reader.ReadBytes(length);
return Encoding.UTF8.GetString(bytes);
}
}
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Globalization;
using System.Text;
using Collections.Special;
namespace StellaOps.Scheduler.ImpactIndex.Ingestion;
internal sealed record BomIndexComponent(string Key, bool UsedByEntrypoint);
internal sealed record BomIndexDocument(string ImageDigest, DateTimeOffset GeneratedAt, ImmutableArray<BomIndexComponent> Components);
internal static class BomIndexReader
{
private const int HeaderMagicLength = 7;
private static readonly byte[] Magic = Encoding.ASCII.GetBytes("BOMIDX1");
public static BomIndexDocument Read(Stream stream)
{
ArgumentNullException.ThrowIfNull(stream);
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
Span<byte> magicBuffer = stackalloc byte[HeaderMagicLength];
if (reader.Read(magicBuffer) != HeaderMagicLength || !magicBuffer.SequenceEqual(Magic))
{
throw new InvalidOperationException("Invalid BOM index header magic.");
}
var version = reader.ReadUInt16();
if (version != 1)
{
throw new NotSupportedException($"Unsupported BOM index version '{version}'.");
}
var flags = reader.ReadUInt16();
var hasEntrypoints = (flags & 0x1) == 1;
var digestLength = reader.ReadUInt16();
var digestBytes = reader.ReadBytes(digestLength);
var imageDigest = Encoding.UTF8.GetString(digestBytes);
var generatedAtMicros = reader.ReadInt64();
var generatedAt = DateTimeOffset.FromUnixTimeMilliseconds(generatedAtMicros / 1000)
.AddTicks((generatedAtMicros % 1000) * TimeSpan.TicksPerMillisecond / 1000);
var layerCount = checked((int)reader.ReadUInt32());
var componentCount = checked((int)reader.ReadUInt32());
var entrypointCount = checked((int)reader.ReadUInt32());
// Layer table (we only need to skip entries but validate length)
for (var i = 0; i < layerCount; i++)
{
_ = ReadUtf8String(reader);
}
var componentKeys = new string[componentCount];
for (var i = 0; i < componentCount; i++)
{
componentKeys[i] = ReadUtf8String(reader);
}
for (var i = 0; i < componentCount; i++)
{
var length = reader.ReadUInt32();
if (length > 0)
{
var payload = reader.ReadBytes(checked((int)length));
using var bitmapStream = new MemoryStream(payload, writable: false);
_ = RoaringBitmap.Deserialize(bitmapStream);
}
}
var entrypointPresence = new bool[componentCount];
if (hasEntrypoints && entrypointCount > 0)
{
// Entrypoint table (skip strings)
for (var i = 0; i < entrypointCount; i++)
{
_ = ReadUtf8String(reader);
}
for (var i = 0; i < componentCount; i++)
{
var length = reader.ReadUInt32();
if (length == 0)
{
entrypointPresence[i] = false;
continue;
}
var payload = reader.ReadBytes(checked((int)length));
using var bitmapStream = new MemoryStream(payload, writable: false);
var bitmap = RoaringBitmap.Deserialize(bitmapStream);
entrypointPresence[i] = bitmap.Any();
}
}
var builder = ImmutableArray.CreateBuilder<BomIndexComponent>(componentCount);
for (var i = 0; i < componentCount; i++)
{
var key = componentKeys[i];
builder.Add(new BomIndexComponent(key, entrypointPresence[i]));
}
return new BomIndexDocument(imageDigest, generatedAt, builder.MoveToImmutable());
}
private static string ReadUtf8String(BinaryReader reader)
{
var length = reader.ReadUInt16();
if (length == 0)
{
return string.Empty;
}
var bytes = reader.ReadBytes(length);
return Encoding.UTF8.GetString(bytes);
}
}

View File

@@ -1,28 +1,28 @@
using System;
using System.Collections.Immutable;
using System.IO;
namespace StellaOps.Scheduler.ImpactIndex.Ingestion;
/// <summary>
/// Describes a BOM-Index ingestion payload for the scheduler impact index.
/// </summary>
public sealed record ImpactIndexIngestionRequest
{
public required string TenantId { get; init; }
public required string ImageDigest { get; init; }
public required string Registry { get; init; }
public required string Repository { get; init; }
public ImmutableArray<string> Namespaces { get; init; } = ImmutableArray<string>.Empty;
public ImmutableArray<string> Tags { get; init; } = ImmutableArray<string>.Empty;
public ImmutableSortedDictionary<string, string> Labels { get; init; } = ImmutableSortedDictionary<string, string>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase);
public required Stream BomIndexStream { get; init; }
= Stream.Null;
}
using System;
using System.Collections.Immutable;
using System.IO;
namespace StellaOps.Scheduler.ImpactIndex.Ingestion;
/// <summary>
/// Describes a BOM-Index ingestion payload for the scheduler impact index.
/// </summary>
public sealed record ImpactIndexIngestionRequest
{
public required string TenantId { get; init; }
public required string ImageDigest { get; init; }
public required string Registry { get; init; }
public required string Repository { get; init; }
public ImmutableArray<string> Namespaces { get; init; } = ImmutableArray<string>.Empty;
public ImmutableArray<string> Tags { get; init; } = ImmutableArray<string>.Empty;
public ImmutableSortedDictionary<string, string> Labels { get; init; } = ImmutableSortedDictionary<string, string>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase);
public required Stream BomIndexStream { get; init; }
= Stream.Null;
}

View File

@@ -1,34 +1,34 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Collections.Special;
using Microsoft.Extensions.Logging;
using StellaOps.Scheduler.ImpactIndex.Ingestion;
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.ImpactIndex;
/// <summary>
/// Roaring bitmap-backed implementation of the scheduler impact index.
/// </summary>
public sealed class RoaringImpactIndex : IImpactIndex
{
private readonly object _gate = new();
private readonly Dictionary<string, int> _imageIds = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<int, ImpactImageRecord> _images = new();
private readonly Dictionary<string, RoaringBitmap> _containsByPurl = new(StringComparer.OrdinalIgnoreCase);
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Collections.Special;
using Microsoft.Extensions.Logging;
using StellaOps.Scheduler.ImpactIndex.Ingestion;
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.ImpactIndex;
/// <summary>
/// Roaring bitmap-backed implementation of the scheduler impact index.
/// </summary>
public sealed class RoaringImpactIndex : IImpactIndex
{
private readonly object _gate = new();
private readonly Dictionary<string, int> _imageIds = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<int, ImpactImageRecord> _images = new();
private readonly Dictionary<string, RoaringBitmap> _containsByPurl = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, RoaringBitmap> _usedByEntrypointByPurl = new(StringComparer.OrdinalIgnoreCase);
private readonly ILogger<RoaringImpactIndex> _logger;
private readonly TimeProvider _timeProvider;
private string? _snapshotId;
public RoaringImpactIndex(ILogger<RoaringImpactIndex> logger, TimeProvider? timeProvider = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -58,103 +58,103 @@ public sealed class RoaringImpactIndex : IImpactIndex
return ValueTask.CompletedTask;
}
public async Task IngestAsync(ImpactIndexIngestionRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(request.BomIndexStream);
using var buffer = new MemoryStream();
await request.BomIndexStream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
buffer.Position = 0;
var document = BomIndexReader.Read(buffer);
if (!string.Equals(document.ImageDigest, request.ImageDigest, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"BOM-Index digest mismatch. Header '{document.ImageDigest}', request '{request.ImageDigest}'.");
}
var tenantId = request.TenantId ?? throw new ArgumentNullException(nameof(request.TenantId));
var registry = request.Registry ?? throw new ArgumentNullException(nameof(request.Registry));
var repository = request.Repository ?? throw new ArgumentNullException(nameof(request.Repository));
var namespaces = request.Namespaces.IsDefault ? ImmutableArray<string>.Empty : request.Namespaces;
var tags = request.Tags.IsDefault ? ImmutableArray<string>.Empty : request.Tags;
var labels = request.Labels.Count == 0
? ImmutableSortedDictionary<string, string>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase)
: request.Labels;
var componentKeys = document.Components
.Select(component => component.Key)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
var entrypointComponents = document.Components
.Where(component => component.UsedByEntrypoint)
.Select(component => component.Key)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
lock (_gate)
{
var imageId = EnsureImageId(request.ImageDigest);
if (_images.TryGetValue(imageId, out var existing))
{
RemoveImageComponents(existing);
}
var metadata = new ImpactImageRecord(
imageId,
tenantId,
request.ImageDigest,
registry,
repository,
namespaces,
tags,
labels,
document.GeneratedAt,
componentKeys,
entrypointComponents);
_images[imageId] = metadata;
_imageIds[request.ImageDigest] = imageId;
foreach (var key in componentKeys)
{
var bitmap = _containsByPurl.GetValueOrDefault(key);
_containsByPurl[key] = AddImageToBitmap(bitmap, imageId);
}
foreach (var key in entrypointComponents)
{
var bitmap = _usedByEntrypointByPurl.GetValueOrDefault(key);
_usedByEntrypointByPurl[key] = AddImageToBitmap(bitmap, imageId);
}
}
_logger.LogInformation(
"ImpactIndex ingested BOM-Index for {Digest} ({TenantId}/{Repository}). Components={ComponentCount} EntrypointComponents={EntrypointCount}",
request.ImageDigest,
tenantId,
repository,
componentKeys.Length,
entrypointComponents.Length);
}
public ValueTask<ImpactSet> ResolveByPurlsAsync(
IEnumerable<string> purls,
bool usageOnly,
Selector selector,
CancellationToken cancellationToken = default)
=> ValueTask.FromResult(ResolveByPurlsCore(purls, usageOnly, selector));
public ValueTask<ImpactSet> ResolveByVulnerabilitiesAsync(
IEnumerable<string> vulnerabilityIds,
bool usageOnly,
Selector selector,
CancellationToken cancellationToken = default)
=> ValueTask.FromResult(CreateEmptyImpactSet(selector, usageOnly));
public async Task IngestAsync(ImpactIndexIngestionRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(request.BomIndexStream);
using var buffer = new MemoryStream();
await request.BomIndexStream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
buffer.Position = 0;
var document = BomIndexReader.Read(buffer);
if (!string.Equals(document.ImageDigest, request.ImageDigest, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"BOM-Index digest mismatch. Header '{document.ImageDigest}', request '{request.ImageDigest}'.");
}
var tenantId = request.TenantId ?? throw new ArgumentNullException(nameof(request.TenantId));
var registry = request.Registry ?? throw new ArgumentNullException(nameof(request.Registry));
var repository = request.Repository ?? throw new ArgumentNullException(nameof(request.Repository));
var namespaces = request.Namespaces.IsDefault ? ImmutableArray<string>.Empty : request.Namespaces;
var tags = request.Tags.IsDefault ? ImmutableArray<string>.Empty : request.Tags;
var labels = request.Labels.Count == 0
? ImmutableSortedDictionary<string, string>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase)
: request.Labels;
var componentKeys = document.Components
.Select(component => component.Key)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
var entrypointComponents = document.Components
.Where(component => component.UsedByEntrypoint)
.Select(component => component.Key)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
lock (_gate)
{
var imageId = EnsureImageId(request.ImageDigest);
if (_images.TryGetValue(imageId, out var existing))
{
RemoveImageComponents(existing);
}
var metadata = new ImpactImageRecord(
imageId,
tenantId,
request.ImageDigest,
registry,
repository,
namespaces,
tags,
labels,
document.GeneratedAt,
componentKeys,
entrypointComponents);
_images[imageId] = metadata;
_imageIds[request.ImageDigest] = imageId;
foreach (var key in componentKeys)
{
var bitmap = _containsByPurl.GetValueOrDefault(key);
_containsByPurl[key] = AddImageToBitmap(bitmap, imageId);
}
foreach (var key in entrypointComponents)
{
var bitmap = _usedByEntrypointByPurl.GetValueOrDefault(key);
_usedByEntrypointByPurl[key] = AddImageToBitmap(bitmap, imageId);
}
}
_logger.LogInformation(
"ImpactIndex ingested BOM-Index for {Digest} ({TenantId}/{Repository}). Components={ComponentCount} EntrypointComponents={EntrypointCount}",
request.ImageDigest,
tenantId,
repository,
componentKeys.Length,
entrypointComponents.Length);
}
public ValueTask<ImpactSet> ResolveByPurlsAsync(
IEnumerable<string> purls,
bool usageOnly,
Selector selector,
CancellationToken cancellationToken = default)
=> ValueTask.FromResult(ResolveByPurlsCore(purls, usageOnly, selector));
public ValueTask<ImpactSet> ResolveByVulnerabilitiesAsync(
IEnumerable<string> vulnerabilityIds,
bool usageOnly,
Selector selector,
CancellationToken cancellationToken = default)
=> ValueTask.FromResult(CreateEmptyImpactSet(selector, usageOnly));
public ValueTask<ImpactSet> ResolveAllAsync(
Selector selector,
bool usageOnly,
@@ -257,102 +257,102 @@ public sealed class RoaringImpactIndex : IImpactIndex
return ValueTask.CompletedTask;
}
private ImpactSet ResolveByPurlsCore(IEnumerable<string> purls, bool usageOnly, Selector selector)
{
ArgumentNullException.ThrowIfNull(purls);
ArgumentNullException.ThrowIfNull(selector);
var normalized = purls
.Where(static purl => !string.IsNullOrWhiteSpace(purl))
.Select(static purl => purl.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
if (normalized.Length == 0)
{
return CreateEmptyImpactSet(selector, usageOnly);
}
RoaringBitmap imageIds;
lock (_gate)
{
imageIds = RoaringBitmap.Create(Array.Empty<int>());
foreach (var purl in normalized)
{
if (_containsByPurl.TryGetValue(purl, out var bitmap))
{
imageIds = imageIds | bitmap;
}
}
}
return BuildImpactSet(imageIds, selector, usageOnly);
}
private ImpactSet ResolveAllCore(Selector selector, bool usageOnly)
{
ArgumentNullException.ThrowIfNull(selector);
RoaringBitmap bitmap;
lock (_gate)
{
var ids = _images.Keys.OrderBy(id => id).ToArray();
bitmap = RoaringBitmap.Create(ids);
}
return BuildImpactSet(bitmap, selector, usageOnly);
}
private ImpactSet BuildImpactSet(RoaringBitmap imageIds, Selector selector, bool usageOnly)
{
var images = new List<ImpactImage>();
var latestGeneratedAt = DateTimeOffset.MinValue;
lock (_gate)
{
foreach (var imageId in imageIds)
{
if (!_images.TryGetValue(imageId, out var metadata))
{
continue;
}
if (!ImageMatchesSelector(metadata, selector))
{
continue;
}
if (usageOnly && metadata.EntrypointComponents.Length == 0)
{
continue;
}
if (metadata.GeneratedAt > latestGeneratedAt)
{
latestGeneratedAt = metadata.GeneratedAt;
}
images.Add(new ImpactImage(
metadata.Digest,
metadata.Registry,
metadata.Repository,
metadata.Namespaces,
metadata.Tags,
metadata.EntrypointComponents.Length > 0,
metadata.Labels));
}
}
if (images.Count == 0)
{
return CreateEmptyImpactSet(selector, usageOnly);
}
images.Sort(static (left, right) => string.Compare(left.ImageDigest, right.ImageDigest, StringComparison.Ordinal));
var generatedAt = latestGeneratedAt == DateTimeOffset.MinValue ? _timeProvider.GetUtcNow() : latestGeneratedAt;
private ImpactSet ResolveByPurlsCore(IEnumerable<string> purls, bool usageOnly, Selector selector)
{
ArgumentNullException.ThrowIfNull(purls);
ArgumentNullException.ThrowIfNull(selector);
var normalized = purls
.Where(static purl => !string.IsNullOrWhiteSpace(purl))
.Select(static purl => purl.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
if (normalized.Length == 0)
{
return CreateEmptyImpactSet(selector, usageOnly);
}
RoaringBitmap imageIds;
lock (_gate)
{
imageIds = RoaringBitmap.Create(Array.Empty<int>());
foreach (var purl in normalized)
{
if (_containsByPurl.TryGetValue(purl, out var bitmap))
{
imageIds = imageIds | bitmap;
}
}
}
return BuildImpactSet(imageIds, selector, usageOnly);
}
private ImpactSet ResolveAllCore(Selector selector, bool usageOnly)
{
ArgumentNullException.ThrowIfNull(selector);
RoaringBitmap bitmap;
lock (_gate)
{
var ids = _images.Keys.OrderBy(id => id).ToArray();
bitmap = RoaringBitmap.Create(ids);
}
return BuildImpactSet(bitmap, selector, usageOnly);
}
private ImpactSet BuildImpactSet(RoaringBitmap imageIds, Selector selector, bool usageOnly)
{
var images = new List<ImpactImage>();
var latestGeneratedAt = DateTimeOffset.MinValue;
lock (_gate)
{
foreach (var imageId in imageIds)
{
if (!_images.TryGetValue(imageId, out var metadata))
{
continue;
}
if (!ImageMatchesSelector(metadata, selector))
{
continue;
}
if (usageOnly && metadata.EntrypointComponents.Length == 0)
{
continue;
}
if (metadata.GeneratedAt > latestGeneratedAt)
{
latestGeneratedAt = metadata.GeneratedAt;
}
images.Add(new ImpactImage(
metadata.Digest,
metadata.Registry,
metadata.Repository,
metadata.Namespaces,
metadata.Tags,
metadata.EntrypointComponents.Length > 0,
metadata.Labels));
}
}
if (images.Count == 0)
{
return CreateEmptyImpactSet(selector, usageOnly);
}
images.Sort(static (left, right) => string.Compare(left.ImageDigest, right.ImageDigest, StringComparison.Ordinal));
var generatedAt = latestGeneratedAt == DateTimeOffset.MinValue ? _timeProvider.GetUtcNow() : latestGeneratedAt;
return new ImpactSet(
selector,
images.ToImmutableArray(),
@@ -362,9 +362,9 @@ public sealed class RoaringImpactIndex : IImpactIndex
snapshotId: _snapshotId,
schemaVersion: SchedulerSchemaVersions.ImpactSet);
}
private ImpactSet CreateEmptyImpactSet(Selector selector, bool usageOnly)
{
private ImpactSet CreateEmptyImpactSet(Selector selector, bool usageOnly)
{
return new ImpactSet(
selector,
ImmutableArray<ImpactImage>.Empty,
@@ -374,167 +374,167 @@ public sealed class RoaringImpactIndex : IImpactIndex
snapshotId: _snapshotId,
schemaVersion: SchedulerSchemaVersions.ImpactSet);
}
private static bool ImageMatchesSelector(ImpactImageRecord image, Selector selector)
{
if (selector.TenantId is not null && !string.Equals(selector.TenantId, image.TenantId, StringComparison.Ordinal))
{
return false;
}
if (!MatchesScope(image, selector))
{
return false;
}
if (selector.Digests.Length > 0 && !selector.Digests.Contains(image.Digest, StringComparer.OrdinalIgnoreCase))
{
return false;
}
if (selector.Repositories.Length > 0)
{
var repoMatch = selector.Repositories.Any(repo =>
string.Equals(repo, image.Repository, StringComparison.OrdinalIgnoreCase) ||
string.Equals(repo, $"{image.Registry}/{image.Repository}", StringComparison.OrdinalIgnoreCase));
if (!repoMatch)
{
return false;
}
}
if (selector.Namespaces.Length > 0)
{
if (image.Namespaces.IsDefaultOrEmpty)
{
return false;
}
var namespaceMatch = selector.Namespaces.Any(ns => image.Namespaces.Contains(ns, StringComparer.OrdinalIgnoreCase));
if (!namespaceMatch)
{
return false;
}
}
if (selector.IncludeTags.Length > 0)
{
if (image.Tags.IsDefaultOrEmpty)
{
return false;
}
var tagMatch = selector.IncludeTags.Any(pattern => image.Tags.Any(tag => MatchesTagPattern(tag, pattern)));
if (!tagMatch)
{
return false;
}
}
if (selector.Labels.Length > 0)
{
if (image.Labels.Count == 0)
{
return false;
}
foreach (var label in selector.Labels)
{
if (!image.Labels.TryGetValue(label.Key, out var value))
{
return false;
}
if (label.Values.Length > 0 && !label.Values.Contains(value, StringComparer.OrdinalIgnoreCase))
{
return false;
}
}
}
return true;
}
private void RemoveImageComponents(ImpactImageRecord record)
{
foreach (var key in record.Components)
{
if (_containsByPurl.TryGetValue(key, out var bitmap))
{
var updated = RemoveImageFromBitmap(bitmap, record.ImageId);
if (updated is null)
{
_containsByPurl.Remove(key);
}
else
{
_containsByPurl[key] = updated;
}
}
}
foreach (var key in record.EntrypointComponents)
{
if (_usedByEntrypointByPurl.TryGetValue(key, out var bitmap))
{
var updated = RemoveImageFromBitmap(bitmap, record.ImageId);
if (updated is null)
{
_usedByEntrypointByPurl.Remove(key);
}
else
{
_usedByEntrypointByPurl[key] = updated;
}
}
}
}
private static RoaringBitmap AddImageToBitmap(RoaringBitmap? bitmap, int imageId)
{
if (bitmap is null)
{
return RoaringBitmap.Create(new[] { imageId });
}
if (bitmap.Any(id => id == imageId))
{
return bitmap;
}
var merged = bitmap
.Concat(new[] { imageId })
.Distinct()
.OrderBy(id => id)
.ToArray();
return RoaringBitmap.Create(merged);
}
private static RoaringBitmap? RemoveImageFromBitmap(RoaringBitmap bitmap, int imageId)
{
var remaining = bitmap
.Where(id => id != imageId)
.OrderBy(id => id)
.ToArray();
if (remaining.Length == 0)
{
return null;
}
return RoaringBitmap.Create(remaining);
}
private static bool ImageMatchesSelector(ImpactImageRecord image, Selector selector)
{
if (selector.TenantId is not null && !string.Equals(selector.TenantId, image.TenantId, StringComparison.Ordinal))
{
return false;
}
if (!MatchesScope(image, selector))
{
return false;
}
if (selector.Digests.Length > 0 && !selector.Digests.Contains(image.Digest, StringComparer.OrdinalIgnoreCase))
{
return false;
}
if (selector.Repositories.Length > 0)
{
var repoMatch = selector.Repositories.Any(repo =>
string.Equals(repo, image.Repository, StringComparison.OrdinalIgnoreCase) ||
string.Equals(repo, $"{image.Registry}/{image.Repository}", StringComparison.OrdinalIgnoreCase));
if (!repoMatch)
{
return false;
}
}
if (selector.Namespaces.Length > 0)
{
if (image.Namespaces.IsDefaultOrEmpty)
{
return false;
}
var namespaceMatch = selector.Namespaces.Any(ns => image.Namespaces.Contains(ns, StringComparer.OrdinalIgnoreCase));
if (!namespaceMatch)
{
return false;
}
}
if (selector.IncludeTags.Length > 0)
{
if (image.Tags.IsDefaultOrEmpty)
{
return false;
}
var tagMatch = selector.IncludeTags.Any(pattern => image.Tags.Any(tag => MatchesTagPattern(tag, pattern)));
if (!tagMatch)
{
return false;
}
}
if (selector.Labels.Length > 0)
{
if (image.Labels.Count == 0)
{
return false;
}
foreach (var label in selector.Labels)
{
if (!image.Labels.TryGetValue(label.Key, out var value))
{
return false;
}
if (label.Values.Length > 0 && !label.Values.Contains(value, StringComparer.OrdinalIgnoreCase))
{
return false;
}
}
}
return true;
}
private void RemoveImageComponents(ImpactImageRecord record)
{
foreach (var key in record.Components)
{
if (_containsByPurl.TryGetValue(key, out var bitmap))
{
var updated = RemoveImageFromBitmap(bitmap, record.ImageId);
if (updated is null)
{
_containsByPurl.Remove(key);
}
else
{
_containsByPurl[key] = updated;
}
}
}
foreach (var key in record.EntrypointComponents)
{
if (_usedByEntrypointByPurl.TryGetValue(key, out var bitmap))
{
var updated = RemoveImageFromBitmap(bitmap, record.ImageId);
if (updated is null)
{
_usedByEntrypointByPurl.Remove(key);
}
else
{
_usedByEntrypointByPurl[key] = updated;
}
}
}
}
private static RoaringBitmap AddImageToBitmap(RoaringBitmap? bitmap, int imageId)
{
if (bitmap is null)
{
return RoaringBitmap.Create(new[] { imageId });
}
if (bitmap.Any(id => id == imageId))
{
return bitmap;
}
var merged = bitmap
.Concat(new[] { imageId })
.Distinct()
.OrderBy(id => id)
.ToArray();
return RoaringBitmap.Create(merged);
}
private static RoaringBitmap? RemoveImageFromBitmap(RoaringBitmap bitmap, int imageId)
{
var remaining = bitmap
.Where(id => id != imageId)
.OrderBy(id => id)
.ToArray();
if (remaining.Length == 0)
{
return null;
}
return RoaringBitmap.Create(remaining);
}
private static bool MatchesScope(ImpactImageRecord image, Selector selector)
{
return selector.Scope switch
{
SelectorScope.AllImages => true,
SelectorScope.ByDigest => selector.Digests.Contains(image.Digest, StringComparer.OrdinalIgnoreCase),
SelectorScope.ByRepository => selector.Repositories.Any(repo =>
string.Equals(repo, image.Repository, StringComparison.OrdinalIgnoreCase) ||
string.Equals(repo, $"{image.Registry}/{image.Repository}", StringComparison.OrdinalIgnoreCase)),
SelectorScope.ByNamespace => !image.Namespaces.IsDefaultOrEmpty && selector.Namespaces.Any(ns => image.Namespaces.Contains(ns, StringComparer.OrdinalIgnoreCase)),
SelectorScope.ByDigest => selector.Digests.Contains(image.Digest, StringComparer.OrdinalIgnoreCase),
SelectorScope.ByRepository => selector.Repositories.Any(repo =>
string.Equals(repo, image.Repository, StringComparison.OrdinalIgnoreCase) ||
string.Equals(repo, $"{image.Registry}/{image.Repository}", StringComparison.OrdinalIgnoreCase)),
SelectorScope.ByNamespace => !image.Namespaces.IsDefaultOrEmpty && selector.Namespaces.Any(ns => image.Namespaces.Contains(ns, StringComparer.OrdinalIgnoreCase)),
SelectorScope.ByLabels => selector.Labels.All(label =>
image.Labels.TryGetValue(label.Key, out var value) &&
(label.Values.Length == 0 || label.Values.Contains(value, StringComparer.OrdinalIgnoreCase))),
@@ -573,63 +573,63 @@ public sealed class RoaringImpactIndex : IImpactIndex
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString()));
return "snap-" + Convert.ToHexString(hash).ToLowerInvariant();
}
private static bool MatchesTagPattern(string tag, string pattern)
{
if (string.IsNullOrWhiteSpace(pattern))
{
return false;
}
if (pattern == "*")
{
return true;
}
if (!pattern.Contains('*') && !pattern.Contains('?'))
{
return string.Equals(tag, pattern, StringComparison.OrdinalIgnoreCase);
}
var escaped = Regex.Escape(pattern)
.Replace("\\*", ".*")
.Replace("\\?", ".");
return Regex.IsMatch(tag, $"^{escaped}$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
private int EnsureImageId(string digest)
{
if (_imageIds.TryGetValue(digest, out var existing))
{
return existing;
}
var candidate = ComputeDeterministicId(digest);
while (_images.ContainsKey(candidate))
{
candidate = (candidate + 1) & int.MaxValue;
if (candidate == 0)
{
candidate = 1;
}
}
_imageIds[digest] = candidate;
return candidate;
}
private static int ComputeDeterministicId(string digest)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(digest));
for (var offset = 0; offset <= bytes.Length - sizeof(int); offset += sizeof(int))
{
var value = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(offset, sizeof(int))) & int.MaxValue;
if (value != 0)
{
return value;
}
}
return digest.GetHashCode(StringComparison.OrdinalIgnoreCase) & int.MaxValue;
}
}
private static bool MatchesTagPattern(string tag, string pattern)
{
if (string.IsNullOrWhiteSpace(pattern))
{
return false;
}
if (pattern == "*")
{
return true;
}
if (!pattern.Contains('*') && !pattern.Contains('?'))
{
return string.Equals(tag, pattern, StringComparison.OrdinalIgnoreCase);
}
var escaped = Regex.Escape(pattern)
.Replace("\\*", ".*")
.Replace("\\?", ".");
return Regex.IsMatch(tag, $"^{escaped}$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
private int EnsureImageId(string digest)
{
if (_imageIds.TryGetValue(digest, out var existing))
{
return existing;
}
var candidate = ComputeDeterministicId(digest);
while (_images.ContainsKey(candidate))
{
candidate = (candidate + 1) & int.MaxValue;
if (candidate == 0)
{
candidate = 1;
}
}
_imageIds[digest] = candidate;
return candidate;
}
private static int ComputeDeterministicId(string digest)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(digest));
for (var offset = 0; offset <= bytes.Length - sizeof(int); offset += sizeof(int))
{
var value = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(offset, sizeof(int))) & int.MaxValue;
if (value != 0)
{
return value;
}
}
return digest.GetHashCode(StringComparison.OrdinalIgnoreCase) & int.MaxValue;
}
}