up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,41 +1,41 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CycloneDX;
using CycloneDX.Models;
using CycloneDX.Models.Vulnerabilities;
using JsonSerializer = CycloneDX.Json.Serializer;
using ProtoSerializer = CycloneDX.Protobuf.Serializer;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Core.Utility;
namespace StellaOps.Scanner.Emit.Composition;
public sealed class CycloneDxComposer
{
private static readonly Guid SerialNamespace = new("0d3a422b-6e1b-4d9b-9c35-654b706c97e8");
using JsonSerializer = CycloneDX.Json.Serializer;
using ProtoSerializer = CycloneDX.Protobuf.Serializer;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Core.Utility;
namespace StellaOps.Scanner.Emit.Composition;
public sealed class CycloneDxComposer
{
private static readonly Guid SerialNamespace = new("0d3a422b-6e1b-4d9b-9c35-654b706c97e8");
private const string InventoryMediaTypeJson = "application/vnd.cyclonedx+json; version=1.6";
private const string UsageMediaTypeJson = "application/vnd.cyclonedx+json; version=1.6; view=usage";
private const string InventoryMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.6";
private const string UsageMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.6; view=usage";
public SbomCompositionResult Compose(SbomCompositionRequest request)
{
ArgumentNullException.ThrowIfNull(request);
if (request.LayerFragments.IsDefaultOrEmpty)
{
throw new ArgumentException("At least one layer fragment is required.", nameof(request));
}
var graph = ComponentGraphBuilder.Build(request.LayerFragments);
var generatedAt = ScannerTimestamps.Normalize(request.GeneratedAt);
throw new ArgumentException("At least one layer fragment is required.", nameof(request));
}
var graph = ComponentGraphBuilder.Build(request.LayerFragments);
var generatedAt = ScannerTimestamps.Normalize(request.GeneratedAt);
var inventoryArtifact = BuildArtifact(
request,
graph,
@@ -44,11 +44,11 @@ public sealed class CycloneDxComposer
generatedAt,
InventoryMediaTypeJson,
InventoryMediaTypeProtobuf);
var usageComponents = graph.Components
.Where(static component => component.Usage.UsedByEntrypoint)
.ToImmutableArray();
var usageComponents = graph.Components
.Where(static component => component.Usage.UsedByEntrypoint)
.ToImmutableArray();
CycloneDxArtifact? usageArtifact = null;
if (!usageComponents.IsEmpty)
{
@@ -90,7 +90,7 @@ public sealed class CycloneDxComposer
CompositionRecipeSha256 = compositionRecipeSha,
};
}
private CycloneDxArtifact BuildArtifact(
SbomCompositionRequest request,
ComponentGraph graph,
@@ -117,7 +117,7 @@ public sealed class CycloneDxComposer
string? compositionRecipeUri = null;
request.AdditionalProperties?.TryGetValue("stellaops:composition.manifest", out compositionUri);
request.AdditionalProperties?.TryGetValue("stellaops:composition.recipe", out compositionRecipeUri);
return new CycloneDxArtifact
{
View = view,
@@ -161,7 +161,7 @@ public sealed class CycloneDxComposer
return Encoding.UTF8.GetBytes(json);
}
private Bom BuildBom(
SbomCompositionRequest request,
ComponentGraph graph,
@@ -188,29 +188,29 @@ public sealed class CycloneDxComposer
bom.SerialNumber = $"urn:uuid:{ScannerIdentifiers.CreateDeterministicGuid(SerialNamespace, Encoding.UTF8.GetBytes(serialPayload)).ToString("d", CultureInfo.InvariantCulture)}";
return bom;
}
private static Metadata BuildMetadata(SbomCompositionRequest request, SbomView view, DateTimeOffset generatedAt)
{
var metadata = new Metadata
{
Timestamp = generatedAt.UtcDateTime,
Component = BuildMetadataComponent(request.Image),
};
}
private static Metadata BuildMetadata(SbomCompositionRequest request, SbomView view, DateTimeOffset generatedAt)
{
var metadata = new Metadata
{
Timestamp = generatedAt.UtcDateTime,
Component = BuildMetadataComponent(request.Image),
};
if (request.AdditionalProperties is not null && request.AdditionalProperties.Count > 0)
{
metadata.Properties = request.AdditionalProperties
.Where(static pair => !string.IsNullOrWhiteSpace(pair.Key) && pair.Value is not null)
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.Select(pair => new Property
{
Name = pair.Key,
Value = pair.Value,
})
.ToList();
}
.Select(pair => new Property
{
Name = pair.Key,
Value = pair.Value,
})
.ToList();
}
if (metadata.Properties is null)
{
metadata.Properties = new List<Property>();
@@ -238,118 +238,118 @@ public sealed class CycloneDxComposer
{
Name = "stellaops:sbom.view",
Value = view.ToString().ToLowerInvariant(),
});
return metadata;
}
private static Component BuildMetadataComponent(ImageArtifactDescriptor image)
{
var digest = image.ImageDigest;
var digestValue = digest.Split(':', 2, StringSplitOptions.TrimEntries)[^1];
var bomRef = $"image:{digestValue}";
var name = image.ImageReference ?? image.Repository ?? digest;
var component = new Component
{
BomRef = bomRef,
Type = Component.Classification.Container,
Name = name,
Version = digestValue,
Purl = BuildImagePurl(image),
Properties = new List<Property>
{
new() { Name = "stellaops:image.digest", Value = image.ImageDigest },
},
};
if (!string.IsNullOrWhiteSpace(image.ImageReference))
{
component.Properties.Add(new Property { Name = "stellaops:image.reference", Value = image.ImageReference });
}
if (!string.IsNullOrWhiteSpace(image.Repository))
{
component.Properties.Add(new Property { Name = "stellaops:image.repository", Value = image.Repository });
}
if (!string.IsNullOrWhiteSpace(image.Tag))
{
component.Properties.Add(new Property { Name = "stellaops:image.tag", Value = image.Tag });
}
if (!string.IsNullOrWhiteSpace(image.Architecture))
{
component.Properties.Add(new Property { Name = "stellaops:image.architecture", Value = image.Architecture });
}
return component;
}
private static string? BuildImagePurl(ImageArtifactDescriptor image)
{
if (string.IsNullOrWhiteSpace(image.Repository))
{
return null;
}
var repo = image.Repository.Trim();
var tag = string.IsNullOrWhiteSpace(image.Tag) ? null : image.Tag.Trim();
var digest = image.ImageDigest.Trim();
var purlBuilder = new StringBuilder("pkg:oci/");
purlBuilder.Append(repo.Replace("/", "%2F", StringComparison.Ordinal));
if (!string.IsNullOrWhiteSpace(tag))
{
purlBuilder.Append('@').Append(tag);
}
purlBuilder.Append("?digest=").Append(Uri.EscapeDataString(digest));
if (!string.IsNullOrWhiteSpace(image.Architecture))
{
purlBuilder.Append("&arch=").Append(Uri.EscapeDataString(image.Architecture.Trim()));
}
return purlBuilder.ToString();
}
private static List<Component> BuildComponents(ImmutableArray<AggregatedComponent> components)
{
var result = new List<Component>(components.Length);
foreach (var component in components)
{
var model = new Component
{
BomRef = component.Identity.Key,
Name = component.Identity.Name,
Version = component.Identity.Version,
Purl = component.Identity.Purl,
Group = component.Identity.Group,
Type = MapClassification(component.Identity.ComponentType),
Scope = MapScope(component.Metadata?.Scope),
Properties = BuildProperties(component),
};
result.Add(model);
}
return result;
}
private static List<Property>? BuildProperties(AggregatedComponent component)
{
var properties = new List<Property>();
});
return metadata;
}
private static Component BuildMetadataComponent(ImageArtifactDescriptor image)
{
var digest = image.ImageDigest;
var digestValue = digest.Split(':', 2, StringSplitOptions.TrimEntries)[^1];
var bomRef = $"image:{digestValue}";
var name = image.ImageReference ?? image.Repository ?? digest;
var component = new Component
{
BomRef = bomRef,
Type = Component.Classification.Container,
Name = name,
Version = digestValue,
Purl = BuildImagePurl(image),
Properties = new List<Property>
{
new() { Name = "stellaops:image.digest", Value = image.ImageDigest },
},
};
if (!string.IsNullOrWhiteSpace(image.ImageReference))
{
component.Properties.Add(new Property { Name = "stellaops:image.reference", Value = image.ImageReference });
}
if (!string.IsNullOrWhiteSpace(image.Repository))
{
component.Properties.Add(new Property { Name = "stellaops:image.repository", Value = image.Repository });
}
if (!string.IsNullOrWhiteSpace(image.Tag))
{
component.Properties.Add(new Property { Name = "stellaops:image.tag", Value = image.Tag });
}
if (!string.IsNullOrWhiteSpace(image.Architecture))
{
component.Properties.Add(new Property { Name = "stellaops:image.architecture", Value = image.Architecture });
}
return component;
}
private static string? BuildImagePurl(ImageArtifactDescriptor image)
{
if (string.IsNullOrWhiteSpace(image.Repository))
{
return null;
}
var repo = image.Repository.Trim();
var tag = string.IsNullOrWhiteSpace(image.Tag) ? null : image.Tag.Trim();
var digest = image.ImageDigest.Trim();
var purlBuilder = new StringBuilder("pkg:oci/");
purlBuilder.Append(repo.Replace("/", "%2F", StringComparison.Ordinal));
if (!string.IsNullOrWhiteSpace(tag))
{
purlBuilder.Append('@').Append(tag);
}
purlBuilder.Append("?digest=").Append(Uri.EscapeDataString(digest));
if (!string.IsNullOrWhiteSpace(image.Architecture))
{
purlBuilder.Append("&arch=").Append(Uri.EscapeDataString(image.Architecture.Trim()));
}
return purlBuilder.ToString();
}
private static List<Component> BuildComponents(ImmutableArray<AggregatedComponent> components)
{
var result = new List<Component>(components.Length);
foreach (var component in components)
{
var model = new Component
{
BomRef = component.Identity.Key,
Name = component.Identity.Name,
Version = component.Identity.Version,
Purl = component.Identity.Purl,
Group = component.Identity.Group,
Type = MapClassification(component.Identity.ComponentType),
Scope = MapScope(component.Metadata?.Scope),
Properties = BuildProperties(component),
};
result.Add(model);
}
return result;
}
private static List<Property>? BuildProperties(AggregatedComponent component)
{
var properties = new List<Property>();
if (component.Metadata?.Properties is not null)
{
foreach (var property in component.Metadata.Properties.OrderBy(static pair => pair.Key, StringComparer.Ordinal))
{
properties.Add(new Property
{
Name = property.Key,
Value = property.Value,
});
{
Name = property.Key,
Value = property.Value,
});
}
}
@@ -367,80 +367,80 @@ public sealed class CycloneDxComposer
{
properties.Add(new Property { Name = "stellaops:lastLayerDigest", Value = component.LastLayerDigest });
}
if (!component.LayerDigests.IsDefaultOrEmpty)
{
properties.Add(new Property
{
Name = "stellaops:layerDigests",
Value = string.Join(",", component.LayerDigests),
});
}
if (component.Usage.UsedByEntrypoint)
{
properties.Add(new Property { Name = "stellaops:usage.usedByEntrypoint", Value = "true" });
}
if (!component.Usage.Entrypoints.IsDefaultOrEmpty && component.Usage.Entrypoints.Length > 0)
{
for (var index = 0; index < component.Usage.Entrypoints.Length; index++)
{
properties.Add(new Property
{
Name = $"stellaops:usage.entrypoint[{index}]",
Value = component.Usage.Entrypoints[index],
});
}
}
for (var index = 0; index < component.Evidence.Length; index++)
{
var evidence = component.Evidence[index];
var builder = new StringBuilder(evidence.Kind);
builder.Append(':').Append(evidence.Value);
if (!string.IsNullOrWhiteSpace(evidence.Source))
{
builder.Append('@').Append(evidence.Source);
}
properties.Add(new Property
{
Name = $"stellaops:evidence[{index}]",
Value = builder.ToString(),
});
}
return properties;
}
private static List<Dependency>? BuildDependencies(ImmutableArray<AggregatedComponent> components)
{
var componentKeys = components.Select(static component => component.Identity.Key).ToImmutableHashSet(StringComparer.Ordinal);
var dependencies = new List<Dependency>();
foreach (var component in components)
{
if (component.Dependencies.IsDefaultOrEmpty || component.Dependencies.Length == 0)
{
continue;
}
var filtered = component.Dependencies.Where(componentKeys.Contains).ToArray();
if (filtered.Length == 0)
{
continue;
}
dependencies.Add(new Dependency
{
Ref = component.Identity.Key,
Dependencies = filtered
.Select(dependencyKey => new Dependency { Ref = dependencyKey })
.ToList(),
});
}
if (!component.LayerDigests.IsDefaultOrEmpty)
{
properties.Add(new Property
{
Name = "stellaops:layerDigests",
Value = string.Join(",", component.LayerDigests),
});
}
if (component.Usage.UsedByEntrypoint)
{
properties.Add(new Property { Name = "stellaops:usage.usedByEntrypoint", Value = "true" });
}
if (!component.Usage.Entrypoints.IsDefaultOrEmpty && component.Usage.Entrypoints.Length > 0)
{
for (var index = 0; index < component.Usage.Entrypoints.Length; index++)
{
properties.Add(new Property
{
Name = $"stellaops:usage.entrypoint[{index}]",
Value = component.Usage.Entrypoints[index],
});
}
}
for (var index = 0; index < component.Evidence.Length; index++)
{
var evidence = component.Evidence[index];
var builder = new StringBuilder(evidence.Kind);
builder.Append(':').Append(evidence.Value);
if (!string.IsNullOrWhiteSpace(evidence.Source))
{
builder.Append('@').Append(evidence.Source);
}
properties.Add(new Property
{
Name = $"stellaops:evidence[{index}]",
Value = builder.ToString(),
});
}
return properties;
}
private static List<Dependency>? BuildDependencies(ImmutableArray<AggregatedComponent> components)
{
var componentKeys = components.Select(static component => component.Identity.Key).ToImmutableHashSet(StringComparer.Ordinal);
var dependencies = new List<Dependency>();
foreach (var component in components)
{
if (component.Dependencies.IsDefaultOrEmpty || component.Dependencies.Length == 0)
{
continue;
}
var filtered = component.Dependencies.Where(componentKeys.Contains).ToArray();
if (filtered.Length == 0)
{
continue;
}
dependencies.Add(new Dependency
{
Ref = component.Identity.Key,
Dependencies = filtered
.Select(dependencyKey => new Dependency { Ref = dependencyKey })
.ToList(),
});
}
return dependencies.Count == 0 ? null : dependencies;
}
@@ -606,50 +606,50 @@ public sealed class CycloneDxComposer
AddDoubleProperty(properties, name, value.Value);
}
private static Component.Classification MapClassification(string? type)
{
if (string.IsNullOrWhiteSpace(type))
{
return Component.Classification.Library;
}
return type.Trim().ToLowerInvariant() switch
{
"application" => Component.Classification.Application,
"framework" => Component.Classification.Framework,
"container" => Component.Classification.Container,
private static Component.Classification MapClassification(string? type)
{
if (string.IsNullOrWhiteSpace(type))
{
return Component.Classification.Library;
}
return type.Trim().ToLowerInvariant() switch
{
"application" => Component.Classification.Application,
"framework" => Component.Classification.Framework,
"container" => Component.Classification.Container,
"operating-system" or "os" => Component.Classification.Operating_System,
"device" => Component.Classification.Device,
"firmware" => Component.Classification.Firmware,
"file" => Component.Classification.File,
_ => Component.Classification.Library,
};
}
private static Component.ComponentScope? MapScope(string? scope)
{
if (string.IsNullOrWhiteSpace(scope))
{
return null;
}
return scope.Trim().ToLowerInvariant() switch
{
"runtime" or "required" => Component.ComponentScope.Required,
"development" or "optional" => Component.ComponentScope.Optional,
"excluded" => Component.ComponentScope.Excluded,
_ => null,
};
}
"device" => Component.Classification.Device,
"firmware" => Component.Classification.Firmware,
"file" => Component.Classification.File,
_ => Component.Classification.Library,
};
}
private static Component.ComponentScope? MapScope(string? scope)
{
if (string.IsNullOrWhiteSpace(scope))
{
return null;
}
return scope.Trim().ToLowerInvariant() switch
{
"runtime" or "required" => Component.ComponentScope.Required,
"development" or "optional" => Component.ComponentScope.Optional,
"excluded" => Component.ComponentScope.Excluded,
_ => null,
};
}
private static string FormatDouble(double value)
=> value.ToString("0.############################", CultureInfo.InvariantCulture);
private static string ComputeSha256(byte[] bytes)
{
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}
{
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}