finish off sprint advisories and sprints
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user