feat: Implement policy attestation features and service account delegation

- Added new policy scopes: `policy:publish` and `policy:promote` with interactive-only enforcement.
- Introduced metadata parameters for policy actions: `policy_reason`, `policy_ticket`, and `policy_digest`.
- Enhanced token validation to require fresh authentication for policy attestation tokens.
- Updated grant handlers to enforce policy scope checks and log audit information.
- Implemented service account delegation configuration, including quotas and validation.
- Seeded service accounts during application initialization based on configuration.
- Updated documentation and tasks to reflect new features and changes.
This commit is contained in:
master
2025-11-03 01:13:21 +02:00
parent 1d962ee6fc
commit ff0eca3a51
67 changed files with 5198 additions and 214 deletions

View File

@@ -0,0 +1,142 @@
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.AdvisoryAI.Abstractions;
using StellaOps.AdvisoryAI.Chunking;
using StellaOps.AdvisoryAI.Documents;
using StellaOps.AdvisoryAI.Retrievers;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class AdvisoryStructuredRetrieverTests
{
[Fact]
public async Task RetrieveAsync_ReturnsCsafChunksWithMetadata()
{
var provider = CreateProvider(
"test-advisory",
AdvisoryDocument.Create(
"CSA-2024-0001",
DocumentFormat.Csaf,
"csaf",
await LoadAsync("sample-csaf.json")));
var retriever = CreateRetriever(provider);
var result = await retriever.RetrieveAsync(new AdvisoryRetrievalRequest("test-advisory"), CancellationToken.None);
result.Chunks.Should().NotBeEmpty();
result.Chunks.Should().HaveCountGreaterThan(4);
result.Chunks.Select(c => c.ChunkId).Should().BeInAscendingOrder();
result.Chunks.All(c => c.Metadata["format"] == "csaf").Should().BeTrue();
result.Chunks.Any(c => c.Section == "vulnerabilities.remediations").Should().BeTrue();
result.Chunks.Any(c => c.Section == "document.notes").Should().BeTrue();
}
[Fact]
public async Task RetrieveAsync_ReturnsOsvChunksWithAffectedMetadata()
{
var provider = CreateProvider(
"osv-advisory",
AdvisoryDocument.Create(
"OSV-2024-0001",
DocumentFormat.Osv,
"osv",
await LoadAsync("sample-osv.json")));
var retriever = CreateRetriever(provider);
var result = await retriever.RetrieveAsync(new AdvisoryRetrievalRequest("osv-advisory"), CancellationToken.None);
result.Chunks.Should().NotBeEmpty();
result.Chunks.Should().ContainSingle(c => c.Section == "summary");
result.Chunks.Should().Contain(c => c.Section == "affected.ranges");
result.Chunks.First(c => c.Section == "affected.ranges").Metadata.Should().ContainKey("package");
}
[Fact]
public async Task RetrieveAsync_ReturnsOpenVexChunksWithStatusMetadata()
{
var provider = CreateProvider(
"openvex-advisory",
AdvisoryDocument.Create(
"OPENVEX-2024-0001",
DocumentFormat.OpenVex,
"exc-provider",
await LoadAsync("sample-openvex.json")));
var retriever = CreateRetriever(provider);
var result = await retriever.RetrieveAsync(new AdvisoryRetrievalRequest("openvex-advisory"), CancellationToken.None);
result.Chunks.Should().HaveCount(2);
result.Chunks.Select(c => c.Metadata["status"]).Should().Contain(new[] { "not_affected", "affected" });
result.Chunks.First().Metadata.Should().ContainKey("justification");
result.Chunks.Should().AllSatisfy(chunk => chunk.Section.Should().Be("vex.statements"));
}
[Fact]
public async Task RetrieveAsync_FiltersToPreferredSections()
{
var provider = CreateProvider(
"markdown-advisory",
AdvisoryDocument.Create(
"VENDOR-2024-0001",
DocumentFormat.Markdown,
"vendor",
await LoadAsync("sample-vendor.md")));
var retriever = CreateRetriever(provider);
var request = new AdvisoryRetrievalRequest(
"markdown-advisory",
PreferredSections: new[] { "Impact" });
var result = await retriever.RetrieveAsync(request, CancellationToken.None);
result.Chunks.Should().NotBeEmpty();
result.Chunks.Should().OnlyContain(chunk => chunk.Section.StartsWith("Impact", StringComparison.Ordinal));
}
private static AdvisoryStructuredRetriever CreateRetriever(IAdvisoryDocumentProvider provider)
{
var chunkers = new IDocumentChunker[]
{
new CsafDocumentChunker(),
new OsvDocumentChunker(),
new MarkdownDocumentChunker(),
new OpenVexDocumentChunker(),
};
return new AdvisoryStructuredRetriever(provider, chunkers, NullLogger<AdvisoryStructuredRetriever>.Instance);
}
private static async Task<string> LoadAsync(string fileName)
{
var path = Path.Combine(AppContext.BaseDirectory, "TestData", fileName);
return await File.ReadAllTextAsync(path);
}
private static IAdvisoryDocumentProvider CreateProvider(string key, params AdvisoryDocument[] documents)
=> new InMemoryAdvisoryDocumentProvider(new Dictionary<string, IReadOnlyList<AdvisoryDocument>>(StringComparer.Ordinal)
{
[key] = documents,
});
private sealed class InMemoryAdvisoryDocumentProvider : IAdvisoryDocumentProvider
{
private readonly IReadOnlyDictionary<string, IReadOnlyList<AdvisoryDocument>> _documents;
public InMemoryAdvisoryDocumentProvider(IReadOnlyDictionary<string, IReadOnlyList<AdvisoryDocument>> documents)
{
_documents = documents;
}
public Task<IReadOnlyList<AdvisoryDocument>> GetDocumentsAsync(string advisoryKey, CancellationToken cancellationToken)
{
if (_documents.TryGetValue(advisoryKey, out var documents))
{
return Task.FromResult(documents);
}
return Task.FromResult<IReadOnlyList<AdvisoryDocument>>(Array.Empty<AdvisoryDocument>());
}
}
}

