Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Emit/Composition/SbomCompositionRequest.cs
2026-01-09 18:27:46 +02:00

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();
}
}