feat: Add initial implementation of Vulnerability Resolver Jobs
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Created project for StellaOps.Scanner.Analyzers.Native.Tests with necessary dependencies.
- Documented roles and guidelines in AGENTS.md for Scheduler module.
- Implemented IResolverJobService interface and InMemoryResolverJobService for handling resolver jobs.
- Added ResolverBacklogNotifier and ResolverBacklogService for monitoring job metrics.
- Developed API endpoints for managing resolver jobs and retrieving metrics.
- Defined models for resolver job requests and responses.
- Integrated dependency injection for resolver job services.
- Implemented ImpactIndexSnapshot for persisting impact index data.
- Introduced SignalsScoringOptions for configurable scoring weights in reachability scoring.
- Added unit tests for ReachabilityScoringService and RuntimeFactsIngestionService.
- Created dotnet-filter.sh script to handle command-line arguments for dotnet.
- Established nuget-prime project for managing package downloads.
This commit is contained in:
master
2025-11-18 07:52:15 +02:00
parent e69b57d467
commit 8355e2ff75
299 changed files with 13293 additions and 2444 deletions

View File

@@ -16,6 +16,8 @@ Expose normalized SBOM projections (components, relationships, scopes, entrypoin
## Required Reading
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/sbomservice/architecture.md`
- `docs/implplan/SPRINT_0142_0001_0001_sbomservice.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in both correspoding sprint file `/docs/implplan/SPRINT_*.md` and the local `TASKS.md` when you start or finish work.

View File

@@ -0,0 +1,87 @@
namespace StellaOps.SbomService.Models;
public sealed record SbomPathQuery(
string Purl,
string? Artifact,
string? Scope,
string? Environment,
int Limit = 50,
int Offset = 0);
public sealed record SbomPathNode(string Name, string Kind);
public sealed record SbomPath(
IReadOnlyList<SbomPathNode> Nodes,
bool RuntimeFlag,
string? BlastRadius,
string? NearestSafeVersion);
public sealed record SbomPathResult(
string Purl,
string? Artifact,
string? Scope,
string? Environment,
IReadOnlyList<SbomPath> Paths,
string? NextCursor);
public sealed record SbomTimelineQuery(
string Artifact,
int Limit = 50,
int Offset = 0);
public sealed record SbomVersion(
string Version,
string Digest,
DateTimeOffset CreatedAt,
string SourceBundleHash,
string? Provenance);
public sealed record SbomTimelineResult(
string Artifact,
IReadOnlyList<SbomVersion> Versions,
string? NextCursor);
public sealed record SbomCatalogQuery(
string? Artifact,
string? License,
string? Scope,
string? AssetTag,
int Limit = 50,
int Offset = 0);
public sealed record SbomCatalogItem(
string Artifact,
string SbomVersion,
string Digest,
string? License,
string Scope,
IReadOnlyDictionary<string, string> AssetTags,
DateTimeOffset CreatedAt,
string ProjectionHash,
string EvaluationMetadata);
public sealed record SbomCatalogResult(
IReadOnlyList<SbomCatalogItem> Items,
string? NextCursor);
public sealed record QueryResult<T>(T Result, bool CacheHit);
public sealed record ComponentLookupQuery(
string Purl,
string? Artifact,
int Limit = 50,
int Offset = 0);
public sealed record ComponentNeighbor(
string Purl,
string Relationship,
string? License,
string Scope,
bool RuntimeFlag);
public sealed record ComponentLookupResult(
string Purl,
string? Artifact,
IReadOnlyList<ComponentNeighbor> Neighbors,
string? NextCursor,
string CacheHint);

View File

@@ -0,0 +1,24 @@
using System.Diagnostics.Metrics;
namespace StellaOps.SbomService.Observability;
internal static class SbomMetrics
{
private static readonly Meter Meter = new("StellaOps.SbomService");
public static readonly Histogram<double> PathsLatencySeconds =
Meter.CreateHistogram<double>("sbom_paths_latency_seconds", unit: "s",
description: "Latency for SBOM path queries by tenant/scenario");
public static readonly Counter<long> PathsQueryTotal =
Meter.CreateCounter<long>("sbom_paths_queries_total",
description: "Total SBOM path queries, tagged by cache hit and scope");
public static readonly Histogram<double> TimelineLatencySeconds =
Meter.CreateHistogram<double>("sbom_timeline_latency_seconds", unit: "s",
description: "Latency for SBOM version timeline queries");
public static readonly Counter<long> TimelineQueryTotal =
Meter.CreateCounter<long>("sbom_timeline_queries_total",
description: "Total SBOM timeline queries");
}

View File

@@ -1,17 +1,202 @@
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables("SBOM_");
builder.Services.AddOptions();
builder.Services.AddLogging();
// TODO: register SBOM projection services, repositories, and Authority integration.
var app = builder.Build();
app.MapGet("/healthz", () => Results.Ok(new { status = "ok" }));
app.MapGet("/readyz", () => Results.Ok(new { status = "warming" }));
app.Run();
using System.Diagnostics;
using System.Globalization;
using System.Diagnostics.Metrics;
using Microsoft.AspNetCore.Mvc;
using StellaOps.SbomService.Models;
using StellaOps.SbomService.Services;
using StellaOps.SbomService.Observability;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables("SBOM_");
builder.Services.AddOptions();
builder.Services.AddLogging();
// Register SBOM query services (InMemory seed; replace with Mongo-backed repository later).
builder.Services.AddSingleton<ISbomQueryService, InMemorySbomQueryService>();
var app = builder.Build();
app.MapGet("/healthz", () => Results.Ok(new { status = "ok" }));
app.MapGet("/readyz", () => Results.Ok(new { status = "warming" }));
app.MapGet("/console/sboms", async Task<IResult> (
[FromServices] ISbomQueryService service,
[FromQuery] string? artifact,
[FromQuery] string? license,
[FromQuery] string? scope,
[FromQuery(Name = "assetTag")] string? assetTag,
[FromQuery] string? cursor,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
{
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
}
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return Results.BadRequest(new { error = "cursor must be an integer offset" });
}
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
var pageSize = limit ?? 50;
var start = Stopwatch.GetTimestamp();
var result = await service.GetConsoleCatalogAsync(
new SbomCatalogQuery(artifact?.Trim(), license?.Trim(), scope?.Trim(), assetTag?.Trim(), pageSize, offset),
cancellationToken);
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new TagList
{
{ "scope", scope ?? string.Empty },
{ "env", string.Empty }
});
SbomMetrics.PathsQueryTotal.Add(1, new TagList
{
{ "cache_hit", result.CacheHit },
{ "scope", scope ?? string.Empty }
});
return Results.Ok(result.Result);
});
app.MapGet("/components/lookup", async Task<IResult> (
[FromServices] ISbomQueryService service,
[FromQuery] string? purl,
[FromQuery] string? artifact,
[FromQuery] string? cursor,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(purl))
{
return Results.BadRequest(new { error = "purl is required" });
}
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
{
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
}
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return Results.BadRequest(new { error = "cursor must be an integer offset" });
}
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
var pageSize = limit ?? 50;
var start = Stopwatch.GetTimestamp();
var result = await service.GetComponentLookupAsync(
new ComponentLookupQuery(purl.Trim(), artifact?.Trim(), pageSize, offset),
cancellationToken);
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new TagList
{
{ "scope", string.Empty },
{ "env", string.Empty }
});
SbomMetrics.PathsQueryTotal.Add(1, new TagList
{
{ "cache_hit", result.CacheHit },
{ "scope", string.Empty }
});
return Results.Ok(result.Result);
});
app.MapGet("/sbom/paths", async Task<IResult> (
[FromServices] ISbomQueryService service,
[FromQuery] string? purl,
[FromQuery] string? artifact,
[FromQuery] string? scope,
[FromQuery(Name = "env")] string? environment,
[FromQuery] string? cursor,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(purl))
{
return Results.BadRequest(new { error = "purl is required" });
}
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
{
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
}
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return Results.BadRequest(new { error = "cursor must be an integer offset" });
}
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
var pageSize = limit ?? 50;
var start = Stopwatch.GetTimestamp();
var result = await service.GetPathsAsync(
new SbomPathQuery(purl.Trim(), artifact?.Trim(), scope?.Trim(), environment?.Trim(), pageSize, offset),
cancellationToken);
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new TagList
{
{ "scope", scope ?? string.Empty },
{ "env", environment ?? string.Empty }
});
SbomMetrics.PathsQueryTotal.Add(1, new TagList
{
{ "cache_hit", result.CacheHit },
{ "scope", scope ?? string.Empty }
});
return Results.Ok(result.Result);
});
app.MapGet("/sbom/versions", async Task<IResult> (
[FromServices] ISbomQueryService service,
[FromQuery] string? artifact,
[FromQuery] string? cursor,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(artifact))
{
return Results.BadRequest(new { error = "artifact is required" });
}
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
{
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
}
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return Results.BadRequest(new { error = "cursor must be an integer offset" });
}
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
var pageSize = limit ?? 50;
var start = Stopwatch.GetTimestamp();
var result = await service.GetTimelineAsync(
new SbomTimelineQuery(artifact.Trim(), pageSize, offset),
cancellationToken);
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
SbomMetrics.TimelineLatencySeconds.Record(elapsedSeconds, new TagList { { "artifact", artifact } });
SbomMetrics.TimelineQueryTotal.Add(1, new TagList { { "artifact", artifact }, { "cache_hit", result.CacheHit } });
return Results.Ok(result.Result);
});
app.Run();
public partial class Program;

