feat: Add Storybook configuration and motion tokens implementation

- Introduced Storybook configuration files (`main.ts`, `preview.ts`, `tsconfig.json`) for Angular components.
- Created motion tokens in `motion-tokens.ts` to define durations, easing functions, and transforms.
- Developed a Storybook story for motion tokens showcasing their usage and reduced motion fallback.
- Added SCSS variables for motion durations, easing, and transforms in `_motion.scss`.
- Implemented accessibility smoke tests using Playwright and Axe for automated accessibility checks.
- Created portable and sealed bundle structures with corresponding JSON files for evidence locker.
- Added shell script for verifying notify kit determinism.
This commit is contained in:
StellaOps Bot
2025-12-04 21:36:06 +02:00
parent 600f3a7a3c
commit f214edff82
68 changed files with 1742 additions and 18 deletions

View File

@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Text.Json.Nodes;
using StellaOps.Graph.Indexer.Ingestion.Sbom;
using StellaOps.Graph.Indexer.Schema;
namespace StellaOps.Graph.Indexer.Analytics;

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text.Json.Nodes;

View File

@@ -44,8 +44,8 @@ public sealed class MongoGraphSnapshotProvider : IGraphSnapshotProvider
var tenant = snapshot.GetValue("tenant", string.Empty).AsString;
var snapshotId = snapshot.GetValue("snapshot_id", string.Empty).AsString;
var generatedAt = snapshot.TryGetValue("generated_at", out var generated)
&& generated.TryToUniversalTime(out var dt)
? dt
&& generated.BsonType == BsonType.DateTime
? DateTime.SpecifyKind(generated.ToUniversalTime(), DateTimeKind.Utc)
: DateTimeOffset.UtcNow;
var nodes = snapshot.TryGetValue("nodes", out var nodesValue) && nodesValue is BsonArray nodesArray

View File

