feat(graph): introduce graph.inspect.v1 contract and schema for SBOM relationships
Some checks failed
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
Docs CI / lint-and-preview (push) Has been cancelled
Console CI / console-ci (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Some checks failed
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
Docs CI / lint-and-preview (push) Has been cancelled
Console CI / console-ci (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
- Added graph.inspect.v1 documentation outlining payload structure and determinism rules. - Created JSON schema for graph.inspect.v1 to enforce payload validation. - Defined mapping rules for graph relationships, advisories, and VEX statements. feat(notifications): establish remediation blueprint for gaps NR1-NR10 - Documented requirements, evidence, and tests for Notifier runtime. - Specified deliverables and next steps for addressing identified gaps. docs(notifications): organize operations and schemas documentation - Created README files for operations, schemas, and security notes to clarify deliverables and policies. feat(advisory): implement PostgreSQL caching for Link-Not-Merge linksets - Created database schema for advisory linkset cache. - Developed repository for managing advisory linkset cache operations. - Added tests to ensure correct functionality of the AdvisoryLinksetCacheRepository.
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Core.Linksets;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Tests.Linksets;
|
||||
|
||||
[Collection(ConcelierPostgresCollection.Name)]
|
||||
public sealed class AdvisoryLinksetCacheRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly AdvisoryLinksetCacheRepository _repository;
|
||||
|
||||
public AdvisoryLinksetCacheRepositoryTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
PostgresOptions options = fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = fixture.SchemaName;
|
||||
var dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
|
||||
_repository = new AdvisoryLinksetCacheRepository(dataSource, NullLogger<AdvisoryLinksetCacheRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task Upsert_NormalizesTenantAndReplaces()
|
||||
{
|
||||
var createdAt = DateTimeOffset.Parse("2025-11-20T12:00:00Z");
|
||||
var initial = BuildLinkset("Tenant-A", "ghsa", "GHSA-1", new[] { "obs-1" }, createdAt, confidence: 0.5);
|
||||
var replacement = BuildLinkset("tenant-a", "ghsa", "GHSA-1", new[] { "obs-2" }, createdAt.AddMinutes(5), confidence: 0.9);
|
||||
|
||||
await _repository.UpsertAsync(initial, CancellationToken.None);
|
||||
await _repository.UpsertAsync(replacement, CancellationToken.None);
|
||||
|
||||
var results = await _repository.FindByTenantAsync("TENANT-A", null, null, cursor: null, limit: 10, CancellationToken.None);
|
||||
|
||||
results.Should().ContainSingle();
|
||||
results[0].TenantId.Should().Be("tenant-a");
|
||||
results[0].ObservationIds.Should().ContainSingle("obs-2");
|
||||
results[0].Confidence.Should().Be(0.9);
|
||||
results[0].CreatedAt.Should().Be(replacement.CreatedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindByTenantAsync_OrdersAndPages()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var linksets = new[]
|
||||
{
|
||||
BuildLinkset("tenant", "src", "ADV-002", new[] { "obs-1" }, now, confidence: null),
|
||||
BuildLinkset("tenant", "src", "ADV-001", new[] { "obs-2" }, now, confidence: 0.7),
|
||||
BuildLinkset("tenant", "src", "ADV-003", new[] { "obs-3" }, now.AddMinutes(-10), confidence: 0.2)
|
||||
};
|
||||
|
||||
foreach (var linkset in linksets)
|
||||
{
|
||||
await _repository.UpsertAsync(linkset, CancellationToken.None);
|
||||
}
|
||||
|
||||
var firstPage = await _repository.FindByTenantAsync("tenant", null, null, cursor: null, limit: 10, CancellationToken.None);
|
||||
firstPage.Select(ls => ls.AdvisoryId).Should().ContainInOrder("ADV-001", "ADV-002", "ADV-003");
|
||||
|
||||
var cursor = new AdvisoryLinksetCursor(firstPage[1].CreatedAt, firstPage[1].AdvisoryId);
|
||||
var secondPage = await _repository.FindByTenantAsync("tenant", null, null, cursor, limit: 10, CancellationToken.None);
|
||||
|
||||
secondPage.Should().ContainSingle();
|
||||
secondPage[0].AdvisoryId.Should().Be("ADV-003");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RoundTrip_PersistsNormalizedConflictsAndProvenance()
|
||||
{
|
||||
var normalized = new AdvisoryLinksetNormalized(
|
||||
Purls: new[] { "pkg:npm/foo@1.0.0" },
|
||||
Cpes: new[] { "cpe:2.3:a:foo:bar:1.0:*:*:*:*:*:*:*" },
|
||||
Versions: new[] { "1.0.0" },
|
||||
Ranges: new[]
|
||||
{
|
||||
new Dictionary<string, object?> { ["type"] = "semver", ["introduced"] = "1.0.0", ["fixed"] = "2.0.0" }
|
||||
},
|
||||
Severities: new[]
|
||||
{
|
||||
new Dictionary<string, object?> { ["system"] = "cvssv3", ["score"] = 7.5 }
|
||||
});
|
||||
|
||||
var conflicts = new List<AdvisoryLinksetConflict>
|
||||
{
|
||||
new("severity", "disagree", new[] { "7.5", "9.8" }, new[] { "nvd", "vendor" })
|
||||
};
|
||||
|
||||
var provenance = new AdvisoryLinksetProvenance(
|
||||
ObservationHashes: new[] { "h1", "h2" },
|
||||
ToolVersion: "lnm-1.0",
|
||||
PolicyHash: "sha256:abc");
|
||||
|
||||
var linkset = new AdvisoryLinkset(
|
||||
TenantId: "tenant-x",
|
||||
Source: "ghsa",
|
||||
AdvisoryId: "GHSA-9999",
|
||||
ObservationIds: ImmutableArray.Create("obs-100"),
|
||||
Normalized: normalized,
|
||||
Provenance: provenance,
|
||||
Confidence: 0.66,
|
||||
Conflicts: conflicts,
|
||||
CreatedAt: DateTimeOffset.Parse("2025-11-20T00:00:00Z"),
|
||||
BuiltByJobId: "job-42");
|
||||
|
||||
await _repository.UpsertAsync(linkset, CancellationToken.None);
|
||||
|
||||
var results = await _repository.FindByTenantAsync("tenant-x", new[] { "GHSA-9999" }, null, cursor: null, limit: 1, CancellationToken.None);
|
||||
results.Should().ContainSingle();
|
||||
|
||||
var cached = results[0];
|
||||
cached.Normalized.Should().NotBeNull();
|
||||
cached.Normalized!.Purls.Should().ContainSingle("pkg:npm/foo@1.0.0");
|
||||
cached.Normalized.Ranges!.Single()["type"].Should().Be("semver");
|
||||
cached.Conflicts.Should().ContainSingle(c => c.Field == "severity" && c.Values!.Contains("9.8"));
|
||||
cached.Provenance!.ObservationHashes.Should().BeEquivalentTo(new[] { "h1", "h2" });
|
||||
cached.BuiltByJobId.Should().Be("job-42");
|
||||
cached.Confidence.Should().Be(0.66);
|
||||
}
|
||||
|
||||
private static AdvisoryLinkset BuildLinkset(
|
||||
string tenant,
|
||||
string source,
|
||||
string advisoryId,
|
||||
IEnumerable<string> observations,
|
||||
DateTimeOffset createdAt,
|
||||
double? confidence)
|
||||
=> new(
|
||||
TenantId: tenant,
|
||||
Source: source,
|
||||
AdvisoryId: advisoryId,
|
||||
ObservationIds: observations.ToImmutableArray(),
|
||||
Normalized: null,
|
||||
Provenance: null,
|
||||
Confidence: confidence,
|
||||
Conflicts: null,
|
||||
CreatedAt: createdAt,
|
||||
BuiltByJobId: "job-1");
|
||||
}
|
||||
Reference in New Issue
Block a user