271 lines
7.5 KiB
C#
271 lines
7.5 KiB
C#
using System.Collections.Immutable;
|
|
|
|
namespace StellaOps.Scanner.Delta;
|
|
|
|
/// <summary>
|
|
/// Scans only changed layers between two image versions for efficient delta scanning.
|
|
/// </summary>
|
|
public interface IDeltaLayerScanner
|
|
{
|
|
/// <summary>
|
|
/// Performs a delta scan comparing two image versions.
|
|
/// </summary>
|
|
/// <param name="oldImage">Reference to the old/baseline image.</param>
|
|
/// <param name="newImage">Reference to the new image to scan.</param>
|
|
/// <param name="options">Delta scan options.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Delta scan result with changed layers and composite SBOM.</returns>
|
|
Task<DeltaScanResult> ScanDeltaAsync(
|
|
string oldImage,
|
|
string newImage,
|
|
DeltaScanOptions? options = null,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Identifies layer changes between two images without scanning.
|
|
/// </summary>
|
|
/// <param name="oldImage">Reference to the old image.</param>
|
|
/// <param name="newImage">Reference to the new image.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Layer change summary.</returns>
|
|
Task<LayerChangeSummary> IdentifyLayerChangesAsync(
|
|
string oldImage,
|
|
string newImage,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Options for delta scanning.
|
|
/// </summary>
|
|
public sealed record DeltaScanOptions
|
|
{
|
|
/// <summary>
|
|
/// Whether to force a full scan even for unchanged layers.
|
|
/// Default is false.
|
|
/// </summary>
|
|
public bool ForceFullScan { get; init; } = false;
|
|
|
|
/// <summary>
|
|
/// Whether to use cached per-layer SBOMs for unchanged layers.
|
|
/// Default is true.
|
|
/// </summary>
|
|
public bool UseCachedSboms { get; init; } = true;
|
|
|
|
/// <summary>
|
|
/// Maximum age of cached SBOMs to consider valid (in days).
|
|
/// Default is 30 days.
|
|
/// </summary>
|
|
public int MaxCacheAgeDays { get; init; } = 30;
|
|
|
|
/// <summary>
|
|
/// SBOM format to produce (CycloneDX or SPDX).
|
|
/// Default is CycloneDX.
|
|
/// </summary>
|
|
public string SbomFormat { get; init; } = "cyclonedx";
|
|
|
|
/// <summary>
|
|
/// Whether to include layer attribution in the SBOM.
|
|
/// Default is true.
|
|
/// </summary>
|
|
public bool IncludeLayerAttribution { get; init; } = true;
|
|
|
|
/// <summary>
|
|
/// Target platform for multi-arch images.
|
|
/// If null, uses default platform.
|
|
/// </summary>
|
|
public string? Platform { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of a delta scan operation.
|
|
/// </summary>
|
|
public sealed record DeltaScanResult
|
|
{
|
|
/// <summary>
|
|
/// Old image reference.
|
|
/// </summary>
|
|
public required string OldImage { get; init; }
|
|
|
|
/// <summary>
|
|
/// Old image manifest digest.
|
|
/// </summary>
|
|
public required string OldManifestDigest { get; init; }
|
|
|
|
/// <summary>
|
|
/// New image reference.
|
|
/// </summary>
|
|
public required string NewImage { get; init; }
|
|
|
|
/// <summary>
|
|
/// New image manifest digest.
|
|
/// </summary>
|
|
public required string NewManifestDigest { get; init; }
|
|
|
|
/// <summary>
|
|
/// Layers that were added in the new image.
|
|
/// </summary>
|
|
public ImmutableArray<LayerChangeInfo> AddedLayers { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Layers that were removed from the old image.
|
|
/// </summary>
|
|
public ImmutableArray<LayerChangeInfo> RemovedLayers { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Layers that are unchanged between images.
|
|
/// </summary>
|
|
public ImmutableArray<LayerChangeInfo> UnchangedLayers { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Composite SBOM for the new image (combining cached + newly scanned).
|
|
/// </summary>
|
|
public string? CompositeSbom { get; init; }
|
|
|
|
/// <summary>
|
|
/// SBOM format (cyclonedx or spdx).
|
|
/// </summary>
|
|
public string? SbomFormat { get; init; }
|
|
|
|
/// <summary>
|
|
/// Total scan duration.
|
|
/// </summary>
|
|
public TimeSpan ScanDuration { get; init; }
|
|
|
|
/// <summary>
|
|
/// Duration spent scanning only added layers.
|
|
/// </summary>
|
|
public TimeSpan AddedLayersScanDuration { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of components found in added layers.
|
|
/// </summary>
|
|
public int AddedComponentCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of components from cached layers.
|
|
/// </summary>
|
|
public int CachedComponentCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether the scan used cached SBOMs.
|
|
/// </summary>
|
|
public bool UsedCache { get; init; }
|
|
|
|
/// <summary>
|
|
/// Percentage of layers that were reused from cache.
|
|
/// </summary>
|
|
public double LayerReuseRatio =>
|
|
(AddedLayers.Length + UnchangedLayers.Length) > 0
|
|
? (double)UnchangedLayers.Length / (AddedLayers.Length + UnchangedLayers.Length)
|
|
: 0;
|
|
|
|
/// <summary>
|
|
/// When the scan was performed.
|
|
/// </summary>
|
|
public DateTimeOffset ScannedAt { get; init; } = DateTimeOffset.UtcNow;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Information about a layer change.
|
|
/// </summary>
|
|
public sealed record LayerChangeInfo
|
|
{
|
|
/// <summary>
|
|
/// Compressed layer digest.
|
|
/// </summary>
|
|
public required string LayerDigest { get; init; }
|
|
|
|
/// <summary>
|
|
/// Uncompressed content hash (diffID).
|
|
/// </summary>
|
|
public required string DiffId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Layer index in the image.
|
|
/// </summary>
|
|
public int LayerIndex { get; init; }
|
|
|
|
/// <summary>
|
|
/// Size of the layer in bytes.
|
|
/// </summary>
|
|
public long Size { get; init; }
|
|
|
|
/// <summary>
|
|
/// Media type of the layer.
|
|
/// </summary>
|
|
public string? MediaType { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether this layer's SBOM was retrieved from cache.
|
|
/// </summary>
|
|
public bool FromCache { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of components found in this layer.
|
|
/// </summary>
|
|
public int ComponentCount { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Summary of layer changes between two images.
|
|
/// </summary>
|
|
public sealed record LayerChangeSummary
|
|
{
|
|
/// <summary>
|
|
/// Old image reference.
|
|
/// </summary>
|
|
public required string OldImage { get; init; }
|
|
|
|
/// <summary>
|
|
/// New image reference.
|
|
/// </summary>
|
|
public required string NewImage { get; init; }
|
|
|
|
/// <summary>
|
|
/// Total layers in old image.
|
|
/// </summary>
|
|
public int OldLayerCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Total layers in new image.
|
|
/// </summary>
|
|
public int NewLayerCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of layers added.
|
|
/// </summary>
|
|
public int AddedCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of layers removed.
|
|
/// </summary>
|
|
public int RemovedCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of layers unchanged.
|
|
/// </summary>
|
|
public int UnchangedCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Estimated scan savings (layers we don't need to scan).
|
|
/// </summary>
|
|
public double EstimatedSavingsRatio => NewLayerCount > 0
|
|
? (double)UnchangedCount / NewLayerCount
|
|
: 0;
|
|
|
|
/// <summary>
|
|
/// DiffIDs of added layers.
|
|
/// </summary>
|
|
public ImmutableArray<string> AddedDiffIds { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// DiffIDs of removed layers.
|
|
/// </summary>
|
|
public ImmutableArray<string> RemovedDiffIds { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// DiffIDs of unchanged layers.
|
|
/// </summary>
|
|
public ImmutableArray<string> UnchangedDiffIds { get; init; } = [];
|
|
}
|