View File

@@ -0,0 +1,82 @@
using FluentAssertions;
using StellaOps.AdvisoryAI.Abstractions;
using StellaOps.AdvisoryAI.Chunking;
using StellaOps.AdvisoryAI.Documents;
using StellaOps.AdvisoryAI.Retrievers;
using StellaOps.AdvisoryAI.Vectorization;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class AdvisoryVectorRetrieverTests
{
[Fact]
public async Task SearchAsync_ReturnsBestMatchingChunk()
{
var advisoryContent = """
# Advisory
## Impact
The vulnerability allows remote attackers to execute arbitrary code.
## Remediation
Update to version 2.1.3 or later and restart the service.
""";
var provider = new InMemoryAdvisoryDocumentProvider(new Dictionary<string, IReadOnlyList<AdvisoryDocument>>(StringComparer.Ordinal)
{
["adv"] = new[]
{
AdvisoryDocument.Create("VENDOR-1", DocumentFormat.Markdown, "vendor", advisoryContent)
}
});
var structuredRetriever = new AdvisoryStructuredRetriever(
provider,
new IDocumentChunker[]
{
new CsafDocumentChunker(),
new OsvDocumentChunker(),
new MarkdownDocumentChunker(),
});
using var encoder = new DeterministicHashVectorEncoder();
var vectorRetriever = new AdvisoryVectorRetriever(structuredRetriever, encoder);
var matches = await vectorRetriever.SearchAsync(
new VectorRetrievalRequest(
new AdvisoryRetrievalRequest("adv"),
Query: "How do I remediate the vulnerability?",
TopK: 1),
CancellationToken.None);
matches.Should().HaveCount(1);
matches[0].Section().Should().Be("Remediation");
}
}
file static class VectorRetrievalMatchExtensions
{
public static string Section(this VectorRetrievalMatch match)
=> match.Metadata.TryGetValue("section", out var value) ? value : string.Empty;
}
file sealed class InMemoryAdvisoryDocumentProvider : IAdvisoryDocumentProvider
{
private readonly IReadOnlyDictionary<string, IReadOnlyList<AdvisoryDocument>> _documents;
public InMemoryAdvisoryDocumentProvider(IReadOnlyDictionary<string, IReadOnlyList<AdvisoryDocument>> documents)
{
_documents = documents;
}
public Task<IReadOnlyList<AdvisoryDocument>> GetDocumentsAsync(string advisoryKey, CancellationToken cancellationToken)
{
if (_documents.TryGetValue(advisoryKey, out var documents))
{
return Task.FromResult(documents);
}
return Task.FromResult<IReadOnlyList<AdvisoryDocument>>(Array.Empty<AdvisoryDocument>());
}
}