@@ -0,0 +1,180 @@
using System.Text.Json.Serialization;
namespace StellaOps.Graph.Indexer.Ingestion.Inspector;
public sealed class GraphInspectorSnapshot
{
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; init; } = "graph.inspect.v1";
[JsonPropertyName("tenant")]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("artifactDigest")]
public string ArtifactDigest { get; init; } = string.Empty;
[JsonPropertyName("sbomDigest")]
public string SbomDigest { get; init; } = string.Empty;
[JsonPropertyName("collectedAt")]
public DateTimeOffset CollectedAt { get; init; } = DateTimeOffset.UnixEpoch;
[JsonPropertyName("eventOffset")]
public long EventOffset { get; init; }
[JsonPropertyName("components")]
public IReadOnlyList<GraphInspectorComponent> Components { get; init; } = Array.Empty<GraphInspectorComponent>();
[JsonPropertyName("links")]
public GraphInspectorLinks Links { get; init; } = new();
}
public sealed class GraphInspectorComponent
{
[JsonPropertyName("purl")]
public string Purl { get; init; } = string.Empty;
[JsonPropertyName("version")]
public string? Version { get; init; }
[JsonPropertyName("scopes")]
public IReadOnlyList<string> Scopes { get; init; } = Array.Empty<string>();
[JsonPropertyName("relationships")]
public IReadOnlyList<GraphInspectorRelationship> Relationships { get; init; } = Array.Empty<GraphInspectorRelationship>();
[JsonPropertyName("advisories")]
public IReadOnlyList<GraphInspectorAdvisoryObservation> Advisories { get; init; } = Array.Empty<GraphInspectorAdvisoryObservation>();
[JsonPropertyName("vexStatements")]
public IReadOnlyList<GraphInspectorVexStatement> VexStatements { get; init; } = Array.Empty<GraphInspectorVexStatement>();
[JsonPropertyName("provenance")]
public GraphInspectorProvenance? Provenance { get; init; }
}
public sealed class GraphInspectorRelationship
{
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("targetPurl")]
public string TargetPurl { get; init; } = string.Empty;
[JsonPropertyName("scope")]
public string? Scope { get; init; }
[JsonPropertyName("source")]
public string? Source { get; init; }
[JsonPropertyName("evidenceHash")]
public string EvidenceHash { get; init; } = string.Empty;
[JsonPropertyName("provenance")]
public GraphInspectorProvenance? Provenance { get; init; }
}
public sealed class GraphInspectorAdvisoryObservation
{
[JsonPropertyName("advisoryId")]
public string AdvisoryId { get; init; } = string.Empty;
[JsonPropertyName("source")]
public string Source { get; init; } = string.Empty;
[JsonPropertyName("status")]
public string Status { get; init; } = string.Empty;
[JsonPropertyName("severity")]
public string? Severity { get; init; }
[JsonPropertyName("cvss")]
public GraphInspectorCvss? Cvss { get; init; }
[JsonPropertyName("justification")]
public string? Justification { get; init; }
[JsonPropertyName("justificationSummary")]
public string? JustificationSummary { get; init; }
[JsonPropertyName("linksetDigest")]
public string? LinksetDigest { get; init; }
[JsonPropertyName("evidenceHash")]
public string EvidenceHash { get; init; } = string.Empty;
[JsonPropertyName("modifiedAt")]
public DateTimeOffset ModifiedAt { get; init; } = DateTimeOffset.UnixEpoch;
[JsonPropertyName("provenance")]
public GraphInspectorProvenance? Provenance { get; init; }
}
public sealed class GraphInspectorCvss
{
[JsonPropertyName("vector")]
public string? Vector { get; init; }
[JsonPropertyName("score")]
public double? Score { get; init; }
}
public sealed class GraphInspectorVexStatement
{
[JsonPropertyName("statementId")]
public string StatementId { get; init; } = string.Empty;
[JsonPropertyName("source")]
public string Source { get; init; } = string.Empty;
[JsonPropertyName("status")]
public string Status { get; init; } = string.Empty;
[JsonPropertyName("justification")]
public string Justification { get; init; } = string.Empty;
[JsonPropertyName("impactStatement")]
public string? ImpactStatement { get; init; }
[JsonPropertyName("knownExploited")]
public bool? KnownExploited { get; init; }
[JsonPropertyName("issuedAt")]
public DateTimeOffset IssuedAt { get; init; } = DateTimeOffset.UnixEpoch;
[JsonPropertyName("expiresAt")]
public DateTimeOffset? ExpiresAt { get; init; }
[JsonPropertyName("evidenceHash")]
public string EvidenceHash { get; init; } = string.Empty;
[JsonPropertyName("provenance")]
public GraphInspectorProvenance? Provenance { get; init; }
}
public sealed class GraphInspectorLinks
{
[JsonPropertyName("sbomObservationEventId")]
public string? SbomObservationEventId { get; init; }
[JsonPropertyName("linksetDigest")]
public string? LinksetDigest { get; init; }
}
public sealed class GraphInspectorProvenance
{
[JsonPropertyName("source")]
public string Source { get; init; } = string.Empty;
[JsonPropertyName("collectedAt")]
public DateTimeOffset CollectedAt { get; init; } = DateTimeOffset.UnixEpoch;
[JsonPropertyName("eventOffset")]
public long EventOffset { get; init; }
[JsonPropertyName("linksetDigest")]
public string? LinksetDigest { get; init; }
[JsonPropertyName("evidenceHash")]
public string? EvidenceHash { get; init; }
}

View File

