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,109 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Diff;
|
||||
|
||||
public enum ComponentChangeKind
|
||||
{
|
||||
Added,
|
||||
Removed,
|
||||
VersionChanged,
|
||||
MetadataChanged,
|
||||
}
|
||||
|
||||
public sealed record ComponentDiffRequest
|
||||
{
|
||||
public required ComponentGraph OldGraph { get; init; }
|
||||
|
||||
public required ComponentGraph NewGraph { get; init; }
|
||||
|
||||
public SbomView View { get; init; } = SbomView.Inventory;
|
||||
|
||||
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public string? OldImageDigest { get; init; }
|
||||
= null;
|
||||
|
||||
public string? NewImageDigest { get; init; }
|
||||
= null;
|
||||
}
|
||||
|
||||
public sealed record ComponentChange
|
||||
{
|
||||
[JsonPropertyName("kind")]
|
||||
public ComponentChangeKind Kind { get; init; }
|
||||
|
||||
[JsonPropertyName("componentKey")]
|
||||
public string ComponentKey { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("introducingLayer")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? IntroducingLayer { get; init; }
|
||||
= null;
|
||||
|
||||
[JsonPropertyName("removingLayer")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? RemovingLayer { get; init; }
|
||||
= null;
|
||||
|
||||
[JsonPropertyName("oldComponent")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public AggregatedComponent? OldComponent { get; init; }
|
||||
= null;
|
||||
|
||||
[JsonPropertyName("newComponent")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public AggregatedComponent? NewComponent { get; init; }
|
||||
= null;
|
||||
}
|
||||
|
||||
public sealed record LayerDiff
|
||||
{
|
||||
[JsonPropertyName("layerDigest")]
|
||||
public string LayerDigest { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("changes")]
|
||||
public ImmutableArray<ComponentChange> Changes { get; init; } = ImmutableArray<ComponentChange>.Empty;
|
||||
}
|
||||
|
||||
public sealed record DiffSummary
|
||||
{
|
||||
[JsonPropertyName("added")]
|
||||
public int Added { get; init; }
|
||||
|
||||
[JsonPropertyName("removed")]
|
||||
public int Removed { get; init; }
|
||||
|
||||
[JsonPropertyName("versionChanged")]
|
||||
public int VersionChanged { get; init; }
|
||||
|
||||
[JsonPropertyName("metadataChanged")]
|
||||
public int MetadataChanged { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ComponentDiffDocument
|
||||
{
|
||||
[JsonPropertyName("generatedAt")]
|
||||
public DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("view")]
|
||||
public SbomView View { get; init; }
|
||||
|
||||
[JsonPropertyName("oldImageDigest")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? OldImageDigest { get; init; }
|
||||
= null;
|
||||
|
||||
[JsonPropertyName("newImageDigest")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? NewImageDigest { get; init; }
|
||||
= null;
|
||||
|
||||
[JsonPropertyName("summary")]
|
||||
public DiffSummary Summary { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("layers")]
|
||||
public ImmutableArray<LayerDiff> Layers { get; init; } = ImmutableArray<LayerDiff>.Empty;
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Diff;
|
||||
|
||||
public enum ComponentChangeKind
|
||||
{
|
||||
Added,
|
||||
Removed,
|
||||
VersionChanged,
|
||||
MetadataChanged,
|
||||
}
|
||||
|
||||
public sealed record ComponentDiffRequest
|
||||
{
|
||||
public required ComponentGraph OldGraph { get; init; }
|
||||
|
||||
public required ComponentGraph NewGraph { get; init; }
|
||||
|
||||
public SbomView View { get; init; } = SbomView.Inventory;
|
||||
|
||||
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public string? OldImageDigest { get; init; }
|
||||
= null;
|
||||
|
||||
public string? NewImageDigest { get; init; }
|
||||
= null;
|
||||
}
|
||||
|
||||
public sealed record ComponentChange
|
||||
{
|
||||
[JsonPropertyName("kind")]
|
||||
public ComponentChangeKind Kind { get; init; }
|
||||
|
||||
[JsonPropertyName("componentKey")]
|
||||
public string ComponentKey { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("introducingLayer")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? IntroducingLayer { get; init; }
|
||||
= null;
|
||||
|
||||
[JsonPropertyName("removingLayer")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? RemovingLayer { get; init; }
|
||||
= null;
|
||||
|
||||
[JsonPropertyName("oldComponent")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public AggregatedComponent? OldComponent { get; init; }
|
||||
= null;
|
||||
|
||||
[JsonPropertyName("newComponent")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public AggregatedComponent? NewComponent { get; init; }
|
||||
= null;
|
||||
}
|
||||
|
||||
public sealed record LayerDiff
|
||||
{
|
||||
[JsonPropertyName("layerDigest")]
|
||||
public string LayerDigest { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("changes")]
|
||||
public ImmutableArray<ComponentChange> Changes { get; init; } = ImmutableArray<ComponentChange>.Empty;
|
||||
}
|
||||
|
||||
public sealed record DiffSummary
|
||||
{
|
||||
[JsonPropertyName("added")]
|
||||
public int Added { get; init; }
|
||||
|
||||
[JsonPropertyName("removed")]
|
||||
public int Removed { get; init; }
|
||||
|
||||
[JsonPropertyName("versionChanged")]
|
||||
public int VersionChanged { get; init; }
|
||||
|
||||
[JsonPropertyName("metadataChanged")]
|
||||
public int MetadataChanged { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ComponentDiffDocument
|
||||
{
|
||||
[JsonPropertyName("generatedAt")]
|
||||
public DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("view")]
|
||||
public SbomView View { get; init; }
|
||||
|
||||
[JsonPropertyName("oldImageDigest")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? OldImageDigest { get; init; }
|
||||
= null;
|
||||
|
||||
[JsonPropertyName("newImageDigest")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? NewImageDigest { get; init; }
|
||||
= null;
|
||||
|
||||
[JsonPropertyName("summary")]
|
||||
public DiffSummary Summary { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("layers")]
|
||||
public ImmutableArray<LayerDiff> Layers { get; init; } = ImmutableArray<LayerDiff>.Empty;
|
||||
}
|
||||
|
||||
@@ -1,204 +1,204 @@
|
||||
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.Diff;
|
||||
|
||||
public sealed class ComponentDiffer
|
||||
{
|
||||
private static readonly StringComparer Ordinal = StringComparer.Ordinal;
|
||||
private const string UnknownLayerKey = "(unknown)";
|
||||
|
||||
public ComponentDiffDocument Compute(ComponentDiffRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var generatedAt = ScannerTimestamps.Normalize(request.GeneratedAt);
|
||||
var oldComponents = ToDictionary(FilterComponents(request.OldGraph, request.View));
|
||||
var newComponents = ToDictionary(FilterComponents(request.NewGraph, request.View));
|
||||
var layerOrder = BuildLayerOrder(request.OldGraph, request.NewGraph);
|
||||
|
||||
var changes = new List<ComponentChange>();
|
||||
var counters = new DiffCounters();
|
||||
|
||||
foreach (var (key, newComponent) in newComponents)
|
||||
{
|
||||
if (!oldComponents.TryGetValue(key, out var oldComponent))
|
||||
{
|
||||
changes.Add(new ComponentChange
|
||||
{
|
||||
Kind = ComponentChangeKind.Added,
|
||||
ComponentKey = key,
|
||||
IntroducingLayer = GetIntroducingLayer(newComponent),
|
||||
NewComponent = newComponent,
|
||||
});
|
||||
counters.Added++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var change = CompareComponents(oldComponent, newComponent, key);
|
||||
if (change is not null)
|
||||
{
|
||||
changes.Add(change);
|
||||
counters.Register(change.Kind);
|
||||
}
|
||||
|
||||
oldComponents.Remove(key);
|
||||
}
|
||||
|
||||
foreach (var (key, oldComponent) in oldComponents)
|
||||
{
|
||||
changes.Add(new ComponentChange
|
||||
{
|
||||
Kind = ComponentChangeKind.Removed,
|
||||
ComponentKey = key,
|
||||
RemovingLayer = GetRemovingLayer(oldComponent),
|
||||
OldComponent = oldComponent,
|
||||
});
|
||||
counters.Removed++;
|
||||
}
|
||||
|
||||
var layerGroups = changes
|
||||
.GroupBy(ResolveLayerKey, Ordinal)
|
||||
.OrderBy(group => layerOrder.TryGetValue(group.Key, out var position) ? position : int.MaxValue)
|
||||
.ThenBy(static group => group.Key, Ordinal)
|
||||
.Select(group => new LayerDiff
|
||||
{
|
||||
LayerDigest = group.Key,
|
||||
Changes = group
|
||||
.OrderBy(change => change.ComponentKey, Ordinal)
|
||||
.ThenBy(change => change.Kind)
|
||||
.ThenBy(change => change.NewComponent?.Identity.Version ?? change.OldComponent?.Identity.Version ?? string.Empty, Ordinal)
|
||||
.ToImmutableArray(),
|
||||
})
|
||||
.ToImmutableArray();
|
||||
|
||||
var document = new ComponentDiffDocument
|
||||
{
|
||||
GeneratedAt = generatedAt,
|
||||
View = request.View,
|
||||
OldImageDigest = request.OldImageDigest,
|
||||
NewImageDigest = request.NewImageDigest,
|
||||
Summary = counters.ToSummary(),
|
||||
Layers = layerGroups,
|
||||
};
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
private static ComponentChange? CompareComponents(AggregatedComponent oldComponent, AggregatedComponent newComponent, string key)
|
||||
{
|
||||
var versionChanged = !string.Equals(oldComponent.Identity.Version, newComponent.Identity.Version, StringComparison.Ordinal);
|
||||
if (versionChanged)
|
||||
{
|
||||
return new ComponentChange
|
||||
{
|
||||
Kind = ComponentChangeKind.VersionChanged,
|
||||
ComponentKey = key,
|
||||
IntroducingLayer = GetIntroducingLayer(newComponent),
|
||||
RemovingLayer = GetRemovingLayer(oldComponent),
|
||||
OldComponent = oldComponent,
|
||||
NewComponent = newComponent,
|
||||
};
|
||||
}
|
||||
|
||||
var metadataChanged = HasMetadataChanged(oldComponent, newComponent);
|
||||
if (!metadataChanged)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ComponentChange
|
||||
{
|
||||
Kind = ComponentChangeKind.MetadataChanged,
|
||||
ComponentKey = key,
|
||||
IntroducingLayer = GetIntroducingLayer(newComponent),
|
||||
RemovingLayer = GetRemovingLayer(oldComponent),
|
||||
OldComponent = oldComponent,
|
||||
NewComponent = newComponent,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasMetadataChanged(AggregatedComponent oldComponent, AggregatedComponent newComponent)
|
||||
{
|
||||
if (!string.Equals(oldComponent.Identity.Name, newComponent.Identity.Name, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.Equals(oldComponent.Identity.ComponentType, newComponent.Identity.ComponentType, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.Equals(oldComponent.Identity.Group, newComponent.Identity.Group, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.Equals(oldComponent.Identity.Purl, newComponent.Identity.Purl, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!oldComponent.Dependencies.SequenceEqual(newComponent.Dependencies, Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!oldComponent.LayerDigests.SequenceEqual(newComponent.LayerDigests, Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!oldComponent.Evidence.SequenceEqual(newComponent.Evidence))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (UsageChanged(oldComponent.Usage, newComponent.Usage))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!MetadataEquals(oldComponent.Metadata, newComponent.Metadata))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool UsageChanged(ComponentUsage oldUsage, ComponentUsage newUsage)
|
||||
{
|
||||
if (oldUsage.UsedByEntrypoint != newUsage.UsedByEntrypoint)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !oldUsage.Entrypoints.SequenceEqual(newUsage.Entrypoints, Ordinal);
|
||||
}
|
||||
|
||||
private static bool MetadataEquals(ComponentMetadata? left, ComponentMetadata? right)
|
||||
{
|
||||
if (left is null && right is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left is null || right is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(left.Scope, right.Scope, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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.Diff;
|
||||
|
||||
public sealed class ComponentDiffer
|
||||
{
|
||||
private static readonly StringComparer Ordinal = StringComparer.Ordinal;
|
||||
private const string UnknownLayerKey = "(unknown)";
|
||||
|
||||
public ComponentDiffDocument Compute(ComponentDiffRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var generatedAt = ScannerTimestamps.Normalize(request.GeneratedAt);
|
||||
var oldComponents = ToDictionary(FilterComponents(request.OldGraph, request.View));
|
||||
var newComponents = ToDictionary(FilterComponents(request.NewGraph, request.View));
|
||||
var layerOrder = BuildLayerOrder(request.OldGraph, request.NewGraph);
|
||||
|
||||
var changes = new List<ComponentChange>();
|
||||
var counters = new DiffCounters();
|
||||
|
||||
foreach (var (key, newComponent) in newComponents)
|
||||
{
|
||||
if (!oldComponents.TryGetValue(key, out var oldComponent))
|
||||
{
|
||||
changes.Add(new ComponentChange
|
||||
{
|
||||
Kind = ComponentChangeKind.Added,
|
||||
ComponentKey = key,
|
||||
IntroducingLayer = GetIntroducingLayer(newComponent),
|
||||
NewComponent = newComponent,
|
||||
});
|
||||
counters.Added++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var change = CompareComponents(oldComponent, newComponent, key);
|
||||
if (change is not null)
|
||||
{
|
||||
changes.Add(change);
|
||||
counters.Register(change.Kind);
|
||||
}
|
||||
|
||||
oldComponents.Remove(key);
|
||||
}
|
||||
|
||||
foreach (var (key, oldComponent) in oldComponents)
|
||||
{
|
||||
changes.Add(new ComponentChange
|
||||
{
|
||||
Kind = ComponentChangeKind.Removed,
|
||||
ComponentKey = key,
|
||||
RemovingLayer = GetRemovingLayer(oldComponent),
|
||||
OldComponent = oldComponent,
|
||||
});
|
||||
counters.Removed++;
|
||||
}
|
||||
|
||||
var layerGroups = changes
|
||||
.GroupBy(ResolveLayerKey, Ordinal)
|
||||
.OrderBy(group => layerOrder.TryGetValue(group.Key, out var position) ? position : int.MaxValue)
|
||||
.ThenBy(static group => group.Key, Ordinal)
|
||||
.Select(group => new LayerDiff
|
||||
{
|
||||
LayerDigest = group.Key,
|
||||
Changes = group
|
||||
.OrderBy(change => change.ComponentKey, Ordinal)
|
||||
.ThenBy(change => change.Kind)
|
||||
.ThenBy(change => change.NewComponent?.Identity.Version ?? change.OldComponent?.Identity.Version ?? string.Empty, Ordinal)
|
||||
.ToImmutableArray(),
|
||||
})
|
||||
.ToImmutableArray();
|
||||
|
||||
var document = new ComponentDiffDocument
|
||||
{
|
||||
GeneratedAt = generatedAt,
|
||||
View = request.View,
|
||||
OldImageDigest = request.OldImageDigest,
|
||||
NewImageDigest = request.NewImageDigest,
|
||||
Summary = counters.ToSummary(),
|
||||
Layers = layerGroups,
|
||||
};
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
private static ComponentChange? CompareComponents(AggregatedComponent oldComponent, AggregatedComponent newComponent, string key)
|
||||
{
|
||||
var versionChanged = !string.Equals(oldComponent.Identity.Version, newComponent.Identity.Version, StringComparison.Ordinal);
|
||||
if (versionChanged)
|
||||
{
|
||||
return new ComponentChange
|
||||
{
|
||||
Kind = ComponentChangeKind.VersionChanged,
|
||||
ComponentKey = key,
|
||||
IntroducingLayer = GetIntroducingLayer(newComponent),
|
||||
RemovingLayer = GetRemovingLayer(oldComponent),
|
||||
OldComponent = oldComponent,
|
||||
NewComponent = newComponent,
|
||||
};
|
||||
}
|
||||
|
||||
var metadataChanged = HasMetadataChanged(oldComponent, newComponent);
|
||||
if (!metadataChanged)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ComponentChange
|
||||
{
|
||||
Kind = ComponentChangeKind.MetadataChanged,
|
||||
ComponentKey = key,
|
||||
IntroducingLayer = GetIntroducingLayer(newComponent),
|
||||
RemovingLayer = GetRemovingLayer(oldComponent),
|
||||
OldComponent = oldComponent,
|
||||
NewComponent = newComponent,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasMetadataChanged(AggregatedComponent oldComponent, AggregatedComponent newComponent)
|
||||
{
|
||||
if (!string.Equals(oldComponent.Identity.Name, newComponent.Identity.Name, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.Equals(oldComponent.Identity.ComponentType, newComponent.Identity.ComponentType, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.Equals(oldComponent.Identity.Group, newComponent.Identity.Group, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.Equals(oldComponent.Identity.Purl, newComponent.Identity.Purl, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!oldComponent.Dependencies.SequenceEqual(newComponent.Dependencies, Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!oldComponent.LayerDigests.SequenceEqual(newComponent.LayerDigests, Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!oldComponent.Evidence.SequenceEqual(newComponent.Evidence))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (UsageChanged(oldComponent.Usage, newComponent.Usage))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!MetadataEquals(oldComponent.Metadata, newComponent.Metadata))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool UsageChanged(ComponentUsage oldUsage, ComponentUsage newUsage)
|
||||
{
|
||||
if (oldUsage.UsedByEntrypoint != newUsage.UsedByEntrypoint)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !oldUsage.Entrypoints.SequenceEqual(newUsage.Entrypoints, Ordinal);
|
||||
}
|
||||
|
||||
private static bool MetadataEquals(ComponentMetadata? left, ComponentMetadata? right)
|
||||
{
|
||||
if (left is null && right is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left is null || right is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(left.Scope, right.Scope, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SequenceEqual(left.Licenses, right.Licenses))
|
||||
{
|
||||
return false;
|
||||
@@ -214,67 +214,67 @@ public sealed class ComponentDiffer
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool SequenceEqual(IReadOnlyList<string>? left, IReadOnlyList<string>? right)
|
||||
{
|
||||
if (left is null && right is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left is null || right is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (left.Count != right.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < left.Count; i++)
|
||||
{
|
||||
if (!string.Equals(left[i], right[i], StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DictionaryEqual(IReadOnlyDictionary<string, string>? left, IReadOnlyDictionary<string, string>? right)
|
||||
{
|
||||
if (left is null && right is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left is null || right is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (left.Count != right.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var (key, value) in left)
|
||||
{
|
||||
if (!right.TryGetValue(key, out var rightValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(value, rightValue, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool SequenceEqual(IReadOnlyList<string>? left, IReadOnlyList<string>? right)
|
||||
{
|
||||
if (left is null && right is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left is null || right is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (left.Count != right.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < left.Count; i++)
|
||||
{
|
||||
if (!string.Equals(left[i], right[i], StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DictionaryEqual(IReadOnlyDictionary<string, string>? left, IReadOnlyDictionary<string, string>? right)
|
||||
{
|
||||
if (left is null && right is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left is null || right is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (left.Count != right.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var (key, value) in left)
|
||||
{
|
||||
if (!right.TryGetValue(key, out var rightValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(value, rightValue, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -299,100 +299,100 @@ public sealed class ComponentDiffer
|
||||
var dictionary = new Dictionary<string, AggregatedComponent>(components.Length, Ordinal);
|
||||
foreach (var component in components)
|
||||
{
|
||||
dictionary[component.Identity.Key] = component;
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private static ImmutableArray<AggregatedComponent> FilterComponents(ComponentGraph graph, SbomView view)
|
||||
{
|
||||
if (view == SbomView.Usage)
|
||||
{
|
||||
return graph.Components.Where(static component => component.Usage.UsedByEntrypoint).ToImmutableArray();
|
||||
}
|
||||
|
||||
return graph.Components;
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> BuildLayerOrder(ComponentGraph oldGraph, ComponentGraph newGraph)
|
||||
{
|
||||
var order = new Dictionary<string, int>(Ordinal);
|
||||
var index = 0;
|
||||
|
||||
foreach (var layer in newGraph.Layers)
|
||||
{
|
||||
AddLayer(order, layer.LayerDigest, ref index);
|
||||
}
|
||||
|
||||
foreach (var layer in oldGraph.Layers)
|
||||
{
|
||||
AddLayer(order, layer.LayerDigest, ref index);
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
private static void AddLayer(IDictionary<string, int> order, string? layerDigest, ref int index)
|
||||
{
|
||||
var normalized = NormalizeLayer(layerDigest);
|
||||
if (normalized is null || order.ContainsKey(normalized))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
order[normalized] = index++;
|
||||
}
|
||||
|
||||
private static string ResolveLayerKey(ComponentChange change)
|
||||
=> NormalizeLayer(change.IntroducingLayer) ?? NormalizeLayer(change.RemovingLayer) ?? UnknownLayerKey;
|
||||
|
||||
private static string? GetIntroducingLayer(AggregatedComponent component)
|
||||
=> NormalizeLayer(component.FirstLayerDigest);
|
||||
|
||||
private static string? GetRemovingLayer(AggregatedComponent component)
|
||||
{
|
||||
var layer = component.LastLayerDigest ?? component.FirstLayerDigest;
|
||||
return NormalizeLayer(layer);
|
||||
}
|
||||
|
||||
private static string? NormalizeLayer(string? layer)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layer))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
private sealed class DiffCounters
|
||||
{
|
||||
public int Added;
|
||||
public int Removed;
|
||||
public int VersionChanged;
|
||||
public int MetadataChanged;
|
||||
|
||||
public void Register(ComponentChangeKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case ComponentChangeKind.VersionChanged:
|
||||
VersionChanged++;
|
||||
break;
|
||||
case ComponentChangeKind.MetadataChanged:
|
||||
MetadataChanged++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public DiffSummary ToSummary()
|
||||
=> new()
|
||||
{
|
||||
Added = Added,
|
||||
Removed = Removed,
|
||||
VersionChanged = VersionChanged,
|
||||
MetadataChanged = MetadataChanged,
|
||||
};
|
||||
}
|
||||
}
|
||||
dictionary[component.Identity.Key] = component;
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private static ImmutableArray<AggregatedComponent> FilterComponents(ComponentGraph graph, SbomView view)
|
||||
{
|
||||
if (view == SbomView.Usage)
|
||||
{
|
||||
return graph.Components.Where(static component => component.Usage.UsedByEntrypoint).ToImmutableArray();
|
||||
}
|
||||
|
||||
return graph.Components;
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> BuildLayerOrder(ComponentGraph oldGraph, ComponentGraph newGraph)
|
||||
{
|
||||
var order = new Dictionary<string, int>(Ordinal);
|
||||
var index = 0;
|
||||
|
||||
foreach (var layer in newGraph.Layers)
|
||||
{
|
||||
AddLayer(order, layer.LayerDigest, ref index);
|
||||
}
|
||||
|
||||
foreach (var layer in oldGraph.Layers)
|
||||
{
|
||||
AddLayer(order, layer.LayerDigest, ref index);
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
private static void AddLayer(IDictionary<string, int> order, string? layerDigest, ref int index)
|
||||
{
|
||||
var normalized = NormalizeLayer(layerDigest);
|
||||
if (normalized is null || order.ContainsKey(normalized))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
order[normalized] = index++;
|
||||
}
|
||||
|
||||
private static string ResolveLayerKey(ComponentChange change)
|
||||
=> NormalizeLayer(change.IntroducingLayer) ?? NormalizeLayer(change.RemovingLayer) ?? UnknownLayerKey;
|
||||
|
||||
private static string? GetIntroducingLayer(AggregatedComponent component)
|
||||
=> NormalizeLayer(component.FirstLayerDigest);
|
||||
|
||||
private static string? GetRemovingLayer(AggregatedComponent component)
|
||||
{
|
||||
var layer = component.LastLayerDigest ?? component.FirstLayerDigest;
|
||||
return NormalizeLayer(layer);
|
||||
}
|
||||
|
||||
private static string? NormalizeLayer(string? layer)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layer))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
private sealed class DiffCounters
|
||||
{
|
||||
public int Added;
|
||||
public int Removed;
|
||||
public int VersionChanged;
|
||||
public int MetadataChanged;
|
||||
|
||||
public void Register(ComponentChangeKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case ComponentChangeKind.VersionChanged:
|
||||
VersionChanged++;
|
||||
break;
|
||||
case ComponentChangeKind.MetadataChanged:
|
||||
MetadataChanged++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public DiffSummary ToSummary()
|
||||
=> new()
|
||||
{
|
||||
Added = Added,
|
||||
Removed = Removed,
|
||||
VersionChanged = VersionChanged,
|
||||
MetadataChanged = MetadataChanged,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Core.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Diff;
|
||||
|
||||
public static class DiffJsonSerializer
|
||||
{
|
||||
public static string Serialize(ComponentDiffDocument document)
|
||||
=> JsonSerializer.Serialize(document, ScannerJsonOptions.Default);
|
||||
}
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Core.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Diff;
|
||||
|
||||
public static class DiffJsonSerializer
|
||||
{
|
||||
public static string Serialize(ComponentDiffDocument document)
|
||||
=> JsonSerializer.Serialize(document, ScannerJsonOptions.Default);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user