up
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.SbomService.Models;
|
||||
|
||||
namespace StellaOps.SbomService.Services;
|
||||
|
||||
internal static class SbomContextAssembler
|
||||
{
|
||||
private const string Schema = "stellaops.sbom.context/1.0";
|
||||
|
||||
private static readonly JsonSerializerOptions HashSerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string> EmptyDictionary =
|
||||
ImmutableDictionary<string, string>.Empty;
|
||||
|
||||
private static readonly IReadOnlyList<SbomContextVersion> EmptyVersions =
|
||||
ImmutableArray<SbomContextVersion>.Empty;
|
||||
|
||||
private static readonly IReadOnlyList<SbomContextDependencyPath> EmptyPaths =
|
||||
ImmutableArray<SbomContextDependencyPath>.Empty;
|
||||
|
||||
public static SbomContextResponse Build(
|
||||
string artifactId,
|
||||
string? purl,
|
||||
DateTimeOffset generated,
|
||||
IReadOnlyList<SbomVersion> timeline,
|
||||
IReadOnlyList<SbomPath> paths,
|
||||
bool includeEnvironmentFlags,
|
||||
bool includeBlastRadius)
|
||||
{
|
||||
var versions = timeline.Count == 0 ? EmptyVersions : BuildVersions(timeline);
|
||||
var dependencyPaths = paths.Count == 0 ? EmptyPaths : BuildDependencyPaths(paths);
|
||||
var environmentFlags = includeEnvironmentFlags
|
||||
? BuildEnvironmentFlags(dependencyPaths)
|
||||
: EmptyDictionary;
|
||||
var blastRadius = includeBlastRadius
|
||||
? BuildBlastRadius(dependencyPaths)
|
||||
: null;
|
||||
var metadata = BuildMetadata(artifactId, generated, versions.Count, dependencyPaths.Count, environmentFlags.Count, blastRadius is not null);
|
||||
|
||||
var response = new SbomContextResponse(
|
||||
Schema,
|
||||
generated,
|
||||
artifactId,
|
||||
purl,
|
||||
versions,
|
||||
dependencyPaths,
|
||||
environmentFlags,
|
||||
blastRadius,
|
||||
metadata,
|
||||
Hash: string.Empty);
|
||||
|
||||
var hash = ComputeHash(response);
|
||||
return response with { Hash = hash };
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SbomContextVersion> BuildVersions(IReadOnlyList<SbomVersion> versions)
|
||||
{
|
||||
return versions
|
||||
.OrderByDescending(v => v.CreatedAt)
|
||||
.ThenBy(v => v.Version, StringComparer.Ordinal)
|
||||
.Select(v =>
|
||||
{
|
||||
var metadata = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
metadata["digest"] = v.Digest;
|
||||
metadata["source_bundle_hash"] = v.SourceBundleHash;
|
||||
if (!string.IsNullOrWhiteSpace(v.Provenance))
|
||||
{
|
||||
metadata["provenance"] = v.Provenance!;
|
||||
}
|
||||
|
||||
return new SbomContextVersion(
|
||||
v.Version,
|
||||
v.CreatedAt,
|
||||
v.CreatedAt,
|
||||
"observed",
|
||||
string.IsNullOrWhiteSpace(v.Provenance) ? "sbom" : v.Provenance!.Trim(),
|
||||
false,
|
||||
metadata.ToImmutable());
|
||||
})
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SbomContextDependencyPath> BuildDependencyPaths(IReadOnlyList<SbomPath> paths)
|
||||
{
|
||||
return paths
|
||||
.Select(path =>
|
||||
{
|
||||
var nodes = path.Nodes
|
||||
.Select(node => new SbomContextDependencyNode(
|
||||
Identifier: string.IsNullOrWhiteSpace(node.Name) ? "unknown" : node.Name,
|
||||
Version: null))
|
||||
.ToImmutableArray();
|
||||
|
||||
var metadata = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
if (!string.IsNullOrWhiteSpace(path.Scope))
|
||||
{
|
||||
metadata["scope"] = path.Scope!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path.Environment))
|
||||
{
|
||||
metadata["environment"] = path.Environment!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path.Artifact))
|
||||
{
|
||||
metadata["artifact"] = path.Artifact!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path.NearestSafeVersion))
|
||||
{
|
||||
metadata["nearest_safe_version"] = path.NearestSafeVersion!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path.BlastRadius))
|
||||
{
|
||||
metadata["blast_radius"] = path.BlastRadius!;
|
||||
}
|
||||
|
||||
metadata["path_length"] = nodes.Length.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
return new SbomContextDependencyPath(
|
||||
nodes,
|
||||
path.RuntimeFlag,
|
||||
"sbom.paths",
|
||||
metadata.ToImmutable());
|
||||
})
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> BuildEnvironmentFlags(IReadOnlyList<SbomContextDependencyPath> paths)
|
||||
{
|
||||
if (paths.Count == 0)
|
||||
{
|
||||
return EmptyDictionary;
|
||||
}
|
||||
|
||||
var builder = ImmutableSortedDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
var environmentCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (path.Metadata.TryGetValue("environment", out var environment) && !string.IsNullOrWhiteSpace(environment))
|
||||
{
|
||||
var key = environment.Trim();
|
||||
environmentCounts[key] = environmentCounts.TryGetValue(key, out var count) ? count + 1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (environmentCounts.Count == 0)
|
||||
{
|
||||
return EmptyDictionary;
|
||||
}
|
||||
|
||||
foreach (var pair in environmentCounts.OrderBy(p => p.Key, StringComparer.Ordinal))
|
||||
{
|
||||
builder[pair.Key] = pair.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static SbomContextBlastRadius? BuildBlastRadius(IReadOnlyList<SbomContextDependencyPath> paths)
|
||||
{
|
||||
if (paths.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var impactedAssets = paths
|
||||
.SelectMany(p => p.Metadata.TryGetValue("scope", out var scope) && !string.IsNullOrWhiteSpace(scope)
|
||||
? new[] { scope.Trim() }
|
||||
: Array.Empty<string>())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Count();
|
||||
|
||||
var impactedNamespaces = paths
|
||||
.SelectMany(p => p.Metadata.TryGetValue("environment", out var environment) && !string.IsNullOrWhiteSpace(environment)
|
||||
? new[] { environment.Trim() }
|
||||
: Array.Empty<string>())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Count();
|
||||
|
||||
var impactedWorkloads = paths.Count(p => p.IsRuntime);
|
||||
double? impactedPercentage = paths.Count == 0
|
||||
? null
|
||||
: Math.Round((double)impactedWorkloads / paths.Count, 3);
|
||||
|
||||
var metadataBuilder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
metadataBuilder["path_sample_count"] = paths.Count.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
var blastTags = paths
|
||||
.Select(p => p.Metadata.TryGetValue("blast_radius", out var tag) ? tag : null)
|
||||
.Where(tag => !string.IsNullOrWhiteSpace(tag))
|
||||
.Select(tag => tag!.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
if (blastTags.Length > 0)
|
||||
{
|
||||
metadataBuilder["blast_radius_tags"] = string.Join(",", blastTags);
|
||||
}
|
||||
|
||||
return new SbomContextBlastRadius(
|
||||
impactedAssets,
|
||||
impactedWorkloads,
|
||||
impactedNamespaces,
|
||||
impactedPercentage,
|
||||
metadataBuilder.ToImmutable());
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> BuildMetadata(
|
||||
string artifactId,
|
||||
DateTimeOffset generated,
|
||||
int versionCount,
|
||||
int dependencyCount,
|
||||
int environmentFlagCount,
|
||||
bool hasBlastRadius)
|
||||
{
|
||||
var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
builder["generated_at"] = generated.ToString("O", CultureInfo.InvariantCulture);
|
||||
builder["artifact"] = artifactId;
|
||||
builder["version_count"] = versionCount.ToString(CultureInfo.InvariantCulture);
|
||||
builder["dependency_count"] = dependencyCount.ToString(CultureInfo.InvariantCulture);
|
||||
builder["environment_flag_count"] = environmentFlagCount.ToString(CultureInfo.InvariantCulture);
|
||||
builder["blast_radius_present"] = hasBlastRadius.ToString();
|
||||
builder["source"] = "sbom-service";
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static string ComputeHash(SbomContextResponse response)
|
||||
{
|
||||
var snapshot = new
|
||||
{
|
||||
response.Schema,
|
||||
response.Generated,
|
||||
response.ArtifactId,
|
||||
response.Purl,
|
||||
response.Versions,
|
||||
response.DependencyPaths,
|
||||
response.EnvironmentFlags,
|
||||
response.BlastRadius,
|
||||
response.Metadata
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(snapshot, HashSerializerOptions);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user