@@ -0,0 +1,433 @@
using System.Collections.Immutable;
using System.Text.Json.Nodes;
using StellaOps.Graph.Indexer.Ingestion.Sbom;
using StellaOps.Graph.Indexer.Schema;
namespace StellaOps.Graph.Indexer.Ingestion.Inspector;
public sealed class GraphInspectorTransformer
{
private const string ArtifactKind = "artifact";
private const string ComponentKind = "component";
private const string AdvisoryKind = "advisory";
private const string VexKind = "vex_statement";
private const string ContainsEdgeKind = "CONTAINS";
private const string DependsOnEdgeKind = "DEPENDS_ON";
private const string ProvidesEdgeKind = "PROVIDES";
private const string ObservedRuntimeEdgeKind = "OBSERVED_RUNTIME";
private const string AffectedByEdgeKind = "AFFECTED_BY";
private const string VexExemptsEdgeKind = "VEX_EXEMPTS";
private const string DefaultSourceType = "inventory";
private const string DefaultSource = "graph.inspect.v1";
public GraphBuildBatch Transform(GraphInspectorSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);
var nodes = new List<JsonObject>();
var edges = new List<JsonObject>();
var componentNodes = new Dictionary<string, JsonObject>(StringComparer.OrdinalIgnoreCase);
var advisoryNodes = new Dictionary<(string Tenant, string Source, string AdvisoryId, string ContentHash), JsonObject>();
var vexNodes = new Dictionary<(string Tenant, string StatementId, string Source, string EvidenceHash), JsonObject>();
var artifactNode = CreateArtifactNode(snapshot);
nodes.Add(artifactNode);
foreach (var component in snapshot.Components)
{
var componentNode = GetOrCreateComponentNode(snapshot, componentNodes, component, component.Provenance);
nodes.Add(componentNode);
foreach (var relationship in component.Relationships ?? Array.Empty<GraphInspectorRelationship>())
{
var targetNode = GetOrCreateComponentNode(
snapshot,
componentNodes,
new GraphInspectorComponent
{
Purl = relationship.TargetPurl,
Version = null,
Scopes = Array.Empty<string>(),
Relationships = Array.Empty<GraphInspectorRelationship>(),
Advisories = Array.Empty<GraphInspectorAdvisoryObservation>(),
VexStatements = Array.Empty<GraphInspectorVexStatement>(),
Provenance = relationship.Provenance
},
relationship.Provenance);
var edge = CreateRelationshipEdge(snapshot, componentNode, targetNode, relationship);
edges.Add(edge);
}
foreach (var advisory in component.Advisories ?? Array.Empty<GraphInspectorAdvisoryObservation>())
{
var advisoryNode = GetOrCreateAdvisoryNode(snapshot, advisoryNodes, advisory);
if (!nodes.Contains(advisoryNode))
{
nodes.Add(advisoryNode);
}
var edge = CreateAffectedByEdge(snapshot, componentNode, advisoryNode, advisory);
edges.Add(edge);
}
foreach (var vex in component.VexStatements ?? Array.Empty<GraphInspectorVexStatement>())
{
var vexNode = GetOrCreateVexNode(snapshot, vexNodes, vex);
if (!nodes.Contains(vexNode))
{
nodes.Add(vexNode);
}
var edge = CreateVexExemptsEdge(snapshot, componentNode, vexNode, vex);
edges.Add(edge);
}
}
var orderedNodes = nodes
.Distinct(JsonNodeEqualityComparer.Instance)
.Select(n => (JsonObject)n)
.OrderBy(n => n["kind"]!.GetValue<string>(), StringComparer.Ordinal)
.ThenBy(n => n["id"]!.GetValue<string>(), StringComparer.Ordinal)
.ToImmutableArray();
var orderedEdges = edges
.Distinct(JsonNodeEqualityComparer.Instance)
.Select(e => (JsonObject)e)
.OrderBy(e => e["kind"]!.GetValue<string>(), StringComparer.Ordinal)
.ThenBy(e => e["id"]!.GetValue<string>(), StringComparer.Ordinal)
.ToImmutableArray();
return new GraphBuildBatch(orderedNodes, orderedEdges);
}
private JsonObject CreateArtifactNode(GraphInspectorSnapshot snapshot)
{
var attributes = new JsonObject
{
["artifact_digest"] = snapshot.ArtifactDigest,
["sbom_digest"] = snapshot.SbomDigest
};
var provenance = CreateProvenance(snapshot.SchemaVersion, snapshot.CollectedAt, snapshot.SbomDigest, snapshot.EventOffset);
return GraphDocumentFactory.CreateNode(new GraphNodeSpec(
Tenant: snapshot.Tenant,
Kind: ArtifactKind,
CanonicalKey: new Dictionary<string, string>
{
["tenant"] = snapshot.Tenant,
["artifact_digest"] = snapshot.ArtifactDigest,
["sbom_digest"] = snapshot.SbomDigest
},
Attributes: attributes,
Provenance: provenance,
ValidFrom: snapshot.CollectedAt,
ValidTo: null));
}
private JsonObject GetOrCreateComponentNode(
GraphInspectorSnapshot snapshot,
IDictionary<string, JsonObject> cache,
GraphInspectorComponent component,
GraphInspectorProvenance? provenanceOverride)
{
var key = $"{component.Purl}|{DefaultSourceType}";
if (cache.TryGetValue(key, out var existing))
{
return existing;
}
var attributes = new JsonObject
{
["purl"] = component.Purl,
["version"] = component.Version ?? string.Empty,
["scope"] = string.Empty,
["license_spdx"] = string.Empty,
["usage"] = string.Empty
};
if (component.Scopes?.Any() == true)
{
var scopesArray = new JsonArray();
foreach (var scope in component.Scopes.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct(StringComparer.Ordinal).OrderBy(s => s, StringComparer.Ordinal))
{
scopesArray.Add(scope);
}
attributes["scopes"] = scopesArray;
}
var provenance = ResolveProvenance(snapshot, provenanceOverride);
var node = GraphDocumentFactory.CreateNode(new GraphNodeSpec(
Tenant: snapshot.Tenant,
Kind: ComponentKind,
CanonicalKey: new Dictionary<string, string>
{
["tenant"] = snapshot.Tenant,
["purl"] = component.Purl,
["source_type"] = DefaultSourceType
},
Attributes: attributes,
Provenance: provenance,
ValidFrom: provenance.CollectedAt,
ValidTo: null));
cache[key] = node;
return node;
}
private JsonObject CreateRelationshipEdge(
GraphInspectorSnapshot snapshot,
JsonObject sourceNode,
JsonObject targetNode,
GraphInspectorRelationship relationship)
{
var kind = relationship.Type?.Trim().ToLowerInvariant() switch
{
"contains" => ContainsEdgeKind,
"depends_on" => DependsOnEdgeKind,
"provides" => ProvidesEdgeKind,
"runtime_observed" => ObservedRuntimeEdgeKind,
_ => DependsOnEdgeKind
};
var attributes = new JsonObject
{
["scope"] = relationship.Scope ?? string.Empty,
["evidence_digest"] = relationship.EvidenceHash,
["source"] = relationship.Source ?? string.Empty
};
var provenance = ResolveProvenance(snapshot, relationship.Provenance);
return GraphDocumentFactory.CreateEdge(new GraphEdgeSpec(
Tenant: snapshot.Tenant,
Kind: kind,
CanonicalKey: new Dictionary<string, string>
{
["tenant"] = snapshot.Tenant,
["source_node_id"] = sourceNode["id"]!.GetValue<string>(),
["target_node_id"] = targetNode["id"]!.GetValue<string>(),
["sbom_digest"] = snapshot.SbomDigest
},
Attributes: attributes,
Provenance: provenance,
ValidFrom: provenance.CollectedAt,
ValidTo: null));
}
private JsonObject GetOrCreateAdvisoryNode(
GraphInspectorSnapshot snapshot,
IDictionary<(string, string, string, string), JsonObject> cache,
GraphInspectorAdvisoryObservation advisory)
{
var contentHash = advisory.EvidenceHash ?? string.Empty;
var key = (snapshot.Tenant, advisory.Source, advisory.AdvisoryId, contentHash);
if (cache.TryGetValue(key, out var existing))
{
return existing;
}
var attributes = new JsonObject
{
["advisory_source"] = advisory.Source,
["advisory_id"] = advisory.AdvisoryId,
["severity"] = advisory.Severity ?? string.Empty,
["published_at"] = GraphTimestamp.Format(advisory.ModifiedAt),
["content_hash"] = contentHash,
["linkset_digest"] = advisory.LinksetDigest ?? string.Empty
};
var canonicalKey = new Dictionary<string, string>
{
["tenant"] = snapshot.Tenant,
["advisory_source"] = advisory.Source,
["advisory_id"] = advisory.AdvisoryId,
["content_hash"] = contentHash
};
var provenance = ResolveProvenance(snapshot, advisory.Provenance);
var node = GraphDocumentFactory.CreateNode(new GraphNodeSpec(
Tenant: snapshot.Tenant,
Kind: AdvisoryKind,
CanonicalKey: canonicalKey,
Attributes: attributes,
Provenance: provenance,
ValidFrom: advisory.ModifiedAt,
ValidTo: null));
cache[key] = node;
return node;
}
private JsonObject CreateAffectedByEdge(
GraphInspectorSnapshot snapshot,
JsonObject componentNode,
JsonObject advisoryNode,
GraphInspectorAdvisoryObservation advisory)
{
var attributes = new JsonObject
{
["evidence_digest"] = advisory.EvidenceHash,
["matched_versions"] = new JsonArray(),
["cvss"] = advisory.Cvss is null ? null : new JsonObject
{
["vector"] = advisory.Cvss.Vector,
["score"] = advisory.Cvss.Score
},
["justification"] = advisory.Justification ?? string.Empty,
["justification_summary"] = advisory.JustificationSummary ?? string.Empty,
["status"] = advisory.Status
};
var canonicalKey = new Dictionary<string, string>
{
["tenant"] = snapshot.Tenant,
["component_node_id"] = componentNode["id"]!.GetValue<string>(),
["advisory_node_id"] = advisoryNode["id"]!.GetValue<string>(),
["linkset_digest"] = advisory.LinksetDigest ?? string.Empty
};
var provenance = ResolveProvenance(snapshot, advisory.Provenance);
return GraphDocumentFactory.CreateEdge(new GraphEdgeSpec(
Tenant: snapshot.Tenant,
Kind: AffectedByEdgeKind,
CanonicalKey: canonicalKey,
Attributes: attributes,
Provenance: provenance,
ValidFrom: advisory.ModifiedAt,
ValidTo: null));
}
private JsonObject GetOrCreateVexNode(
GraphInspectorSnapshot snapshot,
IDictionary<(string Tenant, string StatementId, string Source, string EvidenceHash), JsonObject> cache,
GraphInspectorVexStatement vex)
{
var key = (snapshot.Tenant, vex.StatementId, vex.Source, vex.EvidenceHash);
if (cache.TryGetValue(key, out var existing))
{
return existing;
}
var attributes = new JsonObject
{
["status"] = vex.Status,
["statement_id"] = vex.StatementId,
["justification"] = vex.Justification,
["impact_statement"] = vex.ImpactStatement ?? string.Empty,
["known_exploited"] = vex.KnownExploited ?? false,
["content_hash"] = vex.EvidenceHash
};
var canonicalKey = new Dictionary<string, string>
{
["tenant"] = snapshot.Tenant,
["vex_source"] = vex.Source,
["statement_id"] = vex.StatementId,
["content_hash"] = vex.EvidenceHash
};
var provenance = ResolveProvenance(snapshot, vex.Provenance);
var node = GraphDocumentFactory.CreateNode(new GraphNodeSpec(
Tenant: snapshot.Tenant,
Kind: VexKind,
CanonicalKey: canonicalKey,
Attributes: attributes,
Provenance: provenance,
ValidFrom: vex.IssuedAt,
ValidTo: vex.ExpiresAt));
cache[key] = node;
return node;
}
private JsonObject CreateVexExemptsEdge(
GraphInspectorSnapshot snapshot,
JsonObject componentNode,
JsonObject vexNode,
GraphInspectorVexStatement vex)
{
var attributes = new JsonObject
{
["status"] = vex.Status,
["justification"] = vex.Justification,
["impact_statement"] = vex.ImpactStatement ?? string.Empty,
["evidence_digest"] = vex.EvidenceHash
};
var canonicalKey = new Dictionary<string, string>
{
["tenant"] = snapshot.Tenant,
["component_node_id"] = componentNode["id"]!.GetValue<string>(),
["vex_node_id"] = vexNode["id"]!.GetValue<string>(),
["statement_hash"] = vex.EvidenceHash
};
var provenance = ResolveProvenance(snapshot, vex.Provenance);
return GraphDocumentFactory.CreateEdge(new GraphEdgeSpec(
Tenant: snapshot.Tenant,
Kind: VexExemptsEdgeKind,
CanonicalKey: canonicalKey,
Attributes: attributes,
Provenance: provenance,
ValidFrom: vex.IssuedAt,
ValidTo: vex.ExpiresAt));
}
private static GraphProvenanceSpec ResolveProvenance(GraphInspectorSnapshot snapshot, GraphInspectorProvenance? overrideProvenance)
{
if (overrideProvenance is not null)
{
return new GraphProvenanceSpec(
Source: string.IsNullOrWhiteSpace(overrideProvenance.Source) ? DefaultSource : overrideProvenance.Source,
CollectedAt: overrideProvenance.CollectedAt == DateTimeOffset.UnixEpoch ? snapshot.CollectedAt : overrideProvenance.CollectedAt,
SbomDigest: snapshot.SbomDigest,
EventOffset: overrideProvenance.EventOffset == 0 ? snapshot.EventOffset : overrideProvenance.EventOffset);
}
return new GraphProvenanceSpec(
Source: DefaultSource,
CollectedAt: snapshot.CollectedAt,
SbomDigest: snapshot.SbomDigest,
EventOffset: snapshot.EventOffset);
}
private static GraphProvenanceSpec CreateProvenance(string? source, DateTimeOffset collectedAt, string? sbomDigest, long? eventOffset)
{
return new GraphProvenanceSpec(
Source: string.IsNullOrWhiteSpace(source) ? DefaultSource : source!,
CollectedAt: collectedAt,
SbomDigest: sbomDigest,
EventOffset: eventOffset);
}
private sealed class JsonNodeEqualityComparer : IEqualityComparer<JsonNode>
{
public static readonly JsonNodeEqualityComparer Instance = new();
public bool Equals(JsonNode? x, JsonNode? y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (x is null || y is null)
{
return false;
}
return x.ToJsonString() == y.ToJsonString();
}
public int GetHashCode(JsonNode obj) => obj.ToJsonString().GetHashCode(StringComparison.Ordinal);
}
}

