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
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Core.Utility;
|
||||
|
||||
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;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Core.Utility;
|
||||
|
||||
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;
|
||||
|
||||
@@ -58,17 +58,17 @@ public sealed record SbomCompositionRequest
|
||||
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,
|
||||
{
|
||||
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),
|
||||
@@ -80,10 +80,10 @@ public sealed record SbomCompositionRequest
|
||||
|
||||
private static string? Normalize(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Composition;
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Composition;
|
||||
|
||||
public sealed record CycloneDxArtifact
|
||||
{
|
||||
public required SbomView View { get; init; }
|
||||
|
||||
public required string SerialNumber { get; init; }
|
||||
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
public required ImmutableArray<AggregatedComponent> Components { get; init; }
|
||||
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
public required ImmutableArray<AggregatedComponent> Components { get; init; }
|
||||
|
||||
public required byte[] JsonBytes { get; init; }
|
||||
|
||||
@@ -43,10 +43,10 @@ public sealed record CycloneDxArtifact
|
||||
public required byte[] ProtobufBytes { get; init; }
|
||||
|
||||
public required string ProtobufSha256 { get; init; }
|
||||
|
||||
public required string ProtobufMediaType { get; init; }
|
||||
}
|
||||
|
||||
|
||||
public required string ProtobufMediaType { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SbomCompositionResult
|
||||
{
|
||||
public required CycloneDxArtifact Inventory { get; init; }
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Composition;
|
||||
|
||||
public sealed record SbomPolicyFinding
|
||||
{
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
public required string ComponentKey { get; init; }
|
||||
|
||||
public string? VulnerabilityId { get; init; }
|
||||
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
public double Score { get; init; }
|
||||
|
||||
public string ConfigVersion { get; init; } = string.Empty;
|
||||
|
||||
public ImmutableArray<KeyValuePair<string, double>> Inputs { get; init; } = ImmutableArray<KeyValuePair<string, double>>.Empty;
|
||||
|
||||
public string? QuietedBy { get; init; }
|
||||
|
||||
public bool Quiet { get; init; }
|
||||
|
||||
public double? UnknownConfidence { get; init; }
|
||||
|
||||
public string? ConfidenceBand { get; init; }
|
||||
|
||||
public double? UnknownAgeDays { get; init; }
|
||||
|
||||
public string? SourceTrust { get; init; }
|
||||
|
||||
public string? Reachability { get; init; }
|
||||
|
||||
internal SbomPolicyFinding Normalize()
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(FindingId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(ComponentKey);
|
||||
|
||||
var normalizedInputs = Inputs.IsDefaultOrEmpty
|
||||
? ImmutableArray<KeyValuePair<string, double>>.Empty
|
||||
: Inputs
|
||||
.Where(static pair => !string.IsNullOrWhiteSpace(pair.Key))
|
||||
.Select(static pair => new KeyValuePair<string, double>(pair.Key.Trim(), pair.Value))
|
||||
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return this with
|
||||
{
|
||||
FindingId = FindingId.Trim(),
|
||||
ComponentKey = ComponentKey.Trim(),
|
||||
VulnerabilityId = string.IsNullOrWhiteSpace(VulnerabilityId) ? null : VulnerabilityId.Trim(),
|
||||
Status = string.IsNullOrWhiteSpace(Status) ? string.Empty : Status.Trim(),
|
||||
ConfigVersion = string.IsNullOrWhiteSpace(ConfigVersion) ? string.Empty : ConfigVersion.Trim(),
|
||||
QuietedBy = string.IsNullOrWhiteSpace(QuietedBy) ? null : QuietedBy.Trim(),
|
||||
ConfidenceBand = string.IsNullOrWhiteSpace(ConfidenceBand) ? null : ConfidenceBand.Trim(),
|
||||
SourceTrust = string.IsNullOrWhiteSpace(SourceTrust) ? null : SourceTrust.Trim(),
|
||||
Reachability = string.IsNullOrWhiteSpace(Reachability) ? null : Reachability.Trim(),
|
||||
Inputs = normalizedInputs
|
||||
};
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Composition;
|
||||
|
||||
public sealed record SbomPolicyFinding
|
||||
{
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
public required string ComponentKey { get; init; }
|
||||
|
||||
public string? VulnerabilityId { get; init; }
|
||||
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
public double Score { get; init; }
|
||||
|
||||
public string ConfigVersion { get; init; } = string.Empty;
|
||||
|
||||
public ImmutableArray<KeyValuePair<string, double>> Inputs { get; init; } = ImmutableArray<KeyValuePair<string, double>>.Empty;
|
||||
|
||||
public string? QuietedBy { get; init; }
|
||||
|
||||
public bool Quiet { get; init; }
|
||||
|
||||
public double? UnknownConfidence { get; init; }
|
||||
|
||||
public string? ConfidenceBand { get; init; }
|
||||
|
||||
public double? UnknownAgeDays { get; init; }
|
||||
|
||||
public string? SourceTrust { get; init; }
|
||||
|
||||
public string? Reachability { get; init; }
|
||||
|
||||
internal SbomPolicyFinding Normalize()
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(FindingId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(ComponentKey);
|
||||
|
||||
var normalizedInputs = Inputs.IsDefaultOrEmpty
|
||||
? ImmutableArray<KeyValuePair<string, double>>.Empty
|
||||
: Inputs
|
||||
.Where(static pair => !string.IsNullOrWhiteSpace(pair.Key))
|
||||
.Select(static pair => new KeyValuePair<string, double>(pair.Key.Trim(), pair.Value))
|
||||
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return this with
|
||||
{
|
||||
FindingId = FindingId.Trim(),
|
||||
ComponentKey = ComponentKey.Trim(),
|
||||
VulnerabilityId = string.IsNullOrWhiteSpace(VulnerabilityId) ? null : VulnerabilityId.Trim(),
|
||||
Status = string.IsNullOrWhiteSpace(Status) ? string.Empty : Status.Trim(),
|
||||
ConfigVersion = string.IsNullOrWhiteSpace(ConfigVersion) ? string.Empty : ConfigVersion.Trim(),
|
||||
QuietedBy = string.IsNullOrWhiteSpace(QuietedBy) ? null : QuietedBy.Trim(),
|
||||
ConfidenceBand = string.IsNullOrWhiteSpace(ConfidenceBand) ? null : ConfidenceBand.Trim(),
|
||||
SourceTrust = string.IsNullOrWhiteSpace(SourceTrust) ? null : SourceTrust.Trim(),
|
||||
Reachability = string.IsNullOrWhiteSpace(Reachability) ? null : Reachability.Trim(),
|
||||
Inputs = normalizedInputs
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Composition;
|
||||
|
||||
public static class ScanAnalysisCompositionBuilder
|
||||
{
|
||||
public static SbomCompositionRequest FromAnalysis(
|
||||
ScanAnalysisStore analysis,
|
||||
ImageArtifactDescriptor image,
|
||||
DateTimeOffset generatedAt,
|
||||
string? generatorName = null,
|
||||
string? generatorVersion = null,
|
||||
IReadOnlyDictionary<string, string>? properties = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(analysis);
|
||||
ArgumentNullException.ThrowIfNull(image);
|
||||
|
||||
var fragments = analysis.GetLayerFragments();
|
||||
if (fragments.IsDefaultOrEmpty)
|
||||
{
|
||||
throw new InvalidOperationException("No layer fragments recorded in analysis.");
|
||||
}
|
||||
|
||||
return SbomCompositionRequest.Create(
|
||||
image,
|
||||
fragments,
|
||||
generatedAt,
|
||||
generatorName,
|
||||
generatorVersion,
|
||||
properties);
|
||||
}
|
||||
|
||||
public static ComponentGraph BuildComponentGraph(ScanAnalysisStore analysis)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(analysis);
|
||||
|
||||
var fragments = analysis.GetLayerFragments();
|
||||
if (fragments.IsDefaultOrEmpty)
|
||||
{
|
||||
return new ComponentGraph
|
||||
{
|
||||
Layers = ImmutableArray<LayerComponentFragment>.Empty,
|
||||
Components = ImmutableArray<AggregatedComponent>.Empty,
|
||||
ComponentMap = ImmutableDictionary<string, AggregatedComponent>.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
return ComponentGraphBuilder.Build(fragments);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Composition;
|
||||
|
||||
public static class ScanAnalysisCompositionBuilder
|
||||
{
|
||||
public static SbomCompositionRequest FromAnalysis(
|
||||
ScanAnalysisStore analysis,
|
||||
ImageArtifactDescriptor image,
|
||||
DateTimeOffset generatedAt,
|
||||
string? generatorName = null,
|
||||
string? generatorVersion = null,
|
||||
IReadOnlyDictionary<string, string>? properties = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(analysis);
|
||||
ArgumentNullException.ThrowIfNull(image);
|
||||
|
||||
var fragments = analysis.GetLayerFragments();
|
||||
if (fragments.IsDefaultOrEmpty)
|
||||
{
|
||||
throw new InvalidOperationException("No layer fragments recorded in analysis.");
|
||||
}
|
||||
|
||||
return SbomCompositionRequest.Create(
|
||||
image,
|
||||
fragments,
|
||||
generatedAt,
|
||||
generatorName,
|
||||
generatorVersion,
|
||||
properties);
|
||||
}
|
||||
|
||||
public static ComponentGraph BuildComponentGraph(ScanAnalysisStore analysis)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(analysis);
|
||||
|
||||
var fragments = analysis.GetLayerFragments();
|
||||
if (fragments.IsDefaultOrEmpty)
|
||||
{
|
||||
return new ComponentGraph
|
||||
{
|
||||
Layers = ImmutableArray<LayerComponentFragment>.Empty,
|
||||
Components = ImmutableArray<AggregatedComponent>.Empty,
|
||||
ComponentMap = ImmutableDictionary<string, AggregatedComponent>.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
return ComponentGraphBuilder.Build(fragments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,239 +1,239 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Collections.Special;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Index;
|
||||
|
||||
public sealed record BomIndexBuildRequest
|
||||
{
|
||||
public required string ImageDigest { get; init; }
|
||||
|
||||
public required ComponentGraph Graph { get; init; }
|
||||
|
||||
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed record BomIndexArtifact
|
||||
{
|
||||
public required byte[] Bytes { get; init; }
|
||||
|
||||
public required string Sha256 { get; init; }
|
||||
|
||||
public required int LayerCount { get; init; }
|
||||
|
||||
public required int ComponentCount { get; init; }
|
||||
|
||||
public required int EntrypointCount { get; init; }
|
||||
|
||||
public string MediaType { get; init; } = "application/vnd.stellaops.bom-index.v1+binary";
|
||||
}
|
||||
|
||||
public sealed class BomIndexBuilder
|
||||
{
|
||||
private static readonly byte[] Magic = Encoding.ASCII.GetBytes("BOMIDX1");
|
||||
|
||||
public BomIndexArtifact Build(BomIndexBuildRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
if (string.IsNullOrWhiteSpace(request.ImageDigest))
|
||||
{
|
||||
throw new ArgumentException("Image digest is required.", nameof(request));
|
||||
}
|
||||
|
||||
var normalizedDigest = request.ImageDigest.Trim();
|
||||
var graph = request.Graph ?? throw new ArgumentNullException(nameof(request.Graph));
|
||||
var layers = graph.Layers.Select(layer => layer.LayerDigest).ToImmutableArray();
|
||||
var components = graph.Components;
|
||||
|
||||
var layerIndex = new Dictionary<string, int>(layers.Length, StringComparer.Ordinal);
|
||||
for (var i = 0; i < layers.Length; i++)
|
||||
{
|
||||
layerIndex[layers[i]] = i;
|
||||
}
|
||||
|
||||
var entrypointSet = new SortedSet<string>(StringComparer.Ordinal);
|
||||
foreach (var component in components)
|
||||
{
|
||||
if (!component.Usage.Entrypoints.IsDefaultOrEmpty)
|
||||
{
|
||||
foreach (var entry in component.Usage.Entrypoints)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(entry))
|
||||
{
|
||||
entrypointSet.Add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var entrypoints = entrypointSet.ToImmutableArray();
|
||||
var entrypointIndex = new Dictionary<string, int>(entrypoints.Length, StringComparer.Ordinal);
|
||||
for (var i = 0; i < entrypoints.Length; i++)
|
||||
{
|
||||
entrypointIndex[entrypoints[i]] = i;
|
||||
}
|
||||
|
||||
using var buffer = new MemoryStream();
|
||||
using var writer = new BinaryWriter(buffer, Encoding.UTF8, leaveOpen: true);
|
||||
|
||||
WriteHeader(writer, normalizedDigest, request.GeneratedAt, layers.Length, components.Length, entrypoints.Length);
|
||||
WriteLayerTable(writer, layers);
|
||||
WriteComponentTable(writer, components);
|
||||
WriteComponentBitmaps(writer, components, layerIndex);
|
||||
|
||||
if (entrypoints.Length > 0)
|
||||
{
|
||||
WriteEntrypointTable(writer, entrypoints);
|
||||
WriteEntrypointBitmaps(writer, components, entrypointIndex);
|
||||
}
|
||||
|
||||
writer.Flush();
|
||||
var bytes = buffer.ToArray();
|
||||
var sha256 = ComputeSha256(bytes);
|
||||
|
||||
return new BomIndexArtifact
|
||||
{
|
||||
Bytes = bytes,
|
||||
Sha256 = sha256,
|
||||
LayerCount = layers.Length,
|
||||
ComponentCount = components.Length,
|
||||
EntrypointCount = entrypoints.Length,
|
||||
};
|
||||
}
|
||||
|
||||
private static void WriteHeader(BinaryWriter writer, string imageDigest, DateTimeOffset generatedAt, int layerCount, int componentCount, int entrypointCount)
|
||||
{
|
||||
writer.Write(Magic);
|
||||
writer.Write((ushort)1); // version
|
||||
|
||||
var flags = (ushort)0;
|
||||
if (entrypointCount > 0)
|
||||
{
|
||||
flags |= 0x1;
|
||||
}
|
||||
|
||||
writer.Write(flags);
|
||||
|
||||
var digestBytes = Encoding.UTF8.GetBytes(imageDigest);
|
||||
if (digestBytes.Length > ushort.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException("Image digest exceeds maximum length.");
|
||||
}
|
||||
|
||||
writer.Write((ushort)digestBytes.Length);
|
||||
writer.Write(digestBytes);
|
||||
|
||||
var unixMicroseconds = ToUnixMicroseconds(generatedAt);
|
||||
writer.Write(unixMicroseconds);
|
||||
|
||||
writer.Write((uint)layerCount);
|
||||
writer.Write((uint)componentCount);
|
||||
writer.Write((uint)entrypointCount);
|
||||
}
|
||||
|
||||
private static void WriteLayerTable(BinaryWriter writer, ImmutableArray<string> layers)
|
||||
{
|
||||
foreach (var layer in layers)
|
||||
{
|
||||
WriteUtf8String(writer, layer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteComponentTable(BinaryWriter writer, ImmutableArray<AggregatedComponent> components)
|
||||
{
|
||||
foreach (var component in components)
|
||||
{
|
||||
var key = component.Identity.Purl ?? component.Identity.Key;
|
||||
WriteUtf8String(writer, key);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteComponentBitmaps(BinaryWriter writer, ImmutableArray<AggregatedComponent> components, IReadOnlyDictionary<string, int> layerIndex)
|
||||
{
|
||||
foreach (var component in components)
|
||||
{
|
||||
var indices = component.LayerDigests
|
||||
.Select(digest => layerIndex.TryGetValue(digest, out var index) ? index : -1)
|
||||
.Where(index => index >= 0)
|
||||
.Distinct()
|
||||
.OrderBy(index => index)
|
||||
.ToArray();
|
||||
|
||||
var bitmap = RoaringBitmap.Create(indices).Optimize();
|
||||
WriteBitmap(writer, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteEntrypointTable(BinaryWriter writer, ImmutableArray<string> entrypoints)
|
||||
{
|
||||
foreach (var entry in entrypoints)
|
||||
{
|
||||
WriteUtf8String(writer, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteEntrypointBitmaps(BinaryWriter writer, ImmutableArray<AggregatedComponent> components, IReadOnlyDictionary<string, int> entrypointIndex)
|
||||
{
|
||||
foreach (var component in components)
|
||||
{
|
||||
var indices = component.Usage.Entrypoints
|
||||
.Where(entrypointIndex.ContainsKey)
|
||||
.Select(entry => entrypointIndex[entry])
|
||||
.Distinct()
|
||||
.OrderBy(index => index)
|
||||
.ToArray();
|
||||
|
||||
if (indices.Length == 0)
|
||||
{
|
||||
writer.Write((uint)0);
|
||||
continue;
|
||||
}
|
||||
|
||||
var bitmap = RoaringBitmap.Create(indices).Optimize();
|
||||
WriteBitmap(writer, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteBitmap(BinaryWriter writer, RoaringBitmap bitmap)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
RoaringBitmap.Serialize(bitmap, ms);
|
||||
var data = ms.ToArray();
|
||||
writer.Write((uint)data.Length);
|
||||
writer.Write(data);
|
||||
}
|
||||
|
||||
private static void WriteUtf8String(BinaryWriter writer, string value)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value ?? string.Empty);
|
||||
if (bytes.Length > ushort.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException("String value exceeds maximum length supported by BOM index.");
|
||||
}
|
||||
|
||||
writer.Write((ushort)bytes.Length);
|
||||
writer.Write(bytes);
|
||||
}
|
||||
|
||||
private static long ToUnixMicroseconds(DateTimeOffset timestamp)
|
||||
{
|
||||
var normalized = timestamp.ToUniversalTime();
|
||||
var microseconds = normalized.ToUnixTimeMilliseconds() * 1000L;
|
||||
microseconds += normalized.Ticks % TimeSpan.TicksPerMillisecond / 10;
|
||||
return microseconds;
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] data)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Collections.Special;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Index;
|
||||
|
||||
public sealed record BomIndexBuildRequest
|
||||
{
|
||||
public required string ImageDigest { get; init; }
|
||||
|
||||
public required ComponentGraph Graph { get; init; }
|
||||
|
||||
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed record BomIndexArtifact
|
||||
{
|
||||
public required byte[] Bytes { get; init; }
|
||||
|
||||
public required string Sha256 { get; init; }
|
||||
|
||||
public required int LayerCount { get; init; }
|
||||
|
||||
public required int ComponentCount { get; init; }
|
||||
|
||||
public required int EntrypointCount { get; init; }
|
||||
|
||||
public string MediaType { get; init; } = "application/vnd.stellaops.bom-index.v1+binary";
|
||||
}
|
||||
|
||||
public sealed class BomIndexBuilder
|
||||
{
|
||||
private static readonly byte[] Magic = Encoding.ASCII.GetBytes("BOMIDX1");
|
||||
|
||||
public BomIndexArtifact Build(BomIndexBuildRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
if (string.IsNullOrWhiteSpace(request.ImageDigest))
|
||||
{
|
||||
throw new ArgumentException("Image digest is required.", nameof(request));
|
||||
}
|
||||
|
||||
var normalizedDigest = request.ImageDigest.Trim();
|
||||
var graph = request.Graph ?? throw new ArgumentNullException(nameof(request.Graph));
|
||||
var layers = graph.Layers.Select(layer => layer.LayerDigest).ToImmutableArray();
|
||||
var components = graph.Components;
|
||||
|
||||
var layerIndex = new Dictionary<string, int>(layers.Length, StringComparer.Ordinal);
|
||||
for (var i = 0; i < layers.Length; i++)
|
||||
{
|
||||
layerIndex[layers[i]] = i;
|
||||
}
|
||||
|
||||
var entrypointSet = new SortedSet<string>(StringComparer.Ordinal);
|
||||
foreach (var component in components)
|
||||
{
|
||||
if (!component.Usage.Entrypoints.IsDefaultOrEmpty)
|
||||
{
|
||||
foreach (var entry in component.Usage.Entrypoints)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(entry))
|
||||
{
|
||||
entrypointSet.Add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var entrypoints = entrypointSet.ToImmutableArray();
|
||||
var entrypointIndex = new Dictionary<string, int>(entrypoints.Length, StringComparer.Ordinal);
|
||||
for (var i = 0; i < entrypoints.Length; i++)
|
||||
{
|
||||
entrypointIndex[entrypoints[i]] = i;
|
||||
}
|
||||
|
||||
using var buffer = new MemoryStream();
|
||||
using var writer = new BinaryWriter(buffer, Encoding.UTF8, leaveOpen: true);
|
||||
|
||||
WriteHeader(writer, normalizedDigest, request.GeneratedAt, layers.Length, components.Length, entrypoints.Length);
|
||||
WriteLayerTable(writer, layers);
|
||||
WriteComponentTable(writer, components);
|
||||
WriteComponentBitmaps(writer, components, layerIndex);
|
||||
|
||||
if (entrypoints.Length > 0)
|
||||
{
|
||||
WriteEntrypointTable(writer, entrypoints);
|
||||
WriteEntrypointBitmaps(writer, components, entrypointIndex);
|
||||
}
|
||||
|
||||
writer.Flush();
|
||||
var bytes = buffer.ToArray();
|
||||
var sha256 = ComputeSha256(bytes);
|
||||
|
||||
return new BomIndexArtifact
|
||||
{
|
||||
Bytes = bytes,
|
||||
Sha256 = sha256,
|
||||
LayerCount = layers.Length,
|
||||
ComponentCount = components.Length,
|
||||
EntrypointCount = entrypoints.Length,
|
||||
};
|
||||
}
|
||||
|
||||
private static void WriteHeader(BinaryWriter writer, string imageDigest, DateTimeOffset generatedAt, int layerCount, int componentCount, int entrypointCount)
|
||||
{
|
||||
writer.Write(Magic);
|
||||
writer.Write((ushort)1); // version
|
||||
|
||||
var flags = (ushort)0;
|
||||
if (entrypointCount > 0)
|
||||
{
|
||||
flags |= 0x1;
|
||||
}
|
||||
|
||||
writer.Write(flags);
|
||||
|
||||
var digestBytes = Encoding.UTF8.GetBytes(imageDigest);
|
||||
if (digestBytes.Length > ushort.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException("Image digest exceeds maximum length.");
|
||||
}
|
||||
|
||||
writer.Write((ushort)digestBytes.Length);
|
||||
writer.Write(digestBytes);
|
||||
|
||||
var unixMicroseconds = ToUnixMicroseconds(generatedAt);
|
||||
writer.Write(unixMicroseconds);
|
||||
|
||||
writer.Write((uint)layerCount);
|
||||
writer.Write((uint)componentCount);
|
||||
writer.Write((uint)entrypointCount);
|
||||
}
|
||||
|
||||
private static void WriteLayerTable(BinaryWriter writer, ImmutableArray<string> layers)
|
||||
{
|
||||
foreach (var layer in layers)
|
||||
{
|
||||
WriteUtf8String(writer, layer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteComponentTable(BinaryWriter writer, ImmutableArray<AggregatedComponent> components)
|
||||
{
|
||||
foreach (var component in components)
|
||||
{
|
||||
var key = component.Identity.Purl ?? component.Identity.Key;
|
||||
WriteUtf8String(writer, key);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteComponentBitmaps(BinaryWriter writer, ImmutableArray<AggregatedComponent> components, IReadOnlyDictionary<string, int> layerIndex)
|
||||
{
|
||||
foreach (var component in components)
|
||||
{
|
||||
var indices = component.LayerDigests
|
||||
.Select(digest => layerIndex.TryGetValue(digest, out var index) ? index : -1)
|
||||
.Where(index => index >= 0)
|
||||
.Distinct()
|
||||
.OrderBy(index => index)
|
||||
.ToArray();
|
||||
|
||||
var bitmap = RoaringBitmap.Create(indices).Optimize();
|
||||
WriteBitmap(writer, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteEntrypointTable(BinaryWriter writer, ImmutableArray<string> entrypoints)
|
||||
{
|
||||
foreach (var entry in entrypoints)
|
||||
{
|
||||
WriteUtf8String(writer, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteEntrypointBitmaps(BinaryWriter writer, ImmutableArray<AggregatedComponent> components, IReadOnlyDictionary<string, int> entrypointIndex)
|
||||
{
|
||||
foreach (var component in components)
|
||||
{
|
||||
var indices = component.Usage.Entrypoints
|
||||
.Where(entrypointIndex.ContainsKey)
|
||||
.Select(entry => entrypointIndex[entry])
|
||||
.Distinct()
|
||||
.OrderBy(index => index)
|
||||
.ToArray();
|
||||
|
||||
if (indices.Length == 0)
|
||||
{
|
||||
writer.Write((uint)0);
|
||||
continue;
|
||||
}
|
||||
|
||||
var bitmap = RoaringBitmap.Create(indices).Optimize();
|
||||
WriteBitmap(writer, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteBitmap(BinaryWriter writer, RoaringBitmap bitmap)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
RoaringBitmap.Serialize(bitmap, ms);
|
||||
var data = ms.ToArray();
|
||||
writer.Write((uint)data.Length);
|
||||
writer.Write(data);
|
||||
}
|
||||
|
||||
private static void WriteUtf8String(BinaryWriter writer, string value)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value ?? string.Empty);
|
||||
if (bytes.Length > ushort.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException("String value exceeds maximum length supported by BOM index.");
|
||||
}
|
||||
|
||||
writer.Write((ushort)bytes.Length);
|
||||
writer.Write(bytes);
|
||||
}
|
||||
|
||||
private static long ToUnixMicroseconds(DateTimeOffset timestamp)
|
||||
{
|
||||
var normalized = timestamp.ToUniversalTime();
|
||||
var microseconds = normalized.ToUnixTimeMilliseconds() * 1000L;
|
||||
microseconds += normalized.Ticks % TimeSpan.TicksPerMillisecond / 10;
|
||||
return microseconds;
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] data)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Core.Serialization;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using StellaOps.Scanner.Emit.Index;
|
||||
using StellaOps.Scanner.Storage.Catalog;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Packaging;
|
||||
|
||||
public sealed record ScannerArtifactDescriptor
|
||||
{
|
||||
public required ArtifactDocumentType Type { get; init; }
|
||||
|
||||
public required ArtifactDocumentFormat Format { get; init; }
|
||||
|
||||
public required string MediaType { get; init; }
|
||||
|
||||
public required ReadOnlyMemory<byte> Content { get; init; }
|
||||
|
||||
public required string Sha256 { get; init; }
|
||||
|
||||
public SbomView? View { get; init; }
|
||||
|
||||
public long Size => Content.Length;
|
||||
}
|
||||
|
||||
public sealed record ScannerArtifactManifestEntry
|
||||
{
|
||||
public required string Kind { get; init; }
|
||||
|
||||
public required ArtifactDocumentType Type { get; init; }
|
||||
|
||||
public required ArtifactDocumentFormat Format { get; init; }
|
||||
|
||||
public required string MediaType { get; init; }
|
||||
|
||||
public required string Sha256 { get; init; }
|
||||
|
||||
public required long Size { get; init; }
|
||||
|
||||
public SbomView? View { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ScannerArtifactManifest
|
||||
{
|
||||
public required string ImageDigest { get; init; }
|
||||
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
public required ImmutableArray<ScannerArtifactManifestEntry> Artifacts { get; init; }
|
||||
|
||||
public byte[] ToJsonBytes()
|
||||
=> JsonSerializer.SerializeToUtf8Bytes(this, ScannerJsonOptions.Default);
|
||||
}
|
||||
|
||||
public sealed record ScannerArtifactPackage
|
||||
{
|
||||
public required ImmutableArray<ScannerArtifactDescriptor> Artifacts { get; init; }
|
||||
|
||||
public required ScannerArtifactManifest Manifest { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ScannerArtifactPackageBuilder
|
||||
{
|
||||
public ScannerArtifactPackage Build(
|
||||
string imageDigest,
|
||||
DateTimeOffset generatedAt,
|
||||
SbomCompositionResult composition,
|
||||
BomIndexArtifact bomIndex)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imageDigest))
|
||||
{
|
||||
throw new ArgumentException("Image digest is required.", nameof(imageDigest));
|
||||
}
|
||||
|
||||
var descriptors = new List<ScannerArtifactDescriptor>();
|
||||
|
||||
descriptors.Add(CreateDescriptor(ArtifactDocumentType.ImageBom, ArtifactDocumentFormat.CycloneDxJson, composition.Inventory.JsonMediaType, composition.Inventory.JsonBytes, composition.Inventory.JsonSha256, SbomView.Inventory));
|
||||
descriptors.Add(CreateDescriptor(ArtifactDocumentType.ImageBom, ArtifactDocumentFormat.CycloneDxProtobuf, composition.Inventory.ProtobufMediaType, composition.Inventory.ProtobufBytes, composition.Inventory.ProtobufSha256, SbomView.Inventory));
|
||||
|
||||
if (composition.Usage is not null)
|
||||
{
|
||||
descriptors.Add(CreateDescriptor(ArtifactDocumentType.ImageBom, ArtifactDocumentFormat.CycloneDxJson, composition.Usage.JsonMediaType, composition.Usage.JsonBytes, composition.Usage.JsonSha256, SbomView.Usage));
|
||||
descriptors.Add(CreateDescriptor(ArtifactDocumentType.ImageBom, ArtifactDocumentFormat.CycloneDxProtobuf, composition.Usage.ProtobufMediaType, composition.Usage.ProtobufBytes, composition.Usage.ProtobufSha256, SbomView.Usage));
|
||||
}
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Core.Serialization;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using StellaOps.Scanner.Emit.Index;
|
||||
using StellaOps.Scanner.Storage.Catalog;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Packaging;
|
||||
|
||||
public sealed record ScannerArtifactDescriptor
|
||||
{
|
||||
public required ArtifactDocumentType Type { get; init; }
|
||||
|
||||
public required ArtifactDocumentFormat Format { get; init; }
|
||||
|
||||
public required string MediaType { get; init; }
|
||||
|
||||
public required ReadOnlyMemory<byte> Content { get; init; }
|
||||
|
||||
public required string Sha256 { get; init; }
|
||||
|
||||
public SbomView? View { get; init; }
|
||||
|
||||
public long Size => Content.Length;
|
||||
}
|
||||
|
||||
public sealed record ScannerArtifactManifestEntry
|
||||
{
|
||||
public required string Kind { get; init; }
|
||||
|
||||
public required ArtifactDocumentType Type { get; init; }
|
||||
|
||||
public required ArtifactDocumentFormat Format { get; init; }
|
||||
|
||||
public required string MediaType { get; init; }
|
||||
|
||||
public required string Sha256 { get; init; }
|
||||
|
||||
public required long Size { get; init; }
|
||||
|
||||
public SbomView? View { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ScannerArtifactManifest
|
||||
{
|
||||
public required string ImageDigest { get; init; }
|
||||
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
public required ImmutableArray<ScannerArtifactManifestEntry> Artifacts { get; init; }
|
||||
|
||||
public byte[] ToJsonBytes()
|
||||
=> JsonSerializer.SerializeToUtf8Bytes(this, ScannerJsonOptions.Default);
|
||||
}
|
||||
|
||||
public sealed record ScannerArtifactPackage
|
||||
{
|
||||
public required ImmutableArray<ScannerArtifactDescriptor> Artifacts { get; init; }
|
||||
|
||||
public required ScannerArtifactManifest Manifest { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ScannerArtifactPackageBuilder
|
||||
{
|
||||
public ScannerArtifactPackage Build(
|
||||
string imageDigest,
|
||||
DateTimeOffset generatedAt,
|
||||
SbomCompositionResult composition,
|
||||
BomIndexArtifact bomIndex)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imageDigest))
|
||||
{
|
||||
throw new ArgumentException("Image digest is required.", nameof(imageDigest));
|
||||
}
|
||||
|
||||
var descriptors = new List<ScannerArtifactDescriptor>();
|
||||
|
||||
descriptors.Add(CreateDescriptor(ArtifactDocumentType.ImageBom, ArtifactDocumentFormat.CycloneDxJson, composition.Inventory.JsonMediaType, composition.Inventory.JsonBytes, composition.Inventory.JsonSha256, SbomView.Inventory));
|
||||
descriptors.Add(CreateDescriptor(ArtifactDocumentType.ImageBom, ArtifactDocumentFormat.CycloneDxProtobuf, composition.Inventory.ProtobufMediaType, composition.Inventory.ProtobufBytes, composition.Inventory.ProtobufSha256, SbomView.Inventory));
|
||||
|
||||
if (composition.Usage is not null)
|
||||
{
|
||||
descriptors.Add(CreateDescriptor(ArtifactDocumentType.ImageBom, ArtifactDocumentFormat.CycloneDxJson, composition.Usage.JsonMediaType, composition.Usage.JsonBytes, composition.Usage.JsonSha256, SbomView.Usage));
|
||||
descriptors.Add(CreateDescriptor(ArtifactDocumentType.ImageBom, ArtifactDocumentFormat.CycloneDxProtobuf, composition.Usage.ProtobufMediaType, composition.Usage.ProtobufBytes, composition.Usage.ProtobufSha256, SbomView.Usage));
|
||||
}
|
||||
|
||||
descriptors.Add(CreateDescriptor(ArtifactDocumentType.Index, ArtifactDocumentFormat.BomIndex, "application/vnd.stellaops.bom-index.v1+binary", bomIndex.Bytes, bomIndex.Sha256, null));
|
||||
|
||||
descriptors.Add(CreateDescriptor(
|
||||
@@ -97,7 +97,7 @@ public sealed class ScannerArtifactPackageBuilder
|
||||
composition.CompositionRecipeJson,
|
||||
composition.CompositionRecipeSha256,
|
||||
null));
|
||||
|
||||
|
||||
var manifest = new ScannerArtifactManifest
|
||||
{
|
||||
ImageDigest = imageDigest.Trim(),
|
||||
@@ -108,56 +108,56 @@ public sealed class ScannerArtifactPackageBuilder
|
||||
.ThenBy(entry => entry.Format)
|
||||
.ToImmutableArray(),
|
||||
};
|
||||
|
||||
return new ScannerArtifactPackage
|
||||
{
|
||||
Artifacts = descriptors.ToImmutableArray(),
|
||||
Manifest = manifest,
|
||||
};
|
||||
}
|
||||
|
||||
private static ScannerArtifactDescriptor CreateDescriptor(
|
||||
ArtifactDocumentType type,
|
||||
ArtifactDocumentFormat format,
|
||||
string mediaType,
|
||||
ReadOnlyMemory<byte> content,
|
||||
string sha256,
|
||||
SbomView? view)
|
||||
{
|
||||
return new ScannerArtifactDescriptor
|
||||
{
|
||||
Type = type,
|
||||
Format = format,
|
||||
MediaType = mediaType,
|
||||
Content = content,
|
||||
Sha256 = sha256,
|
||||
View = view,
|
||||
};
|
||||
}
|
||||
|
||||
private static ScannerArtifactManifestEntry ToManifestEntry(ScannerArtifactDescriptor descriptor)
|
||||
{
|
||||
var kind = descriptor.Type switch
|
||||
{
|
||||
ArtifactDocumentType.Index => "bom-index",
|
||||
ArtifactDocumentType.ImageBom when descriptor.View == SbomView.Usage => "sbom-usage",
|
||||
ArtifactDocumentType.ImageBom => "sbom-inventory",
|
||||
ArtifactDocumentType.LayerBom => "layer-sbom",
|
||||
ArtifactDocumentType.Diff => "diff",
|
||||
|
||||
return new ScannerArtifactPackage
|
||||
{
|
||||
Artifacts = descriptors.ToImmutableArray(),
|
||||
Manifest = manifest,
|
||||
};
|
||||
}
|
||||
|
||||
private static ScannerArtifactDescriptor CreateDescriptor(
|
||||
ArtifactDocumentType type,
|
||||
ArtifactDocumentFormat format,
|
||||
string mediaType,
|
||||
ReadOnlyMemory<byte> content,
|
||||
string sha256,
|
||||
SbomView? view)
|
||||
{
|
||||
return new ScannerArtifactDescriptor
|
||||
{
|
||||
Type = type,
|
||||
Format = format,
|
||||
MediaType = mediaType,
|
||||
Content = content,
|
||||
Sha256 = sha256,
|
||||
View = view,
|
||||
};
|
||||
}
|
||||
|
||||
private static ScannerArtifactManifestEntry ToManifestEntry(ScannerArtifactDescriptor descriptor)
|
||||
{
|
||||
var kind = descriptor.Type switch
|
||||
{
|
||||
ArtifactDocumentType.Index => "bom-index",
|
||||
ArtifactDocumentType.ImageBom when descriptor.View == SbomView.Usage => "sbom-usage",
|
||||
ArtifactDocumentType.ImageBom => "sbom-inventory",
|
||||
ArtifactDocumentType.LayerBom => "layer-sbom",
|
||||
ArtifactDocumentType.Diff => "diff",
|
||||
ArtifactDocumentType.Attestation => "attestation",
|
||||
ArtifactDocumentType.CompositionRecipe => "composition-recipe",
|
||||
_ => descriptor.Type.ToString().ToLowerInvariant(),
|
||||
};
|
||||
|
||||
return new ScannerArtifactManifestEntry
|
||||
{
|
||||
Kind = kind,
|
||||
Type = descriptor.Type,
|
||||
Format = descriptor.Format,
|
||||
MediaType = descriptor.MediaType,
|
||||
Sha256 = descriptor.Sha256,
|
||||
Size = descriptor.Size,
|
||||
View = descriptor.View,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new ScannerArtifactManifestEntry
|
||||
{
|
||||
Kind = kind,
|
||||
Type = descriptor.Type,
|
||||
Format = descriptor.Format,
|
||||
MediaType = descriptor.MediaType,
|
||||
Sha256 = descriptor.Sha256,
|
||||
Size = descriptor.Size,
|
||||
View = descriptor.View,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user