157 lines
5.0 KiB
C#
157 lines
5.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using StellaOps.Scanner.Core.Contracts;
|
|
using StellaOps.Scanner.Core.Utility;
|
|
using StellaOps.Scanner.Emit.Pedigree;
|
|
|
|
namespace StellaOps.Scanner.Emit.Composition;
|
|
|
|
public sealed record ImageArtifactDescriptor
|
|
{
|
|
public string ImageDigest { get; init; } = string.Empty;
|
|
|
|
public string? ImageReference { get; init; }
|
|
= null;
|
|
|
|
public string? Repository { get; init; }
|
|
= null;
|
|
|
|
public string? Tag { get; init; }
|
|
= null;
|
|
|
|
public string? Architecture { get; init; }
|
|
= null;
|
|
}
|
|
|
|
public sealed record SbomCompositionRequest
|
|
{
|
|
public required ImageArtifactDescriptor Image { get; init; }
|
|
|
|
public required ImmutableArray<LayerComponentFragment> LayerFragments { get; init; }
|
|
|
|
public DateTimeOffset GeneratedAt { get; init; }
|
|
= ScannerTimestamps.UtcNow();
|
|
|
|
public string? GeneratorName { get; init; }
|
|
= null;
|
|
|
|
public string? GeneratorVersion { get; init; }
|
|
= null;
|
|
|
|
public IReadOnlyDictionary<string, string>? AdditionalProperties { get; init; }
|
|
= null;
|
|
|
|
public ImmutableArray<SbomPolicyFinding> PolicyFindings { get; init; }
|
|
= ImmutableArray<SbomPolicyFinding>.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets the pre-fetched pedigree data keyed by component PURL.
|
|
/// This enables synchronous composition while allowing async pedigree lookups
|
|
/// to happen before calling <see cref="CycloneDxComposer.Compose"/>.
|
|
/// Sprint: SPRINT_20260107_005_002 Task PD-009
|
|
/// </summary>
|
|
public IReadOnlyDictionary<string, PedigreeData>? PedigreeDataByPurl { get; init; }
|
|
= null;
|
|
|
|
/// <summary>
|
|
/// Gets whether pedigree data should be included in the SBOM.
|
|
/// Defaults to true if pedigree data is provided.
|
|
/// </summary>
|
|
public bool IncludePedigree { get; init; } = true;
|
|
|
|
public static SbomCompositionRequest Create(
|
|
ImageArtifactDescriptor image,
|
|
IEnumerable<LayerComponentFragment> fragments,
|
|
DateTimeOffset generatedAt,
|
|
string? generatorName = null,
|
|
string? generatorVersion = null,
|
|
IReadOnlyDictionary<string, string>? properties = null,
|
|
IEnumerable<SbomPolicyFinding>? policyFindings = null,
|
|
IReadOnlyDictionary<string, PedigreeData>? pedigreeData = null,
|
|
bool includePedigree = true)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(image);
|
|
ArgumentNullException.ThrowIfNull(fragments);
|
|
|
|
var normalizedImage = new ImageArtifactDescriptor
|
|
{
|
|
ImageDigest = ScannerIdentifiers.NormalizeDigest(image.ImageDigest) ?? throw new ArgumentException("Image digest is required.", nameof(image)),
|
|
ImageReference = Normalize(image.ImageReference),
|
|
Repository = Normalize(image.Repository),
|
|
Tag = Normalize(image.Tag),
|
|
Architecture = Normalize(image.Architecture),
|
|
};
|
|
|
|
return new SbomCompositionRequest
|
|
{
|
|
Image = normalizedImage,
|
|
LayerFragments = fragments.ToImmutableArray(),
|
|
GeneratedAt = ScannerTimestamps.Normalize(generatedAt),
|
|
GeneratorName = Normalize(generatorName),
|
|
GeneratorVersion = Normalize(generatorVersion),
|
|
AdditionalProperties = properties,
|
|
PolicyFindings = NormalizePolicyFindings(policyFindings),
|
|
PedigreeDataByPurl = pedigreeData,
|
|
IncludePedigree = includePedigree,
|
|
};
|
|
}
|
|
|
|
private static string? Normalize(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return value.Trim();
|
|
}
|
|
|
|
private static ImmutableArray<SbomPolicyFinding> NormalizePolicyFindings(IEnumerable<SbomPolicyFinding>? policyFindings)
|
|
{
|
|
if (policyFindings is null)
|
|
{
|
|
return ImmutableArray<SbomPolicyFinding>.Empty;
|
|
}
|
|
|
|
var builder = ImmutableArray.CreateBuilder<SbomPolicyFinding>();
|
|
foreach (var finding in policyFindings)
|
|
{
|
|
if (finding is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
SbomPolicyFinding normalized;
|
|
try
|
|
{
|
|
normalized = finding.Normalize();
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(normalized.FindingId) || string.IsNullOrWhiteSpace(normalized.ComponentKey))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
builder.Add(normalized);
|
|
}
|
|
|
|
if (builder.Count == 0)
|
|
{
|
|
return ImmutableArray<SbomPolicyFinding>.Empty;
|
|
}
|
|
|
|
return builder
|
|
.ToImmutable()
|
|
.OrderBy(static finding => finding.FindingId, StringComparer.Ordinal)
|
|
.ThenBy(static finding => finding.ComponentKey, StringComparer.Ordinal)
|
|
.ThenBy(static finding => finding.VulnerabilityId ?? string.Empty, StringComparer.Ordinal)
|
|
.ToImmutableArray();
|
|
}
|
|
}
|