View File

@@ -0,0 +1,14 @@
using StellaOps.SbomService.Models;
namespace StellaOps.SbomService.Services;
public interface ISbomQueryService
{
Task<QueryResult<SbomPathResult>> GetPathsAsync(SbomPathQuery query, CancellationToken cancellationToken);
Task<QueryResult<SbomTimelineResult>> GetTimelineAsync(SbomTimelineQuery query, CancellationToken cancellationToken);
Task<QueryResult<SbomCatalogResult>> GetConsoleCatalogAsync(SbomCatalogQuery query, CancellationToken cancellationToken);
Task<QueryResult<ComponentLookupResult>> GetComponentLookupAsync(ComponentLookupQuery query, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,361 @@
using System.Globalization;
using StellaOps.SbomService.Models;
namespace StellaOps.SbomService.Services;
internal sealed class InMemorySbomQueryService : ISbomQueryService
{
private readonly IReadOnlyList<PathRecord> _paths;
private readonly IReadOnlyList<TimelineRecord> _timelines;
private readonly IReadOnlyList<CatalogRecord> _catalog;
private readonly IReadOnlyList<ComponentLookupRecord> _components;
private readonly ConcurrentDictionary<string, object> _cache = new();
public InMemorySbomQueryService()
{
// Deterministic seed data for early contract testing; replace with Mongo-backed implementation later.
_paths = SeedPaths();
_timelines = SeedTimelines();
_catalog = SeedCatalog();
_components = SeedComponents();
}
public Task<QueryResult<SbomPathResult>> GetPathsAsync(SbomPathQuery query, CancellationToken cancellationToken)
{
var cacheKey = $"paths|{query.Purl}|{query.Artifact}|{query.Scope}|{query.Environment}|{query.Offset}|{query.Limit}";
if (_cache.TryGetValue(cacheKey, out var cached) && cached is SbomPathResult cachedResult)
{
return Task.FromResult(new QueryResult<SbomPathResult>(cachedResult, true));
}
var filtered = _paths
.Where(p => p.Purl.Equals(query.Purl, StringComparison.OrdinalIgnoreCase))
.Where(p => query.Artifact is null || p.Artifact.Equals(query.Artifact, StringComparison.OrdinalIgnoreCase))
.Where(p => query.Scope is null || string.Equals(p.Scope, query.Scope, StringComparison.OrdinalIgnoreCase))
.Where(p => query.Environment is null || string.Equals(p.Environment, query.Environment, StringComparison.OrdinalIgnoreCase))
.OrderBy(p => p.Artifact)
.ThenBy(p => p.Environment)
.ThenBy(p => p.Scope)
.ThenBy(p => string.Join("->", p.Nodes.Select(n => n.Name)))
.ToList();
var page = filtered
.Skip(query.Offset)
.Take(query.Limit)
.Select(r => new SbomPath(r.Nodes, r.RuntimeFlag, r.BlastRadius, r.NearestSafeVersion))
.ToList();
string? nextCursor = query.Offset + query.Limit < filtered.Count
? (query.Offset + query.Limit).ToString(CultureInfo.InvariantCulture)
: null;
var result = new SbomPathResult(
Purl: query.Purl,
Artifact: query.Artifact,
Scope: query.Scope,
Environment: query.Environment,
Paths: page,
NextCursor: nextCursor);
_cache[cacheKey] = result;
return Task.FromResult(new QueryResult<SbomPathResult>(result, false));
}
public Task<QueryResult<SbomTimelineResult>> GetTimelineAsync(SbomTimelineQuery query, CancellationToken cancellationToken)
{
var cacheKey = $"timeline|{query.Artifact}|{query.Offset}|{query.Limit}";
if (_cache.TryGetValue(cacheKey, out var cached) && cached is SbomTimelineResult cachedTimeline)
{
return Task.FromResult(new QueryResult<SbomTimelineResult>(cachedTimeline, true));
}
var filtered = _timelines
.Where(t => t.Artifact.Equals(query.Artifact, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(t => t.CreatedAt)
.ThenByDescending(t => t.Version)
.ToList();
var page = filtered
.Skip(query.Offset)
.Take(query.Limit)
.Select(t => new SbomVersion(t.Version, t.Digest, t.CreatedAt, t.SourceBundleHash, t.Provenance))
.ToList();
string? nextCursor = query.Offset + query.Limit < filtered.Count
? (query.Offset + query.Limit).ToString(CultureInfo.InvariantCulture)
: null;
var result = new SbomTimelineResult(query.Artifact, page, nextCursor);
_cache[cacheKey] = result;
return Task.FromResult(new QueryResult<SbomTimelineResult>(result, false));
}
public Task<QueryResult<SbomCatalogResult>> GetConsoleCatalogAsync(SbomCatalogQuery query, CancellationToken cancellationToken)
{
var cacheKey = $"catalog|{query.Artifact}|{query.License}|{query.Scope}|{query.AssetTag}|{query.Offset}|{query.Limit}";
if (_cache.TryGetValue(cacheKey, out var cached) && cached is SbomCatalogResult cachedCatalog)
{
return Task.FromResult(new QueryResult<SbomCatalogResult>(cachedCatalog, true));
}
var filtered = _catalog
.Where(c => query.Artifact is null || c.Artifact.Contains(query.Artifact, StringComparison.OrdinalIgnoreCase))
.Where(c => query.License is null || string.Equals(c.License, query.License, StringComparison.OrdinalIgnoreCase))
.Where(c => query.Scope is null || string.Equals(c.Scope, query.Scope, StringComparison.OrdinalIgnoreCase))
.Where(c => query.AssetTag is null || c.AssetTags.ContainsKey(query.AssetTag))
.OrderByDescending(c => c.CreatedAt)
.ThenBy(c => c.Artifact)
.ToList();
var page = filtered
.Skip(query.Offset)
.Take(query.Limit)
.Select(c => new SbomCatalogItem(
c.Artifact,
c.SbomVersion,
c.Digest,
c.License,
c.Scope,
c.AssetTags,
c.CreatedAt,
c.ProjectionHash,
c.EvaluationMetadata))
.ToList();
string? nextCursor = query.Offset + query.Limit < filtered.Count
? (query.Offset + query.Limit).ToString(CultureInfo.InvariantCulture)
: null;
var result = new SbomCatalogResult(page, nextCursor);
_cache[cacheKey] = result;
return Task.FromResult(new QueryResult<SbomCatalogResult>(result, false));
}
public Task<QueryResult<ComponentLookupResult>> GetComponentLookupAsync(ComponentLookupQuery query, CancellationToken cancellationToken)
{
var cacheKey = $"component|{query.Purl}|{query.Artifact}|{query.Offset}|{query.Limit}";
if (_cache.TryGetValue(cacheKey, out var cached) && cached is ComponentLookupResult cachedResult)
{
return Task.FromResult(new QueryResult<ComponentLookupResult>(cachedResult, true));
}
var filtered = _components
.Where(c => c.Purl.Equals(query.Purl, StringComparison.OrdinalIgnoreCase))
.Where(c => query.Artifact is null || c.Artifact.Equals(query.Artifact, StringComparison.OrdinalIgnoreCase))
.OrderBy(c => c.Artifact)
.ThenBy(c => c.Purl)
.ToList();
var page = filtered
.Skip(query.Offset)
.Take(query.Limit)
.Select(c => new ComponentNeighbor(c.NeighborPurl, c.Relationship, c.License, c.Scope, c.RuntimeFlag))
.ToList();
string? nextCursor = query.Offset + query.Limit < filtered.Count
? (query.Offset + query.Limit).ToString(CultureInfo.InvariantCulture)
: null;
var result = new ComponentLookupResult(query.Purl, query.Artifact, page, nextCursor, CacheHint: "seeded");
_cache[cacheKey] = result;
return Task.FromResult(new QueryResult<ComponentLookupResult>(result, false));
}
private static IReadOnlyList<PathRecord> SeedPaths()
{
return new List<PathRecord>
{
new(
Artifact: "ghcr.io/stellaops/sample-api@sha256:111",
Purl: "pkg:npm/lodash@4.17.21",
Scope: "runtime",
Environment: "prod",
RuntimeFlag: true,
BlastRadius: "medium",
NearestSafeVersion: "pkg:npm/lodash@4.17.22",
Nodes: new[]
{
new SbomPathNode("sample-api", "artifact"),
new SbomPathNode("express", "npm"),
new SbomPathNode("lodash", "npm")
}),
new(
Artifact: "ghcr.io/stellaops/sample-api@sha256:111",
Purl: "pkg:npm/lodash@4.17.21",
Scope: "build",
Environment: "prod",
RuntimeFlag: false,
BlastRadius: "low",
NearestSafeVersion: "pkg:npm/lodash@4.17.22",
Nodes: new[]
{
new SbomPathNode("sample-api", "artifact"),
new SbomPathNode("rollup", "npm"),
new SbomPathNode("lodash", "npm")
}),
new(
Artifact: "ghcr.io/stellaops/sample-api@sha256:222",
Purl: "pkg:nuget/Newtonsoft.Json@13.0.2",
Scope: "runtime",
Environment: "staging",
RuntimeFlag: true,
BlastRadius: "high",
NearestSafeVersion: "pkg:nuget/Newtonsoft.Json@13.0.3",
Nodes: new[]
{
new SbomPathNode("sample-worker", "artifact"),
new SbomPathNode("StellaOps.Core", "nuget"),
new SbomPathNode("Newtonsoft.Json", "nuget")
})
};
}
private static IReadOnlyList<TimelineRecord> SeedTimelines()
{
return new List<TimelineRecord>
{
new(
Artifact: "ghcr.io/stellaops/sample-api",
Version: "2025.11.15.1",
Digest: "sha256:111",
SourceBundleHash: "sha256:bundle111",
CreatedAt: new DateTimeOffset(2025, 11, 15, 12, 0, 0, TimeSpan.Zero),
Provenance: "scanner:surface_bundle_mock_v1.tgz"),
new(
Artifact: "ghcr.io/stellaops/sample-api",
Version: "2025.11.16.1",
Digest: "sha256:112",
SourceBundleHash: "sha256:bundle112",
CreatedAt: new DateTimeOffset(2025, 11, 16, 12, 0, 0, TimeSpan.Zero),
Provenance: "scanner:surface_bundle_mock_v1.tgz"),
new(
Artifact: "ghcr.io/stellaops/sample-worker",
Version: "2025.11.12.0",
Digest: "sha256:222",
SourceBundleHash: "sha256:bundle222",
CreatedAt: new DateTimeOffset(2025, 11, 12, 8, 0, 0, TimeSpan.Zero),
Provenance: "upload:spdx:worker"),
};
}
private static IReadOnlyList<CatalogRecord> SeedCatalog()
{
return new List<CatalogRecord>
{
new(
Artifact: "ghcr.io/stellaops/sample-api",
SbomVersion: "2025.11.16.1",
Digest: "sha256:112",
License: "MIT",
Scope: "runtime",
AssetTags: new Dictionary<string, string>
{
["owner"] = "payments",
["criticality"] = "high",
["env"] = "prod"
},
CreatedAt: new DateTimeOffset(2025, 11, 16, 12, 0, 0, TimeSpan.Zero),
ProjectionHash: "sha256:proj112",
EvaluationMetadata: "eval:passed:v1"),
new(
Artifact: "ghcr.io/stellaops/sample-api",
SbomVersion: "2025.11.15.1",
Digest: "sha256:111",
License: "MIT",
Scope: "runtime",
AssetTags: new Dictionary<string, string>
{
["owner"] = "payments",
["criticality"] = "high",
["env"] = "prod"
},
CreatedAt: new DateTimeOffset(2025, 11, 15, 12, 0, 0, TimeSpan.Zero),
ProjectionHash: "sha256:proj111",
EvaluationMetadata: "eval:passed:v1"),
new(
Artifact: "ghcr.io/stellaops/sample-worker",
SbomVersion: "2025.11.12.0",
Digest: "sha256:222",
License: "Apache-2.0",
Scope: "runtime",
AssetTags: new Dictionary<string, string>
{
["owner"] = "platform",
["criticality"] = "medium",
["env"] = "staging"
},
CreatedAt: new DateTimeOffset(2025, 11, 12, 8, 0, 0, TimeSpan.Zero),
ProjectionHash: "sha256:proj222",
EvaluationMetadata: "eval:pending:v1"),
};
}
private static IReadOnlyList<ComponentLookupRecord> SeedComponents()
{
return new List<ComponentLookupRecord>
{
new(
Artifact: "ghcr.io/stellaops/sample-api",
Purl: "pkg:npm/lodash@4.17.21",
NeighborPurl: "pkg:npm/express@4.18.2",
Relationship: "DEPENDS_ON",
License: "MIT",
Scope: "runtime",
RuntimeFlag: true),
new(
Artifact: "ghcr.io/stellaops/sample-api",
Purl: "pkg:npm/lodash@4.17.21",
NeighborPurl: "pkg:npm/rollup@3.0.0",
Relationship: "DEPENDS_ON",
License: "MIT",
Scope: "build",
RuntimeFlag: false),
new(
Artifact: "ghcr.io/stellaops/sample-worker",
Purl: "pkg:nuget/Newtonsoft.Json@13.0.2",
NeighborPurl: "pkg:nuget/StellaOps.Core@1.0.0",
Relationship: "DEPENDS_ON",
License: "Apache-2.0",
Scope: "runtime",
RuntimeFlag: true)
};
}
private sealed record PathRecord(
string Artifact,
string Purl,
string? Scope,
string? Environment,
bool RuntimeFlag,
string? BlastRadius,
string? NearestSafeVersion,
IReadOnlyList<SbomPathNode> Nodes);
private sealed record TimelineRecord(
string Artifact,
string Version,
string Digest,
string SourceBundleHash,
DateTimeOffset CreatedAt,
string? Provenance);
private sealed record CatalogRecord(
string Artifact,
string SbomVersion,
string Digest,
string? License,
string Scope,
IReadOnlyDictionary<string, string> AssetTags,
DateTimeOffset CreatedAt,
string ProjectionHash,
string EvaluationMetadata);
private sealed record ComponentLookupRecord(
string Artifact,
string Purl,
string NeighborPurl,
string Relationship,
string? License,
string Scope,
bool RuntimeFlag);
}