View File

@@ -0,0 +1,115 @@
using StellaOps.Graph.Indexer.Ingestion.Inspector;
using StellaOps.Graph.Indexer.Schema;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class GraphInspectorTransformerTests
{
[Fact]
public void Transform_BuildsNodesAndEdges_FromInspectorSnapshot()
{
var snapshot = new GraphInspectorSnapshot
{
Tenant = "acme-dev",
ArtifactDigest = "sha256:artifact",
SbomDigest = "sha256:sbom",
CollectedAt = DateTimeOffset.Parse("2025-12-04T15:30:00Z"),
EventOffset = 5123,
Components = new[]
{
new GraphInspectorComponent
{
Purl = "pkg:maven/org.example/foo@1.2.3",
Scopes = new[] {"runtime"},
Relationships = new[]
{
new GraphInspectorRelationship
{
Type = "depends_on",
TargetPurl = "pkg:npm/lodash@4.17.21",
Scope = "runtime",
EvidenceHash = "e1",
Source = "concelier.linkset.v1",
Provenance = new GraphInspectorProvenance
{
Source = "concelier.linkset.v1",
CollectedAt = DateTimeOffset.Parse("2025-12-04T15:29:00Z"),
EventOffset = 6000,
EvidenceHash = "e1"
}
}
},
Advisories = new[]
{
new GraphInspectorAdvisoryObservation
{
AdvisoryId = "CVE-2024-1111",
Source = "ghsa",
Status = "affected",
Severity = "HIGH",
EvidenceHash = "a1",
LinksetDigest = "abcdef",
ModifiedAt = DateTimeOffset.Parse("2025-11-30T12:00:00Z"),
Provenance = new GraphInspectorProvenance
{
Source = "concelier.linkset.v1",
CollectedAt = DateTimeOffset.Parse("2025-11-30T11:55:00Z"),
EventOffset = 4421,
EvidenceHash = "a1"
}
}
},
VexStatements = new[]
{
new GraphInspectorVexStatement
{
StatementId = "VEX-2025-0001",
Source = "excitor.vex.v1",
Status = "not_affected",
Justification = "component_not_present",
EvidenceHash = "v1",
IssuedAt = DateTimeOffset.Parse("2025-12-01T08:00:00Z"),
ExpiresAt = DateTimeOffset.Parse("2026-12-01T00:00:00Z"),
Provenance = new GraphInspectorProvenance
{
Source = "excitor.overlay.v1",
CollectedAt = DateTimeOffset.Parse("2025-12-01T08:00:00Z"),
EventOffset = 171,
EvidenceHash = "v1"
}
}
},
Provenance = new GraphInspectorProvenance
{
Source = "concelier.linkset.v1",
CollectedAt = DateTimeOffset.Parse("2025-12-04T15:29:00Z"),
EventOffset = 5123,
EvidenceHash = "c1"
}
}
}
};
var transformer = new GraphInspectorTransformer();
var batch = transformer.Transform(snapshot);
// Nodes: artifact + source component + target component + advisory + vex
Assert.Equal(5, batch.Nodes.Length);
Assert.Contains(batch.Nodes, n => n["kind"]!.GetValue<string>() == "artifact");
Assert.Contains(batch.Nodes, n => n["kind"]!.GetValue<string>() == "component" && n["canonical_key"]!["purl"]!.GetValue<string>() == "pkg:maven/org.example/foo@1.2.3");
Assert.Contains(batch.Nodes, n => n["kind"]!.GetValue<string>() == "component" && n["canonical_key"]!["purl"]!.GetValue<string>() == "pkg:npm/lodash@4.17.21");
Assert.Contains(batch.Nodes, n => n["kind"]!.GetValue<string>() == "advisory");
Assert.Contains(batch.Nodes, n => n["kind"]!.GetValue<string>() == "vex_statement");
// Edges: depends_on, affected_by, vex_exempts
Assert.Contains(batch.Edges, e => e["kind"]!.GetValue<string>() == "DEPENDS_ON");
Assert.Contains(batch.Edges, e => e["kind"]!.GetValue<string>() == "AFFECTED_BY");
Assert.Contains(batch.Edges, e => e["kind"]!.GetValue<string>() == "VEX_EXEMPTS");
// Provenance should carry sbom digest and event offset from snapshot/provenance overrides.
var dependsOn = batch.Edges.Single(e => e["kind"]!.GetValue<string>() == "DEPENDS_ON");
Assert.Equal("sha256:sbom", dependsOn["provenance"]!["sbom_digest"]!.GetValue<string>());
Assert.Equal(6000, dependsOn["provenance"]!["event_offset"]!.GetValue<long>());
}
}