finish off sprint advisories and sprints

This commit is contained in:
master
2026-01-24 00:12:43 +02:00
parent 726d70dc7f
commit c70e83719e
266 changed files with 46699 additions and 1328 deletions

View File

@@ -106,6 +106,20 @@ public sealed record DeltaSigPredicate
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
/// <summary>
/// SHA-256 digest of the associated SBOM document.
/// </summary>
[JsonPropertyName("sbomDigest")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? SbomDigest { get; init; }
/// <summary>
/// References to large binary blobs stored out-of-band (by digest).
/// </summary>
[JsonPropertyName("largeBlobs")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<LargeBlobReference>? LargeBlobs { get; init; }
/// <summary>
/// Gets the old binary subject.
/// </summary>
@@ -442,3 +456,36 @@ public sealed record VersionRange
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Constraint { get; init; }
}
/// <summary>
/// Reference to a large binary blob stored out-of-band (by content-addressable digest).
/// Used in two-tier bundle format for separating metadata from heavy binaries.
/// </summary>
public sealed record LargeBlobReference
{
/// <summary>
/// Blob kind: "preBinary", "postBinary", "debugSymbols", "irDiff", etc.
/// </summary>
[JsonPropertyName("kind")]
public required string Kind { get; init; }
/// <summary>
/// Content-addressable digest (e.g., "sha256:abc123...").
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
/// <summary>
/// Media type of the blob (e.g., "application/octet-stream").
/// </summary>
[JsonPropertyName("mediaType")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? MediaType { get; init; }
/// <summary>
/// Size in bytes (for transfer planning).
/// </summary>
[JsonPropertyName("sizeBytes")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public long? SizeBytes { get; init; }
}

View File

@@ -99,6 +99,20 @@ public sealed record DeltaSigPredicateV2
[JsonPropertyName("metadata")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
/// <summary>
/// SHA-256 digest of the associated SBOM document.
/// </summary>
[JsonPropertyName("sbomDigest")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? SbomDigest { get; init; }
/// <summary>
/// References to large binary blobs stored out-of-band (by digest).
/// </summary>
[JsonPropertyName("largeBlobs")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<LargeBlobReference>? LargeBlobs { get; init; }
}
/// <summary>

View File

@@ -98,7 +98,14 @@ public sealed class DeltaSigService : IDeltaSigService
// 6. Compute summary
var summary = ComputeSummary(comparison, deltas);
// 7. Build predicate
// 7. Build large blob references if requested
List<LargeBlobReference>? largeBlobs = null;
if (request.IncludeLargeBlobs)
{
largeBlobs = BuildLargeBlobReferences(request.OldBinary, request.NewBinary);
}
// 8. Build predicate
var predicate = new DeltaSigPredicate
{
Subject = new[]
@@ -146,7 +153,9 @@ public sealed class DeltaSigService : IDeltaSigService
},
_ => null
},
Metadata = request.Metadata
Metadata = request.Metadata,
SbomDigest = request.SbomDigest,
LargeBlobs = largeBlobs
};
_logger.LogInformation(
@@ -571,4 +580,37 @@ public sealed class DeltaSigService : IDeltaSigService
var version = assembly.GetName().Version;
return version?.ToString() ?? "1.0.0";
}
private static List<LargeBlobReference> BuildLargeBlobReferences(
BinaryReference oldBinary,
BinaryReference newBinary)
{
var blobs = new List<LargeBlobReference>();
// Add pre-binary reference
if (oldBinary.Digest.TryGetValue("sha256", out var oldSha256))
{
blobs.Add(new LargeBlobReference
{
Kind = "preBinary",
Digest = $"sha256:{oldSha256}",
MediaType = "application/octet-stream",
SizeBytes = oldBinary.Size
});
}
// Add post-binary reference
if (newBinary.Digest.TryGetValue("sha256", out var newSha256))
{
blobs.Add(new LargeBlobReference
{
Kind = "postBinary",
Digest = $"sha256:{newSha256}",
MediaType = "application/octet-stream",
SizeBytes = newBinary.Size
});
}
return blobs;
}
}

View File

@@ -153,6 +153,19 @@ public sealed record DeltaSigRequest
/// Additional metadata to include in predicate.
/// </summary>
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
/// <summary>
/// SHA-256 digest of the associated SBOM document.
/// If provided, this will be included in the predicate for cross-referencing.
/// </summary>
public string? SbomDigest { get; init; }
/// <summary>
/// Whether to include large blob references in the predicate.
/// When true, the predicate will include digests and sizes of the pre/post binaries
/// for the two-tier bundle format.
/// </summary>
public bool IncludeLargeBlobs { get; init; } = true;
}
/// <summary>

View File

@@ -68,6 +68,29 @@ public sealed record SbomStabilityRequest
/// Package version for identification.
/// </summary>
public string? PackageVersion { get; init; }
/// <summary>
/// Whether to normalize SBOM content before hashing (strip volatile fields).
/// Default: true.
/// </summary>
public bool NormalizeBeforeHash { get; init; } = true;
/// <summary>
/// SBOM format for normalization (CycloneDX or SPDX).
/// When null, auto-detected from content.
/// </summary>
public SbomFormatHint? FormatHint { get; init; }
}
/// <summary>
/// Hint for SBOM format detection in stability validation.
/// </summary>
public enum SbomFormatHint
{
/// <summary>CycloneDX format.</summary>
CycloneDx,
/// <summary>SPDX format.</summary>
Spdx
}
/// <summary>
@@ -157,6 +180,21 @@ public sealed record SbomRunResult
public string? SbomContent { get; init; }
}
/// <summary>
/// Optional content normalizer for stripping volatile fields before hashing.
/// Decouples SbomStabilityValidator from the AirGap.Importer normalizer.
/// </summary>
public interface ISbomContentNormalizer
{
/// <summary>
/// Normalizes SBOM content by stripping volatile fields and producing canonical JSON.
/// </summary>
/// <param name="sbomContent">Raw SBOM JSON.</param>
/// <param name="format">SBOM format hint.</param>
/// <returns>Normalized canonical JSON string.</returns>
string Normalize(string sbomContent, SbomFormatHint format);
}
/// <summary>
/// Implementation of SBOM stability validation.
/// </summary>
@@ -164,6 +202,7 @@ public sealed class SbomStabilityValidator : ISbomStabilityValidator
{
private readonly ILogger<SbomStabilityValidator> _logger;
private readonly ISbomGenerator? _sbomGenerator;
private readonly ISbomContentNormalizer? _normalizer;
// Canonical JSON options for deterministic serialization
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
@@ -175,10 +214,12 @@ public sealed class SbomStabilityValidator : ISbomStabilityValidator
public SbomStabilityValidator(
ILogger<SbomStabilityValidator> logger,
ISbomGenerator? sbomGenerator = null)
ISbomGenerator? sbomGenerator = null,
ISbomContentNormalizer? normalizer = null)
{
_logger = logger;
_sbomGenerator = sbomGenerator;
_normalizer = normalizer;
}
/// <inheritdoc/>
@@ -297,7 +338,8 @@ public sealed class SbomStabilityValidator : ISbomStabilityValidator
{
// Generate SBOM
var sbomContent = await GenerateSbomAsync(request.ArtifactPath, ct);
var canonicalHash = ComputeCanonicalHash(sbomContent);
var contentForHash = MaybeNormalize(sbomContent, request);
var canonicalHash = ComputeCanonicalHash(contentForHash);
stopwatch.Stop();
@@ -339,7 +381,8 @@ public sealed class SbomStabilityValidator : ISbomStabilityValidator
try
{
var sbomContent = await GenerateSbomAsync(request.ArtifactPath, ct);
var canonicalHash = ComputeCanonicalHash(sbomContent);
var contentForHash = MaybeNormalize(sbomContent, request);
var canonicalHash = ComputeCanonicalHash(contentForHash);
stopwatch.Stop();
@@ -365,6 +408,29 @@ public sealed class SbomStabilityValidator : ISbomStabilityValidator
}
}
private string MaybeNormalize(string sbomContent, SbomStabilityRequest request)
{
if (!request.NormalizeBeforeHash || _normalizer is null)
{
return sbomContent;
}
var format = request.FormatHint ?? DetectFormat(sbomContent);
return _normalizer.Normalize(sbomContent, format);
}
private static SbomFormatHint DetectFormat(string sbomContent)
{
// Simple heuristic: CycloneDX has "bomFormat", SPDX has "spdxVersion"
if (sbomContent.Contains("\"bomFormat\"", StringComparison.Ordinal) ||
sbomContent.Contains("\"specVersion\"", StringComparison.Ordinal))
{
return SbomFormatHint.CycloneDx;
}
return SbomFormatHint.Spdx;
}
private async Task<string> GenerateSbomAsync(string artifactPath, CancellationToken ct)
{
if (_sbomGenerator is not null)