View File

@@ -0,0 +1,75 @@
using System.Collections.Immutable;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.AdvisoryAI.Providers;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.RawModels;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class ConcelierAdvisoryDocumentProviderTests
{
[Fact]
public async Task GetDocumentsAsync_ReturnsMappedDocuments()
{
var rawDocument = RawDocumentFactory.CreateAdvisory(
tenant: "tenant-a",
source: new RawSourceMetadata("vendor-a", "connector", "1.0"),
upstream: new RawUpstreamMetadata(
"UP-1",
"1",
DateTimeOffset.UtcNow,
"hash-123",
new RawSignatureMetadata(false),
ImmutableDictionary<string, string>.Empty),
content: new RawContent("csaf", "2.0", JsonDocument.Parse("{\"document\": {\"notes\": []}, \"vulnerabilities\": []}").RootElement),
identifiers: new RawIdentifiers(ImmutableArray<string>.Empty, "UP-1"),
linkset: new RawLinkset());
var records = new[]
{
new AdvisoryRawRecord("id-1", rawDocument, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow)
};
var service = new FakeAdvisoryRawService(records);
var provider = new ConcelierAdvisoryDocumentProvider(
service,
Options.Create(new ConcelierAdvisoryDocumentProviderOptions
{
Tenant = "tenant-a",
MaxDocuments = 5,
}),
NullLogger<ConcelierAdvisoryDocumentProvider>.Instance);
var results = await provider.GetDocumentsAsync("UP-1", CancellationToken.None);
results.Should().HaveCount(1);
results[0].Format.Should().Be(Documents.DocumentFormat.Csaf);
results[0].Source.Should().Be("vendor-a");
}
private sealed class FakeAdvisoryRawService : IAdvisoryRawService
{
private readonly IReadOnlyList<AdvisoryRawRecord> _records;
public FakeAdvisoryRawService(IReadOnlyList<AdvisoryRawRecord> records)
{
_records = records;
}
public Task<AdvisoryRawRecord?> FindByIdAsync(string tenant, string id, CancellationToken cancellationToken)
=> Task.FromResult<AdvisoryRawRecord?>(null);
public Task<AdvisoryRawUpsertResult> IngestAsync(AdvisoryRawDocument document, CancellationToken cancellationToken)
=> throw new NotImplementedException();
public Task<AdvisoryRawQueryResult> QueryAsync(AdvisoryRawQueryOptions options, CancellationToken cancellationToken)
=> Task.FromResult(new AdvisoryRawQueryResult(_records, nextCursor: null, hasMore: false));
public Task<AdvisoryRawVerificationResult> VerifyAsync(AdvisoryRawVerificationRequest request, CancellationToken cancellationToken)
=> throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,148 @@
using System.Collections.Immutable;
using System.Text.Json.Nodes;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.AdvisoryAI.Documents;
using StellaOps.AdvisoryAI.Providers;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Observations;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class ExcititorVexDocumentProviderTests
{
[Fact]
public async Task GetDocumentsAsync_ReturnsMappedObservation()
{
const string vulnerabilityId = "CVE-2024-9999";
const string productKey = "product-key";
const string packageUrl = "pkg:docker/sample@1.0.0";
const string cpe = "cpe:/a:sample:service";
const string providerId = "exc-provider";
const string tenantId = "tenant-a";
var observation = CreateObservation(vulnerabilityId, productKey, packageUrl, cpe, providerId, tenantId);
var aggregate = new VexObservationAggregate(
ImmutableArray.Create(vulnerabilityId),
ImmutableArray.Create(productKey),
ImmutableArray.Create(packageUrl),
ImmutableArray.Create(cpe),
ImmutableArray<VexObservationReference>.Empty,
ImmutableArray.Create(providerId));
var queryResult = new VexObservationQueryResult(
ImmutableArray.Create(observation),
aggregate,
NextCursor: null,
HasMore: false);
var service = new FakeVexObservationQueryService(queryResult);
var provider = new ExcititorVexDocumentProvider(
service,
Options.Create(new ExcititorVexDocumentProviderOptions
{
Tenant = tenantId,
MaxObservations = 5,
ProviderIds = ImmutableArray.Create(providerId),
Statuses = ImmutableArray.Create(VexClaimStatus.NotAffected),
}),
NullLogger<ExcititorVexDocumentProvider>.Instance);
var documents = await provider.GetDocumentsAsync(vulnerabilityId, CancellationToken.None);
documents.Should().HaveCount(1);
var document = documents[0];
document.DocumentId.Should().Be("obs-1");
document.Format.Should().Be(DocumentFormat.OpenVex);
document.Source.Should().Be(providerId);
document.Metadata.Should().ContainKey("status_counts");
document.Metadata["status_counts"].Should().Be("not_affected:1");
document.Metadata.Should().ContainKey("aliases");
service.LastOptions.Should().NotBeNull();
service.LastOptions!.Tenant.Should().Be(tenantId);
service.LastOptions.ProviderIds.Should().ContainSingle().Which.Should().Be(providerId);
service.LastOptions.Statuses.Should().ContainSingle(VexClaimStatus.NotAffected);
service.LastOptions.VulnerabilityIds.Should().Contain(vulnerabilityId);
service.LastOptions.Limit.Should().Be(5);
}
private static VexObservation CreateObservation(
string vulnerabilityId,
string productKey,
string packageUrl,
string cpe,
string providerId,
string tenantId)
{
var upstream = new VexObservationUpstream(
"VEX-1",
1,
DateTimeOffset.Parse("2025-10-10T08:00:00Z"),
DateTimeOffset.Parse("2025-10-10T08:05:00Z"),
"hash-abc123",
new VexObservationSignature(true, "dsse", "key-1", "signature"));
var evidence = ImmutableArray.Create<JsonNode>(JsonNode.Parse("{\"note\":\"deterministic\"}")!);
var statement = new VexObservationStatement(
vulnerabilityId,
productKey,
VexClaimStatus.NotAffected,
DateTimeOffset.Parse("2025-10-10T08:00:00Z"),
locator: "selector",
justification: VexJustification.ComponentNotPresent,
introducedVersion: null,
fixedVersion: null,
purl: packageUrl,
cpe: cpe,
evidence: evidence,
metadata: ImmutableDictionary<string, string>.Empty);
var content = new VexObservationContent(
"openvex",
"0.2",
JsonNode.Parse("{\"statements\":[{\"status\":\"not_affected\"}]}")!);
var linkset = new VexObservationLinkset(
aliases: new[] { vulnerabilityId },
purls: new[] { packageUrl },
cpes: new[] { cpe },
references: null);
return new VexObservation(
"obs-1",
tenantId,
providerId,
"default",
upstream,
ImmutableArray.Create(statement),
content,
linkset,
DateTimeOffset.Parse("2025-10-11T09:00:00Z"),
supersedes: ImmutableArray<string>.Empty,
attributes: ImmutableDictionary<string, string>.Empty);
}
private sealed class FakeVexObservationQueryService : IVexObservationQueryService
{
private readonly VexObservationQueryResult _result;
public FakeVexObservationQueryService(VexObservationQueryResult result)
{
_result = result;
}
public VexObservationQueryOptions? LastOptions { get; private set; }
public ValueTask<VexObservationQueryResult> QueryAsync(
VexObservationQueryOptions options,
CancellationToken cancellationToken)
{
LastOptions = options;
return ValueTask.FromResult(_result);
}
}
}

View File

@@ -0,0 +1,39 @@
using FluentAssertions;
using StellaOps.AdvisoryAI.Abstractions;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class SbomContextRequestTests
{
[Fact]
public void Constructor_NormalizesWhitespaceAndLimits()
{
var request = new SbomContextRequest(
artifactId: " scan-42 ",
purl: " pkg:docker/sample@1.2.3 ",
maxTimelineEntries: 600,
maxDependencyPaths: -5,
includeEnvironmentFlags: false,
includeBlastRadius: false);
request.ArtifactId.Should().Be("scan-42");
request.Purl.Should().Be("pkg:docker/sample@1.2.3");
request.MaxTimelineEntries.Should().Be(SbomContextRequest.TimelineLimitCeiling);
request.MaxDependencyPaths.Should().Be(0);
request.IncludeEnvironmentFlags.Should().BeFalse();
request.IncludeBlastRadius.Should().BeFalse();
}
[Fact]
public void Constructor_AllowsNullPurlAndDefaults()
{
var request = new SbomContextRequest(artifactId: "scan-123", purl: null);
request.Purl.Should().BeNull();
request.MaxTimelineEntries.Should().BeGreaterThan(0);
request.MaxDependencyPaths.Should().BeGreaterThan(0);
request.IncludeEnvironmentFlags.Should().BeTrue();
request.IncludeBlastRadius.Should().BeTrue();
}
}

View File

@@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using StellaOps.AdvisoryAI.Abstractions;
using StellaOps.AdvisoryAI.Context;
using StellaOps.AdvisoryAI.Providers;
using StellaOps.AdvisoryAI.Retrievers;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class SbomContextRetrieverTests
{
[Fact]
public async Task RetrieveAsync_ReturnsDeterministicOrderingAndMetadata()
{
var document = new SbomContextDocument(
"artifact-123",
"pkg:docker/sample@1.0.0",
ImmutableArray.Create(
new SbomVersionRecord(
"1.0.1",
new DateTimeOffset(2025, 10, 15, 12, 0, 0, TimeSpan.Zero),
null,
"affected",
"scanner",
false,
ImmutableDictionary<string, string>.Empty),
new SbomVersionRecord(
"1.0.0",
new DateTimeOffset(2025, 9, 10, 8, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2025, 10, 15, 11, 0, 0, TimeSpan.Zero),
"fixed",
"inventory",
true,
ImmutableDictionary<string, string>.Empty)),
ImmutableArray.Create(
new SbomDependencyPathRecord(
ImmutableArray.Create(
new SbomDependencyNodeRecord("app", "1.0.0"),
new SbomDependencyNodeRecord("lib-a", "2.1.3"),
new SbomDependencyNodeRecord("lib-b", "3.4.5")),
true,
"runtime",
ImmutableDictionary<string, string>.Empty),
new SbomDependencyPathRecord(
ImmutableArray.Create(
new SbomDependencyNodeRecord("app", "1.0.0"),
new SbomDependencyNodeRecord("test-helper", "0.9.0")),
false,
"dev",
ImmutableDictionary<string, string>.Empty),
new SbomDependencyPathRecord(
ImmutableArray.Create(
new SbomDependencyNodeRecord("app", "1.0.0"),
new SbomDependencyNodeRecord("lib-a", "2.1.3"),
new SbomDependencyNodeRecord("lib-b", "3.4.5")),
true,
"runtime",
ImmutableDictionary<string, string>.Empty)),
ImmutableDictionary.CreateRange(new[]
{
new KeyValuePair<string, string>("environment/prod", "true"),
new KeyValuePair<string, string>("environment/dev", "false"),
}),
new SbomBlastRadiusRecord(
12,
8,
4,
0.25,
ImmutableDictionary<string, string>.Empty),
ImmutableDictionary<string, string>.Empty);
var client = new FakeSbomContextClient(document);
var retriever = new SbomContextRetriever(client);
var request = new SbomContextRequest(
artifactId: "artifact-123",
purl: "pkg:docker/sample@1.0.0",
maxTimelineEntries: 2,
maxDependencyPaths: 2);
var result = await retriever.RetrieveAsync(request, CancellationToken.None);
result.ArtifactId.Should().Be("artifact-123");
result.Purl.Should().Be("pkg:docker/sample@1.0.0");
result.VersionTimeline.Select(v => v.Version).Should().ContainInOrder("1.0.0", "1.0.1");
result.DependencyPaths.Should().HaveCount(2);
result.DependencyPaths.First().IsRuntime.Should().BeTrue();
result.DependencyPaths.First().Nodes.Select(n => n.Identifier).Should().Equal("app", "lib-a", "lib-b");
result.EnvironmentFlags.Keys.Should().Equal(new[] { "environment/dev", "environment/prod" });
result.EnvironmentFlags["environment/prod"].Should().Be("true");
result.BlastRadius.Should().NotBeNull();
result.BlastRadius!.ImpactedAssets.Should().Be(12);
result.Metadata["version_count"].Should().Be("2");
result.Metadata["dependency_path_count"].Should().Be("2");
result.Metadata["environment_flag_count"].Should().Be("2");
result.Metadata["blast_radius_present"].Should().Be(bool.TrueString);
}
[Fact]
public async Task RetrieveAsync_ReturnsEmptyWhenNoDocument()
{
var client = new FakeSbomContextClient(null);
var retriever = new SbomContextRetriever(client);
var request = new SbomContextRequest("missing-artifact");
var result = await retriever.RetrieveAsync(request, CancellationToken.None);
result.ArtifactId.Should().Be("missing-artifact");
result.VersionTimeline.Should().BeEmpty();
result.DependencyPaths.Should().BeEmpty();
result.EnvironmentFlags.Should().BeEmpty();
result.BlastRadius.Should().BeNull();
}
[Fact]
public async Task RetrieveAsync_HonorsEnvironmentFlagToggle()
{
var document = new SbomContextDocument(
"artifact-flag",
null,
ImmutableArray<SbomVersionRecord>.Empty,
ImmutableArray<SbomDependencyPathRecord>.Empty,
ImmutableDictionary.CreateRange(new[]
{
new KeyValuePair<string, string>("environment/prod", "true"),
}),
blastRadius: null,
metadata: ImmutableDictionary<string, string>.Empty);
var client = new FakeSbomContextClient(document);
var retriever = new SbomContextRetriever(client);
var request = new SbomContextRequest(
artifactId: "artifact-flag",
includeEnvironmentFlags: false,
includeBlastRadius: false);
var result = await retriever.RetrieveAsync(request, CancellationToken.None);
result.EnvironmentFlags.Should().BeEmpty();
result.Metadata["environment_flag_count"].Should().Be("0");
client.LastQuery.Should().NotBeNull();
client.LastQuery!.IncludeEnvironmentFlags.Should().BeFalse();
client.LastQuery.IncludeBlastRadius.Should().BeFalse();
}
[Fact]
public async Task RetrieveAsync_DeduplicatesDependencyPaths()
{
var duplicatePath = ImmutableArray.Create(
new SbomDependencyNodeRecord("app", "1.0.0"),
new SbomDependencyNodeRecord("lib-a", "2.0.0"));
var document = new SbomContextDocument(
"artifact-paths",
null,
ImmutableArray<SbomVersionRecord>.Empty,
ImmutableArray.Create(
new SbomDependencyPathRecord(duplicatePath, true, "runtime", ImmutableDictionary<string, string>.Empty),
new SbomDependencyPathRecord(duplicatePath, true, "runtime", ImmutableDictionary<string, string>.Empty),
new SbomDependencyPathRecord(
ImmutableArray.Create(
new SbomDependencyNodeRecord("app", "1.0.0"),
new SbomDependencyNodeRecord("dev-tool", "0.1.0")),
false,
"dev",
ImmutableDictionary<string, string>.Empty)),
ImmutableDictionary<string, string>.Empty,
blastRadius: null,
metadata: ImmutableDictionary<string, string>.Empty);
var client = new FakeSbomContextClient(document);
var retriever = new SbomContextRetriever(client);
var request = new SbomContextRequest(
artifactId: "artifact-paths",
maxDependencyPaths: 5);
var result = await retriever.RetrieveAsync(request, CancellationToken.None);
result.DependencyPaths.Should().HaveCount(2);
result.DependencyPaths.First().IsRuntime.Should().BeTrue();
result.DependencyPaths.Last().IsRuntime.Should().BeFalse();
result.Metadata["dependency_path_count"].Should().Be("2");
}
private sealed class FakeSbomContextClient : ISbomContextClient
{
private readonly SbomContextDocument? _document;
public FakeSbomContextClient(SbomContextDocument? document)
{
_document = document;
}
public SbomContextQuery? LastQuery { get; private set; }
public Task<SbomContextDocument?> GetContextAsync(SbomContextQuery query, CancellationToken cancellationToken)
{
LastQuery = query;
return Task.FromResult(_document);
}
}
}

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
<ProjectReference Include="..\..\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="TestData/*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData/*.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,43 @@
{
"document": {
"tracking": {
"id": "CSA-2024-0001"
},
"notes": [
{
"category": "summary",
"text": "The vendor has published guidance for CVE-2024-1234."
},
{
"category": "description",
"text": "Additional context for operators."
},
{
"category": "other",
"text": "This note should be ignored."
}
]
},
"vulnerabilities": [
{
"id": "CVE-2024-1234",
"title": "Important vulnerability in component",
"description": "Remote attackers may execute arbitrary code.",
"notes": [
{
"category": "description",
"text": "Applies to installations using default configuration."
}
],
"remediations": [
{
"category": "mitigation",
"details": "Apply patch level QFE-2024-11 or later.",
"product_ids": [
"pkg:deb/debian/component@1.2.3"
]
}
]
}
]
}

View File

@@ -0,0 +1,30 @@
{
"openvex": "https://openvex.dev/ns/v0.2",
"timestamp": "2025-10-15T12:00:00Z",
"version": "1",
"statements": [
{
"vulnerability": "CVE-2024-9999",
"products": [
{
"product_id": "pkg:docker/sample@1.0.0"
}
],
"status": "not_affected",
"justification": "component_not_present",
"impact_statement": "Component not shipped",
"status_notes": "Distribution excludes this component",
"timestamp": "2025-10-10T08:00:00Z",
"last_updated": "2025-10-11T09:00:00Z"
},
{
"vulnerability": "CVE-2024-8888",
"products": [
"component://sample/service"
],
"status": "affected",
"status_notes": "Patch scheduled",
"timestamp": "2025-10-12T13:30:00Z"
}
]
}

View File

@@ -0,0 +1,33 @@
{
"id": "OSV-2024-0001",
"summary": "Vulnerability in package affects multiple versions.",
"details": "Remote attackers may exploit the issue under specific conditions.",
"affected": [
{
"package": {
"name": "example",
"ecosystem": "npm"
},
"ranges": [
{
"type": "SEMVER",
"events": [
{
"introduced": "0"
},
{
"fixed": "1.2.3"
}
]
}
],
"versions": ["0.9.0", "1.0.0"]
}
],
"references": [
{
"type": "ADVISORY",
"url": "https://example.org/advisory"
}
]
}

View File

@@ -0,0 +1,12 @@
# Vendor Advisory 2024-01
Initial notice describing the vulnerability and affected platforms.
## Impact
End-users may experience remote compromise when default credentials are unchanged.
## Remediation
Apply hotfix package 2024.11.5 and rotate secrets within 24 hours.
## References
- https://vendor.example.com/advisories/2024-01