feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Lineage;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for content-addressable SBOM storage with lineage tracking.
|
||||
/// </summary>
|
||||
public interface ISbomStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores an SBOM with optional parent reference.
|
||||
/// </summary>
|
||||
/// <param name="sbomContent">The canonical SBOM content.</param>
|
||||
/// <param name="imageDigest">The image digest.</param>
|
||||
/// <param name="parentId">Optional parent SBOM ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The assigned SBOM ID.</returns>
|
||||
Task<SbomId> StoreAsync(
|
||||
string sbomContent,
|
||||
string imageDigest,
|
||||
SbomId? parentId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an SBOM by its content hash.
|
||||
/// </summary>
|
||||
Task<SbomLineage?> GetByHashAsync(string contentHash, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an SBOM by its ID.
|
||||
/// </summary>
|
||||
Task<SbomLineage?> GetByIdAsync(SbomId id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lineage chain for an SBOM.
|
||||
/// </summary>
|
||||
Task<ImmutableArray<SbomLineage>> GetLineageAsync(SbomId id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the diff between two SBOMs.
|
||||
/// </summary>
|
||||
Task<SbomDiff?> GetDiffAsync(SbomId fromId, SbomId toId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all SBOM versions for an image.
|
||||
/// </summary>
|
||||
Task<ImmutableArray<SbomLineage>> GetByImageDigestAsync(string imageDigest, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for SBOM lineage traversal.
|
||||
/// </summary>
|
||||
public static class SbomLineageExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the full ancestor chain as a list.
|
||||
/// </summary>
|
||||
public static async Task<IReadOnlyList<SbomLineage>> GetFullAncestryAsync(
|
||||
this ISbomStore store,
|
||||
SbomId id,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var ancestry = new List<SbomLineage>();
|
||||
var current = await store.GetByIdAsync(id, ct);
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
ancestry.Add(current);
|
||||
|
||||
if (current.ParentId is null)
|
||||
break;
|
||||
|
||||
current = await store.GetByIdAsync(current.ParentId.Value, ct);
|
||||
}
|
||||
|
||||
return ancestry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the common ancestor of two SBOM versions.
|
||||
/// </summary>
|
||||
public static async Task<SbomId?> FindCommonAncestorAsync(
|
||||
this ISbomStore store,
|
||||
SbomId id1,
|
||||
SbomId id2,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var lineage1 = await store.GetLineageAsync(id1, ct);
|
||||
var lineage2 = await store.GetLineageAsync(id2, ct);
|
||||
|
||||
var ancestors1 = lineage1.Select(l => l.Id).ToHashSet();
|
||||
|
||||
foreach (var ancestor in lineage2)
|
||||
{
|
||||
if (ancestors1.Contains(ancestor.Id))
|
||||
return ancestor.Id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Lineage;
|
||||
|
||||
/// <summary>
|
||||
/// Proof manifest that enables reproducible SBOM generation.
|
||||
/// </summary>
|
||||
public sealed record RebuildProof
|
||||
{
|
||||
/// <summary>
|
||||
/// The SBOM ID this proof applies to.
|
||||
/// </summary>
|
||||
public required SbomId SbomId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The image digest that was scanned.
|
||||
/// </summary>
|
||||
public required string ImageDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of Stella Ops used for the scan.
|
||||
/// </summary>
|
||||
public required string StellaOpsVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Snapshots of all feeds used during the scan.
|
||||
/// </summary>
|
||||
public required ImmutableArray<FeedSnapshot> FeedSnapshots { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Versions of all analyzers used during the scan.
|
||||
/// </summary>
|
||||
public required ImmutableArray<AnalyzerVersion> AnalyzerVersions { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the policy configuration used.
|
||||
/// </summary>
|
||||
public required string PolicyHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the proof was generated.
|
||||
/// </summary>
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE signature of the proof (optional).
|
||||
/// </summary>
|
||||
public string? DsseSignature { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the entire proof document.
|
||||
/// </summary>
|
||||
public string? ProofHash { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of a vulnerability/advisory feed at a point in time.
|
||||
/// </summary>
|
||||
public sealed record FeedSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique feed identifier.
|
||||
/// </summary>
|
||||
public required string FeedId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Feed name/description.
|
||||
/// </summary>
|
||||
public required string FeedName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the feed content at snapshot time.
|
||||
/// </summary>
|
||||
public required string SnapshotHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the snapshot was taken.
|
||||
/// </summary>
|
||||
public required DateTimeOffset AsOf { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of entries in the feed.
|
||||
/// </summary>
|
||||
public int? EntryCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Feed version/revision if available.
|
||||
/// </summary>
|
||||
public string? FeedVersion { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Version of an analyzer used during scanning.
|
||||
/// </summary>
|
||||
public sealed record AnalyzerVersion
|
||||
{
|
||||
/// <summary>
|
||||
/// Analyzer identifier.
|
||||
/// </summary>
|
||||
public required string AnalyzerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Analyzer name.
|
||||
/// </summary>
|
||||
public required string AnalyzerName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version string.
|
||||
/// </summary>
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of analyzer code/rules if available.
|
||||
/// </summary>
|
||||
public string? CodeHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Configuration hash if applicable.
|
||||
/// </summary>
|
||||
public string? ConfigHash { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a rebuild verification.
|
||||
/// </summary>
|
||||
public sealed record RebuildVerification
|
||||
{
|
||||
/// <summary>
|
||||
/// The proof that was verified.
|
||||
/// </summary>
|
||||
public required RebuildProof Proof { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the rebuild was successful.
|
||||
/// </summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The SBOM produced by the rebuild.
|
||||
/// </summary>
|
||||
public SbomId? RebuiltSbomId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the rebuilt SBOM matches the original.
|
||||
/// </summary>
|
||||
public bool? HashMatches { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Differences if the rebuild didn't match.
|
||||
/// </summary>
|
||||
public SbomDiff? Differences { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if rebuild failed.
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the verification was performed.
|
||||
/// </summary>
|
||||
public required DateTimeOffset VerifiedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Lineage;
|
||||
|
||||
/// <summary>
|
||||
/// Semantic diff between two SBOM versions.
|
||||
/// </summary>
|
||||
public sealed record SbomDiff
|
||||
{
|
||||
/// <summary>
|
||||
/// Source SBOM ID.
|
||||
/// </summary>
|
||||
public required SbomId FromId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target SBOM ID.
|
||||
/// </summary>
|
||||
public required SbomId ToId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Individual component-level changes.
|
||||
/// </summary>
|
||||
public required ImmutableArray<ComponentDelta> Deltas { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Summary of the diff.
|
||||
/// </summary>
|
||||
public required DiffSummary Summary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the diff was computed.
|
||||
/// </summary>
|
||||
public required DateTimeOffset ComputedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single component-level change.
|
||||
/// </summary>
|
||||
public sealed record ComponentDelta
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of change.
|
||||
/// </summary>
|
||||
public required ComponentDeltaType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The component reference before the change (null if added).
|
||||
/// </summary>
|
||||
public ComponentRef? Before { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The component reference after the change (null if removed).
|
||||
/// </summary>
|
||||
public ComponentRef? After { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// List of fields that changed (for modified components).
|
||||
/// </summary>
|
||||
public ImmutableArray<string> ChangedFields { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of component change.
|
||||
/// </summary>
|
||||
public enum ComponentDeltaType
|
||||
{
|
||||
/// <summary>
|
||||
/// Component was added.
|
||||
/// </summary>
|
||||
Added,
|
||||
|
||||
/// <summary>
|
||||
/// Component was removed.
|
||||
/// </summary>
|
||||
Removed,
|
||||
|
||||
/// <summary>
|
||||
/// Component version changed.
|
||||
/// </summary>
|
||||
VersionChanged,
|
||||
|
||||
/// <summary>
|
||||
/// Component license changed.
|
||||
/// </summary>
|
||||
LicenseChanged,
|
||||
|
||||
/// <summary>
|
||||
/// Component dependencies changed.
|
||||
/// </summary>
|
||||
DependencyChanged,
|
||||
|
||||
/// <summary>
|
||||
/// Other metadata changed.
|
||||
/// </summary>
|
||||
MetadataChanged
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to a component.
|
||||
/// </summary>
|
||||
public sealed record ComponentRef
|
||||
{
|
||||
/// <summary>
|
||||
/// Package URL (PURL).
|
||||
/// </summary>
|
||||
public required string Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Component name.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Component version.
|
||||
/// </summary>
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Component type/ecosystem.
|
||||
/// </summary>
|
||||
public string? Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// License expression (SPDX).
|
||||
/// </summary>
|
||||
public string? License { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary statistics for a diff.
|
||||
/// </summary>
|
||||
public sealed record DiffSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of components added.
|
||||
/// </summary>
|
||||
public required int Added { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of components removed.
|
||||
/// </summary>
|
||||
public required int Removed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of components with version changes.
|
||||
/// </summary>
|
||||
public required int VersionChanged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of components with other modifications.
|
||||
/// </summary>
|
||||
public required int OtherModified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of components unchanged.
|
||||
/// </summary>
|
||||
public required int Unchanged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total components in target SBOM.
|
||||
/// </summary>
|
||||
public int TotalComponents => Added + VersionChanged + OtherModified + Unchanged;
|
||||
|
||||
/// <summary>
|
||||
/// Is this a breaking change (any removals or version downgrades)?
|
||||
/// </summary>
|
||||
public bool IsBreaking { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Lineage;
|
||||
|
||||
/// <summary>
|
||||
/// Engine for computing semantic diffs between SBOM versions.
|
||||
/// </summary>
|
||||
public sealed class SbomDiffEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the semantic diff between two SBOMs.
|
||||
/// </summary>
|
||||
public SbomDiff ComputeDiff(
|
||||
SbomId fromId,
|
||||
IReadOnlyList<ComponentRef> fromComponents,
|
||||
SbomId toId,
|
||||
IReadOnlyList<ComponentRef> toComponents)
|
||||
{
|
||||
var fromByPurl = fromComponents.ToDictionary(c => c.Purl, c => c);
|
||||
var toByPurl = toComponents.ToDictionary(c => c.Purl, c => c);
|
||||
|
||||
var deltas = new List<ComponentDelta>();
|
||||
var added = 0;
|
||||
var removed = 0;
|
||||
var versionChanged = 0;
|
||||
var otherModified = 0;
|
||||
var unchanged = 0;
|
||||
var isBreaking = false;
|
||||
|
||||
// Find added and modified components
|
||||
foreach (var (purl, toComp) in toByPurl)
|
||||
{
|
||||
if (!fromByPurl.TryGetValue(purl, out var fromComp))
|
||||
{
|
||||
// Added
|
||||
deltas.Add(new ComponentDelta
|
||||
{
|
||||
Type = ComponentDeltaType.Added,
|
||||
After = toComp
|
||||
});
|
||||
added++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Possibly modified
|
||||
var changedFields = CompareComponents(fromComp, toComp);
|
||||
if (changedFields.Length > 0)
|
||||
{
|
||||
var deltaType = changedFields.Contains("Version")
|
||||
? ComponentDeltaType.VersionChanged
|
||||
: changedFields.Contains("License")
|
||||
? ComponentDeltaType.LicenseChanged
|
||||
: ComponentDeltaType.MetadataChanged;
|
||||
|
||||
deltas.Add(new ComponentDelta
|
||||
{
|
||||
Type = deltaType,
|
||||
Before = fromComp,
|
||||
After = toComp,
|
||||
ChangedFields = changedFields
|
||||
});
|
||||
|
||||
if (deltaType == ComponentDeltaType.VersionChanged)
|
||||
versionChanged++;
|
||||
else
|
||||
otherModified++;
|
||||
|
||||
// Check for version downgrade (breaking)
|
||||
if (changedFields.Contains("Version") && IsVersionDowngrade(fromComp.Version, toComp.Version))
|
||||
isBreaking = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
unchanged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find removed components
|
||||
foreach (var (purl, fromComp) in fromByPurl)
|
||||
{
|
||||
if (!toByPurl.ContainsKey(purl))
|
||||
{
|
||||
deltas.Add(new ComponentDelta
|
||||
{
|
||||
Type = ComponentDeltaType.Removed,
|
||||
Before = fromComp
|
||||
});
|
||||
removed++;
|
||||
isBreaking = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort deltas for determinism
|
||||
var sortedDeltas = deltas
|
||||
.OrderBy(d => d.Type)
|
||||
.ThenBy(d => d.Before?.Purl ?? d.After?.Purl)
|
||||
.ToImmutableArray();
|
||||
|
||||
return new SbomDiff
|
||||
{
|
||||
FromId = fromId,
|
||||
ToId = toId,
|
||||
Deltas = sortedDeltas,
|
||||
Summary = new DiffSummary
|
||||
{
|
||||
Added = added,
|
||||
Removed = removed,
|
||||
VersionChanged = versionChanged,
|
||||
OtherModified = otherModified,
|
||||
Unchanged = unchanged,
|
||||
IsBreaking = isBreaking
|
||||
},
|
||||
ComputedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a diff pointer from a diff.
|
||||
/// </summary>
|
||||
public SbomDiffPointer CreatePointer(SbomDiff diff)
|
||||
{
|
||||
var hash = ComputeDiffHash(diff);
|
||||
|
||||
return new SbomDiffPointer
|
||||
{
|
||||
ComponentsAdded = diff.Summary.Added,
|
||||
ComponentsRemoved = diff.Summary.Removed,
|
||||
ComponentsModified = diff.Summary.VersionChanged + diff.Summary.OtherModified,
|
||||
DiffHash = hash
|
||||
};
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> CompareComponents(ComponentRef from, ComponentRef to)
|
||||
{
|
||||
var changes = new List<string>();
|
||||
|
||||
if (from.Version != to.Version)
|
||||
changes.Add("Version");
|
||||
|
||||
if (from.License != to.License)
|
||||
changes.Add("License");
|
||||
|
||||
if (from.Type != to.Type)
|
||||
changes.Add("Type");
|
||||
|
||||
return [.. changes];
|
||||
}
|
||||
|
||||
private static bool IsVersionDowngrade(string fromVersion, string toVersion)
|
||||
{
|
||||
// Simple semver-like comparison
|
||||
// In production, use proper version comparison per ecosystem
|
||||
try
|
||||
{
|
||||
var fromParts = fromVersion.Split('.').Select(int.Parse).ToArray();
|
||||
var toParts = toVersion.Split('.').Select(int.Parse).ToArray();
|
||||
|
||||
for (var i = 0; i < Math.Min(fromParts.Length, toParts.Length); i++)
|
||||
{
|
||||
if (toParts[i] < fromParts[i]) return true;
|
||||
if (toParts[i] > fromParts[i]) return false;
|
||||
}
|
||||
|
||||
return toParts.Length < fromParts.Length;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall back to string comparison
|
||||
return string.Compare(toVersion, fromVersion, StringComparison.Ordinal) < 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeDiffHash(SbomDiff diff)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
diff.FromId,
|
||||
diff.ToId,
|
||||
Deltas = diff.Deltas.Select(d => new
|
||||
{
|
||||
d.Type,
|
||||
BeforePurl = d.Before?.Purl,
|
||||
AfterPurl = d.After?.Purl,
|
||||
d.ChangedFields
|
||||
})
|
||||
}, new JsonSerializerOptions { WriteIndented = false });
|
||||
|
||||
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(json));
|
||||
return Convert.ToHexStringLower(hashBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Lineage;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an SBOM with lineage tracking to its parent versions.
|
||||
/// </summary>
|
||||
public sealed record SbomLineage
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this SBOM version.
|
||||
/// </summary>
|
||||
public required SbomId Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Parent SBOM ID (null if this is the first version).
|
||||
/// </summary>
|
||||
public SbomId? ParentId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The image digest this SBOM describes.
|
||||
/// </summary>
|
||||
public required string ImageDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content-addressable hash (SHA-256 of canonical SBOM).
|
||||
/// </summary>
|
||||
public required string ContentHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this SBOM version was created.
|
||||
/// </summary>
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ancestor chain (parent, grandparent, etc.).
|
||||
/// </summary>
|
||||
public ImmutableArray<SbomId> Ancestors { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Pointer to the diff from parent (null if no parent).
|
||||
/// </summary>
|
||||
public SbomDiffPointer? DiffFromParent { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strongly-typed SBOM identifier.
|
||||
/// </summary>
|
||||
public readonly record struct SbomId(Guid Value)
|
||||
{
|
||||
public static SbomId New() => new(Guid.NewGuid());
|
||||
public static SbomId Parse(string value) => new(Guid.Parse(value));
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pointer to a diff document with summary statistics.
|
||||
/// </summary>
|
||||
public sealed record SbomDiffPointer
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of components added since parent.
|
||||
/// </summary>
|
||||
public required int ComponentsAdded { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of components removed since parent.
|
||||
/// </summary>
|
||||
public required int ComponentsRemoved { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of components modified since parent.
|
||||
/// </summary>
|
||||
public required int ComponentsModified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the diff document for retrieval.
|
||||
/// </summary>
|
||||
public required string DiffHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of changes.
|
||||
/// </summary>
|
||||
public int TotalChanges => ComponentsAdded + ComponentsRemoved + ComponentsModified;
|
||||
}
|
||||
Reference in New Issue
Block a user