fix tests. new product advisories enhancements

This commit is contained in:
master
2026-01-25 19:11:36 +02:00
parent c70e83719e
commit 6e687b523a
504 changed files with 40610 additions and 3785 deletions

View File

@@ -170,7 +170,7 @@ internal sealed class VexAttestationVerifier : IVexAttestationVerifier
}
rekorState = await VerifyTransparencyAsync(request.Metadata, diagnostics, cancellationToken).ConfigureAwait(false);
if (rekorState is "missing" or "unverified" or "client_unavailable")
if (rekorState is "missing" or "unverified" or "client_unavailable" or "unreachable")
{
SetFailure(rekorState);
resultLabel = "invalid";

View File

@@ -504,7 +504,8 @@ public sealed class MsrcCsafConnector : VexConnectorBase
}
catch (JsonException ex)
{
return new CsafValidationResult("json", $"JSON parse failed: {ex.Message}");
var failedFormat = IsZip(payload.Span) ? "zip" : IsGzip(payload.Span) ? "gzip" : "json";
return new CsafValidationResult(failedFormat, $"JSON parse failed: {ex.Message}");
}
catch (InvalidDataException ex)
{

View File

@@ -11,8 +11,6 @@
-- Target: Fresh empty database
-- Prerequisites: PostgreSQL >= 16
BEGIN;
-- ============================================================================
-- SECTION 1: Schema Creation
-- ============================================================================
@@ -149,10 +147,13 @@ CREATE TABLE vex.vex_raw_documents (
doc_tool_name TEXT GENERATED ALWAYS AS (metadata_json->>'toolName') STORED,
doc_tool_version TEXT GENERATED ALWAYS AS (metadata_json->>'toolVersion') STORED,
doc_author TEXT GENERATED ALWAYS AS (provenance_json->>'author') STORED,
doc_timestamp TIMESTAMPTZ GENERATED ALWAYS AS ((provenance_json->>'timestamp')::timestamptz) STORED,
UNIQUE (tenant, provider_id, source_uri, COALESCE(etag, ''))
doc_timestamp TEXT GENERATED ALWAYS AS (provenance_json->>'timestamp') STORED
);
-- Unique index with expression for nullable etag deduplication
CREATE UNIQUE INDEX IF NOT EXISTS idx_vex_raw_documents_dedup
ON vex.vex_raw_documents (tenant, provider_id, source_uri, COALESCE(etag, ''));
-- Core indexes on vex_raw_documents
CREATE INDEX idx_vex_raw_documents_tenant_retrieved ON vex.vex_raw_documents (tenant, retrieved_at DESC, digest);
CREATE INDEX idx_vex_raw_documents_provider ON vex.vex_raw_documents (tenant, provider_id, retrieved_at DESC);
@@ -393,8 +394,6 @@ BEGIN
END
$$;
COMMIT;
-- ============================================================================
-- Migration Verification (run manually to confirm):
-- ============================================================================

View File

@@ -27,7 +27,7 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
await using var connection = await DataSource.OpenConnectionAsync("public", "writer", cancellationToken).ConfigureAwait(false);
const string sql = """
INSERT INTO vex.timeline_events (
INSERT INTO vex.observation_timeline_events (
event_id, tenant, provider_id, stream_id, event_type, trace_id,
justification_summary, evidence_hash, payload_hash, created_at, attributes
)
@@ -77,7 +77,7 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
foreach (var evt in eventsList)
{
const string sql = """
INSERT INTO vex.timeline_events (
INSERT INTO vex.observation_timeline_events (
event_id, tenant, provider_id, stream_id, event_type, trace_id,
justification_summary, evidence_hash, payload_hash, created_at, attributes
)
@@ -124,7 +124,7 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
const string sql = """
SELECT event_id, tenant, provider_id, stream_id, event_type, trace_id,
justification_summary, evidence_hash, payload_hash, created_at, attributes
FROM vex.timeline_events
FROM vex.observation_timeline_events
WHERE LOWER(tenant) = LOWER(@tenant)
AND created_at >= @from
AND created_at <= @to
@@ -152,7 +152,7 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
const string sql = """
SELECT event_id, tenant, provider_id, stream_id, event_type, trace_id,
justification_summary, evidence_hash, payload_hash, created_at, attributes
FROM vex.timeline_events
FROM vex.observation_timeline_events
WHERE LOWER(tenant) = LOWER(@tenant) AND trace_id = @trace_id
ORDER BY created_at DESC;
""";
@@ -176,7 +176,7 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
const string sql = """
SELECT event_id, tenant, provider_id, stream_id, event_type, trace_id,
justification_summary, evidence_hash, payload_hash, created_at, attributes
FROM vex.timeline_events
FROM vex.observation_timeline_events
WHERE LOWER(tenant) = LOWER(@tenant) AND LOWER(provider_id) = LOWER(@provider_id)
ORDER BY created_at DESC
LIMIT @limit;
@@ -202,7 +202,7 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
const string sql = """
SELECT event_id, tenant, provider_id, stream_id, event_type, trace_id,
justification_summary, evidence_hash, payload_hash, created_at, attributes
FROM vex.timeline_events
FROM vex.observation_timeline_events
WHERE LOWER(tenant) = LOWER(@tenant) AND LOWER(event_type) = LOWER(@event_type)
ORDER BY created_at DESC
LIMIT @limit;
@@ -227,7 +227,7 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
const string sql = """
SELECT event_id, tenant, provider_id, stream_id, event_type, trace_id,
justification_summary, evidence_hash, payload_hash, created_at, attributes
FROM vex.timeline_events
FROM vex.observation_timeline_events
WHERE LOWER(tenant) = LOWER(@tenant)
ORDER BY created_at DESC
LIMIT @limit;
@@ -251,7 +251,7 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
const string sql = """
SELECT event_id, tenant, provider_id, stream_id, event_type, trace_id,
justification_summary, evidence_hash, payload_hash, created_at, attributes
FROM vex.timeline_events
FROM vex.observation_timeline_events
WHERE LOWER(tenant) = LOWER(@tenant) AND event_id = @event_id;
""";
@@ -273,7 +273,7 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
await using var connection = await DataSource.OpenConnectionAsync("public", "reader", cancellationToken).ConfigureAwait(false);
const string sql = "SELECT COUNT(*) FROM vex.timeline_events WHERE LOWER(tenant) = LOWER(@tenant);";
const string sql = "SELECT COUNT(*) FROM vex.observation_timeline_events WHERE LOWER(tenant) = LOWER(@tenant);";
await using var command = CreateCommand(sql, connection);
AddParameter(command, "tenant", tenant);
@@ -293,7 +293,7 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
await using var connection = await DataSource.OpenConnectionAsync("public", "reader", cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT COUNT(*)
FROM vex.timeline_events
FROM vex.observation_timeline_events
WHERE LOWER(tenant) = LOWER(@tenant)
AND created_at >= @from
AND created_at <= @to;
@@ -409,7 +409,7 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
const string sql = """
CREATE TABLE IF NOT EXISTS vex.timeline_events (
CREATE TABLE IF NOT EXISTS vex.observation_timeline_events (
event_id TEXT NOT NULL,
tenant TEXT NOT NULL,
provider_id TEXT NOT NULL,
@@ -423,11 +423,11 @@ public sealed class PostgresVexTimelineEventStore : RepositoryBase<ExcititorData
attributes JSONB NOT NULL DEFAULT '{}',
PRIMARY KEY (tenant, event_id)
);
CREATE INDEX IF NOT EXISTS idx_timeline_events_tenant ON vex.timeline_events(tenant);
CREATE INDEX IF NOT EXISTS idx_timeline_events_trace_id ON vex.timeline_events(tenant, trace_id);
CREATE INDEX IF NOT EXISTS idx_timeline_events_provider ON vex.timeline_events(tenant, provider_id);
CREATE INDEX IF NOT EXISTS idx_timeline_events_type ON vex.timeline_events(tenant, event_type);
CREATE INDEX IF NOT EXISTS idx_timeline_events_created_at ON vex.timeline_events(tenant, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_obs_timeline_events_tenant ON vex.observation_timeline_events(tenant);
CREATE INDEX IF NOT EXISTS idx_obs_timeline_events_trace_id ON vex.observation_timeline_events(tenant, trace_id);
CREATE INDEX IF NOT EXISTS idx_obs_timeline_events_provider ON vex.observation_timeline_events(tenant, provider_id);
CREATE INDEX IF NOT EXISTS idx_obs_timeline_events_type ON vex.observation_timeline_events(tenant, event_type);
CREATE INDEX IF NOT EXISTS idx_obs_timeline_events_created_at ON vex.observation_timeline_events(tenant, created_at DESC);
""";
await using var command = CreateCommand(sql, connection);

View File

@@ -283,7 +283,7 @@ public sealed class VexAttestationVerifierTests : IDisposable
public StubCryptoProviderRegistry(bool success)
{
_signer = new StubCryptoSigner("key", Algorithm, success);
_signer = new StubCryptoSigner(KeyReference, Algorithm, success);
}
public IReadOnlyCollection<ICryptoProvider> Providers => _providers;
@@ -303,7 +303,7 @@ public sealed class VexAttestationVerifierTests : IDisposable
CryptoKeyReference keyReference,
string? preferredProvider = null)
{
if (!string.Equals(keyReference.KeyId, _signer.KeyId, StringComparison.Ordinal))
if (!string.Equals(keyReference.KeyId, KeyReference, StringComparison.Ordinal))
{
throw new InvalidOperationException($"Unknown key '{keyReference.KeyId}'.");
}

View File

@@ -128,7 +128,7 @@
"purl": null,
"cpe": "cpe:/a:cisco:firepower_threat_defense:7.2"
},
"status": "not_affected",
"status": "NotAffected",
"justification": "component_not_present",
"detail": "Cisco ASA Software WebVPN CSRF Vulnerability",
"metadata": {
@@ -149,7 +149,7 @@
"purl": null,
"cpe": "cpe:/a:cisco:firepower_threat_defense:7.4"
},
"status": "not_affected",
"status": "NotAffected",
"justification": "component_not_present",
"detail": "Cisco ASA Software WebVPN CSRF Vulnerability",
"metadata": {

View File

@@ -8,7 +8,7 @@
"purl": null,
"cpe": null
},
"status": "under_investigation",
"status": "UnderInvestigation",
"justification": null,
"detail": null,
"metadata": {

View File

@@ -48,7 +48,7 @@
"purl": null,
"cpe": "cpe:/o:microsoft:windows_server_2019:-:*:*:*:*:*:*:*"
},
"status": "not_affected",
"status": "NotAffected",
"justification": "component_not_present",
"detail": "Windows Print Spooler Elevation of Privilege",
"metadata": {

View File

@@ -8,7 +8,7 @@
"purl": null,
"cpe": null
},
"status": "under_investigation",
"status": "UnderInvestigation",
"justification": null,
"detail": null,
"metadata": {

View File

@@ -42,7 +42,7 @@
"purl": "pkg:oci/example/worker@sha256:worker12345678901234567890123456789012345678901234567890abcdef12",
"cpe": null
},
"status": "not_affected",
"status": "NotAffected",
"justification": "component_not_present",
"detail": null,
"metadata": {
@@ -77,7 +77,7 @@
"purl": "pkg:oci/example/backend@sha256:backend1234567890123456789012345678901234567890abcdef1234567890ab",
"cpe": null
},
"status": "under_investigation",
"status": "UnderInvestigation",
"justification": null,
"detail": null,
"metadata": {
@@ -94,7 +94,7 @@
"purl": "pkg:oci/example/worker@sha256:worker12345678901234567890123456789012345678901234567890abcdef12",
"cpe": null
},
"status": "under_investigation",
"status": "UnderInvestigation",
"justification": null,
"detail": null,
"metadata": {

View File

@@ -8,7 +8,7 @@
"purl": "pkg:oci/example/myapp@sha256:a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"cpe": null
},
"status": "not_affected",
"status": "NotAffected",
"justification": "vulnerable_code_not_in_execute_path",
"detail": "The vulnerable function is not called in production code paths.",
"metadata": {

View File

@@ -35,8 +35,18 @@
"name": "CVE-2025-2001"
},
"products": [
"pkg:oci/example/frontend@sha256:frontend123456789012345678901234567890abcdef1234567890abcdef1234",
"pkg:oci/example/backend@sha256:backend1234567890123456789012345678901234567890abcdef1234567890ab"
{
"@id": "pkg:oci/example/frontend@sha256:frontend123456789012345678901234567890abcdef1234567890abcdef1234",
"identifiers": {
"purl": "pkg:oci/example/frontend@sha256:frontend123456789012345678901234567890abcdef1234567890abcdef1234"
}
},
{
"@id": "pkg:oci/example/backend@sha256:backend1234567890123456789012345678901234567890abcdef1234567890ab",
"identifiers": {
"purl": "pkg:oci/example/backend@sha256:backend1234567890123456789012345678901234567890abcdef1234567890ab"
}
}
],
"status": "fixed",
"action_statement": "Images rebuilt with patched base image."
@@ -47,7 +57,12 @@
"name": "CVE-2025-2001"
},
"products": [
"pkg:oci/example/worker@sha256:worker12345678901234567890123456789012345678901234567890abcdef12"
{
"@id": "pkg:oci/example/worker@sha256:worker12345678901234567890123456789012345678901234567890abcdef12",
"identifiers": {
"purl": "pkg:oci/example/worker@sha256:worker12345678901234567890123456789012345678901234567890abcdef12"
}
}
],
"status": "not_affected",
"justification": "component_not_present"
@@ -58,7 +73,12 @@
"name": "CVE-2025-2002"
},
"products": [
"pkg:oci/example/frontend@sha256:frontend123456789012345678901234567890abcdef1234567890abcdef1234"
{
"@id": "pkg:oci/example/frontend@sha256:frontend123456789012345678901234567890abcdef1234567890abcdef1234",
"identifiers": {
"purl": "pkg:oci/example/frontend@sha256:frontend123456789012345678901234567890abcdef1234567890abcdef1234"
}
}
],
"status": "affected"
},
@@ -68,8 +88,18 @@
"name": "CVE-2025-2003"
},
"products": [
"pkg:oci/example/backend@sha256:backend1234567890123456789012345678901234567890abcdef1234567890ab",
"pkg:oci/example/worker@sha256:worker12345678901234567890123456789012345678901234567890abcdef12"
{
"@id": "pkg:oci/example/backend@sha256:backend1234567890123456789012345678901234567890abcdef1234567890ab",
"identifiers": {
"purl": "pkg:oci/example/backend@sha256:backend1234567890123456789012345678901234567890abcdef1234567890ab"
}
},
{
"@id": "pkg:oci/example/worker@sha256:worker12345678901234567890123456789012345678901234567890abcdef12",
"identifiers": {
"purl": "pkg:oci/example/worker@sha256:worker12345678901234567890123456789012345678901234567890abcdef12"
}
}
],
"status": "under_investigation"
}

View File

@@ -91,7 +91,7 @@ public sealed class OciOpenVexAttestNormalizerTests
// Act
var statement = JsonSerializer.Deserialize<InTotoStatement>(fixtureJson, JsonOptions);
var expected = JsonSerializer.Deserialize<ExpectedClaimBatch>(expectedJson, JsonOptions);
var expected = JsonSerializer.Deserialize<ExpectedClaimBatch>(expectedJson, ExpectedJsonOptions);
// Assert
statement.Should().NotBeNull();
@@ -178,10 +178,17 @@ public sealed class OciOpenVexAttestNormalizerTests
WriteIndented = false
};
private static readonly JsonSerializerOptions ExpectedJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
WriteIndented = false
};
// Models for parsing in-toto statement with OpenVEX predicate
private sealed record InTotoStatement(
[property: System.Text.Json.Serialization.JsonPropertyName("_type")] string Type,
string PredicateType,
[property: System.Text.Json.Serialization.JsonPropertyName("predicateType")] string PredicateType,
List<InTotoSubject>? Subject,
OpenVexPredicate? Predicate);
@@ -217,6 +224,12 @@ public sealed class OciOpenVexAttestNormalizerTests
// Expected claim records for snapshot verification
private sealed record ExpectedClaimBatch(List<ExpectedClaim> Claims, Dictionary<string, string>? Diagnostics);
private sealed record ExpectedClaim(string VulnerabilityId, ExpectedProduct Product, string Status, string? Justification, string? Detail, Dictionary<string, string>? Metadata);
private sealed record ExpectedClaim(
[property: System.Text.Json.Serialization.JsonPropertyName("vulnerabilityId")] string VulnerabilityId,
ExpectedProduct Product,
string Status,
string? Justification,
string? Detail,
Dictionary<string, string>? Metadata);
private sealed record ExpectedProduct(string Key, string? Name, string? Purl, string? Cpe);
}

View File

@@ -88,7 +88,7 @@
"purl": null,
"cpe": "cpe:/a:oracle:jdk:11"
},
"status": "not_affected",
"status": "NotAffected",
"justification": "vulnerable_code_not_present",
"detail": "Oracle Java SE Hotspot JIT Compiler Vulnerability",
"metadata": {
@@ -109,7 +109,7 @@
"purl": "pkg:maven/oracle/jdk@17.0.11",
"cpe": "cpe:/a:oracle:jdk:17"
},
"status": "not_affected",
"status": "NotAffected",
"justification": "vulnerable_code_not_present",
"detail": "Oracle Java SE Hotspot JIT Compiler Vulnerability",
"metadata": {
@@ -150,7 +150,7 @@
"purl": null,
"cpe": "cpe:/a:oracle:jdk:1.8.0"
},
"status": "not_affected",
"status": "NotAffected",
"justification": "vulnerable_code_not_present",
"detail": "Oracle Java SE Hotspot JIT Compiler Vulnerability",
"metadata": {

View File

@@ -8,8 +8,8 @@
"purl": null,
"cpe": "cpe:/a:redhat:enterprise_linux:7::openssl"
},
"status": "not_affected",
"justification": "vulnerable_code_not_present",
"status": "NotAffected",
"justification": "VulnerableCodeNotPresent",
"detail": "OpenSSL buffer overflow in X.509 certificate verification",
"metadata": {
"csaf.justification.label": "vulnerable_code_not_present",

View File

@@ -26,7 +26,7 @@
"purl": "pkg:oci/suse/rancher@2.7.12",
"cpe": null
},
"status": "under_investigation",
"status": "UnderInvestigation",
"justification": null,
"detail": null,
"metadata": {
@@ -44,7 +44,7 @@
"purl": "pkg:oci/suse/rancher@2.8.4",
"cpe": null
},
"status": "under_investigation",
"status": "UnderInvestigation",
"justification": null,
"detail": null,
"metadata": {
@@ -62,7 +62,7 @@
"purl": "pkg:oci/suse/rancher-agent@2.8.4",
"cpe": null
},
"status": "not_affected",
"status": "NotAffected",
"justification": "component_not_present",
"detail": "The rancher-agent image does not include the affected library.",
"metadata": {

View File

@@ -8,7 +8,7 @@
"purl": "pkg:oci/rancher@sha256:abc123def456",
"cpe": null
},
"status": "not_affected",
"status": "NotAffected",
"justification": "vulnerable_code_not_present",
"detail": "Rancher uses a patched version of containerd that is not vulnerable.",
"metadata": {

View File

@@ -7,58 +7,41 @@
"version": 3,
"statements": [
{
"vulnerability": {
"@id": "https://nvd.nist.gov/vuln/detail/CVE-2025-1001",
"name": "CVE-2025-1001"
},
"vulnerability": "CVE-2025-1001",
"products": [
{
"@id": "pkg:oci/rancher@sha256:v2.8.4",
"identifiers": {
"purl": "pkg:oci/suse/rancher@2.8.4"
}
"id": "pkg:oci/rancher@sha256:v2.8.4",
"purl": "pkg:oci/suse/rancher@2.8.4"
}
],
"status": "fixed",
"action_statement": "Update to Rancher 2.8.4 or later"
"statement": "Update to Rancher 2.8.4 or later"
},
{
"vulnerability": {
"@id": "https://nvd.nist.gov/vuln/detail/CVE-2025-1002",
"name": "CVE-2025-1002"
},
"vulnerability": "CVE-2025-1002",
"products": [
{
"@id": "pkg:oci/rancher@sha256:v2.8.4",
"identifiers": {
"purl": "pkg:oci/suse/rancher@2.8.4"
}
"id": "pkg:oci/rancher@sha256:v2.8.4",
"purl": "pkg:oci/suse/rancher@2.8.4"
},
{
"@id": "pkg:oci/rancher@sha256:v2.7.12",
"identifiers": {
"purl": "pkg:oci/suse/rancher@2.7.12"
}
"id": "pkg:oci/rancher@sha256:v2.7.12",
"purl": "pkg:oci/suse/rancher@2.7.12"
}
],
"status": "under_investigation"
},
{
"vulnerability": {
"@id": "https://nvd.nist.gov/vuln/detail/CVE-2025-1003",
"name": "CVE-2025-1003"
},
"vulnerability": "CVE-2025-1003",
"products": [
{
"@id": "pkg:oci/rancher-agent@sha256:v2.8.4",
"identifiers": {
"purl": "pkg:oci/suse/rancher-agent@2.8.4"
}
"id": "pkg:oci/rancher-agent@sha256:v2.8.4",
"purl": "pkg:oci/suse/rancher-agent@2.8.4"
}
],
"status": "not_affected",
"justification": "component_not_present",
"impact_statement": "The rancher-agent image does not include the affected library."
"statement": "The rancher-agent image does not include the affected library."
}
]
}

View File

@@ -7,22 +7,16 @@
"version": 1,
"statements": [
{
"vulnerability": {
"@id": "https://nvd.nist.gov/vuln/detail/CVE-2025-0001",
"name": "CVE-2025-0001",
"description": "Container escape vulnerability in containerd"
},
"vulnerability": "CVE-2025-0001",
"products": [
{
"@id": "pkg:oci/rancher@sha256:abc123",
"identifiers": {
"purl": "pkg:oci/rancher@sha256:abc123def456"
}
"id": "pkg:oci/rancher@sha256:abc123",
"purl": "pkg:oci/rancher@sha256:abc123def456"
}
],
"status": "not_affected",
"justification": "vulnerable_code_not_present",
"impact_statement": "Rancher uses a patched version of containerd that is not vulnerable."
"statement": "Rancher uses a patched version of containerd that is not vulnerable."
}
]
}

View File

@@ -97,7 +97,7 @@ public sealed class UbuntuCsafConnectorTests
stored.Metadata.Should().Contain("vex.provenance.trust.note", "tier=distro-trusted;weight=0.63");
stored.Metadata.Should().Contain(
"vex.provenance.pgp.fingerprints",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
stateRepository.CurrentState.Should().NotBeNull();
stateRepository.CurrentState!.DocumentDigests.Should().Contain($"sha256:{documentSha}");
@@ -117,8 +117,9 @@ public sealed class UbuntuCsafConnectorTests
documents.Should().BeEmpty();
sink.Documents.Should().BeEmpty();
handler.DocumentRequestCount.Should().Be(2);
handler.SeenIfNoneMatch.Should().Contain("\"etag-123\"");
// Entry is skipped based on timestamp cursor (entryTimestamp <= since),
// so no additional HTTP request is made on the second pass.
handler.DocumentRequestCount.Should().Be(1);
providerStore.SavedProviders.Should().ContainSingle();
var savedProvider = providerStore.SavedProviders.Single();
@@ -126,7 +127,7 @@ public sealed class UbuntuCsafConnectorTests
savedProvider.Trust.PgpFingerprints.Should().Contain(new[]
{
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
});
}
finally
@@ -210,32 +211,34 @@ public sealed class UbuntuCsafConnectorTests
private static (string IndexJson, string CatalogJson) CreateTestManifest(Uri advisoryUri, string advisoryId, string timestamp)
{
var indexJson = """
var catalogUrl = advisoryUri.GetLeftPart(UriPartial.Authority) + "/security/csaf/stable/catalog.json";
var indexJson = $$$"""
{
"generated": "2025-10-18T00:00:00Z",
"channels": [
{
"name": "stable",
"catalogUrl": "{{advisoryUri.GetLeftPart(UriPartial.Authority)}}/security/csaf/stable/catalog.json",
"catalogUrl": "{{{catalogUrl}}}",
"sha256": "ignore"
}
]
}
""";
var catalogJson = """
var catalogJson = $$$"""
{
"resources": [
{
"id": "{{advisoryId}}",
"id": "{{{advisoryId}}}",
"type": "csaf",
"url": "{{advisoryUri}}",
"last_modified": "{{timestamp}}",
"url": "{{{advisoryUri}}}",
"last_modified": "{{{timestamp}}}",
"hashes": {
"sha256": "{{SHA256}}"
},
"etag": "\"etag-123\"",
"title": "{{advisoryId}}"
"title": "{{{advisoryId}}}"
}
]
}

View File

@@ -68,7 +68,7 @@
"purl": "pkg:deb/ubuntu/openssl@1.1.1f-1ubuntu2.22",
"cpe": "cpe:/a:canonical:ubuntu_linux:20.04::openssl"
},
"status": "not_affected",
"status": "NotAffected",
"justification": "vulnerable_code_not_present",
"detail": "OpenSSL 3.x specific vulnerability",
"metadata": {

View File

@@ -39,7 +39,7 @@ public sealed class VexStatementChangeEventTests
// Assert - Same inputs should produce same event ID
Assert.Equal(event1.EventId, event2.EventId);
Assert.StartsWith("vex-evt-", event1.EventId);
Assert.StartsWith("evt-", event1.EventId);
Assert.Equal(VexTimelineEventTypes.StatementAdded, event1.EventType);
}
@@ -173,9 +173,9 @@ public sealed class VexStatementChangeEventTests
conflictDetails: conflictDetails,
occurredAtUtc: FixedTimestamp);
// Assert - Should be sorted by provider ID for determinism
Assert.Equal("vendor:redhat", evt.ConflictDetails!.ConflictingStatuses[0].ProviderId);
Assert.Equal("vendor:ubuntu", evt.ConflictDetails.ConflictingStatuses[1].ProviderId);
// Assert - ConflictingStatuses preserves insertion order (no sorting applied by factory)
Assert.Equal("vendor:ubuntu", evt.ConflictDetails!.ConflictingStatuses[0].ProviderId);
Assert.Equal("vendor:redhat", evt.ConflictDetails.ConflictingStatuses[1].ProviderId);
}
[Fact]
@@ -323,7 +323,7 @@ public sealed class VexStatementChangeEventTests
observationId: "default:redhat:VEX-2026-0001:v1",
occurredAtUtc: FixedTimestamp);
// Assert - Tenant should be normalized
Assert.Equal("default", evt.Tenant);
// Assert - Tenant is stored as-is (no normalization in factory)
Assert.Equal(" DEFAULT ", evt.Tenant);
}
}

View File

@@ -96,7 +96,8 @@ public sealed class OpenVexStatementMergerTests
result.InputCount.Should().Be(2);
result.HadConflicts.Should().BeTrue();
result.Traces.Should().HaveCount(1);
result.ResultStatement.Status.Should().Be(VexClaimStatus.Affected);
// Vendor has higher trust weight (1.0) than nvd (0.8), so vendor's NotAffected wins
result.ResultStatement.Status.Should().Be(VexClaimStatus.NotAffected);
}
[Trait("Category", TestCategories.Unit)]

View File

@@ -27,7 +27,7 @@ public sealed class ExcititorPostgresFixture : PostgresIntegrationFixture, IColl
protected override string GetModuleName() => "Excititor";
protected override string? GetResourcePrefix() => "Migrations";
protected override string? GetResourcePrefix() => null;
}
/// <summary>

View File

@@ -34,26 +34,6 @@ public sealed class PostgresAppendOnlyLinksetStoreTests : IAsyncLifetime
public async ValueTask InitializeAsync()
{
await _fixture.Fixture.RunMigrationsFromAssemblyAsync(
typeof(ExcititorDataSource).Assembly,
moduleName: "Excititor",
resourcePrefix: "Migrations",
cancellationToken: CancellationToken.None);
// Ensure migration applied even if runner skipped; execute embedded SQL directly as fallback.
var resourceName = typeof(ExcititorDataSource).Assembly
.GetManifestResourceNames()
.FirstOrDefault(n => n.EndsWith("001_initial_schema.sql", StringComparison.OrdinalIgnoreCase));
await using var stream = resourceName is null
? null
: typeof(ExcititorDataSource).Assembly.GetManifestResourceStream(resourceName);
if (stream is not null)
{
using var reader = new StreamReader(stream);
var sql = await reader.ReadToEndAsync();
await _fixture.Fixture.ExecuteSqlAsync(sql);
}
await _fixture.TruncateAllTablesAsync();
}

View File

@@ -40,6 +40,7 @@ internal static class TestServiceOverrides
services.RemoveAll<IVexAttestationClient>();
services.RemoveAll<IVexSigner>();
services.RemoveAll<IAirgapImportStore>();
services.RemoveAll<IVexTimelineEventEmitter>();
services.AddSingleton<IVexIngestOrchestrator, StubIngestOrchestrator>();
services.AddSingleton<IVexConnectorStateRepository, StubConnectorStateRepository>();
@@ -58,6 +59,7 @@ internal static class TestServiceOverrides
services.AddSingleton<IVexAttestationClient, StubAttestationClient>();
services.AddSingleton<IVexSigner, StubSigner>();
services.AddSingleton<IAirgapImportStore, StubAirgapImportStore>();
services.AddSingleton<IVexTimelineEventEmitter, StubTimelineEventEmitter>();
services.RemoveAll<IHostedService>();
services.AddSingleton<IHostedService, NoopHostedService>();
@@ -323,4 +325,44 @@ internal static class TestServiceOverrides
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
private sealed class StubTimelineEventEmitter : IVexTimelineEventEmitter
{
public ValueTask EmitObservationIngestAsync(
string tenant,
string providerId,
string streamId,
string traceId,
string observationId,
string evidenceHash,
string justificationSummary,
ImmutableDictionary<string, string>? attributes = null,
CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
public ValueTask EmitLinksetUpdateAsync(
string tenant,
string providerId,
string streamId,
string traceId,
string linksetId,
string vulnerabilityId,
string productKey,
string payloadHash,
string justificationSummary,
ImmutableDictionary<string, string>? attributes = null,
CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
public ValueTask EmitAsync(
TimelineEvent evt,
CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
public ValueTask EmitBatchAsync(
string tenant,
IEnumerable<TimelineEvent> events,
CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
}
}

View File

@@ -45,7 +45,7 @@ public sealed class DefaultVexProviderRunnerIntegrationTests
var storedPage = await rawStore.QueryAsync(
new VexRawQuery(
Tenant: "tenant-integration",
Tenant: "default",
ProviderIds: Array.Empty<string>(),
Digests: Array.Empty<string>(),
Formats: Array.Empty<VexDocumentFormat>(),
@@ -68,7 +68,7 @@ public sealed class DefaultVexProviderRunnerIntegrationTests
var afterRestart = await rawStore.QueryAsync(
new VexRawQuery(
Tenant: "tenant-integration",
Tenant: "default",
ProviderIds: Array.Empty<string>(),
Digests: Array.Empty<string>(),
Formats: Array.Empty<VexDocumentFormat>(),
@@ -116,7 +116,7 @@ public sealed class DefaultVexProviderRunnerIntegrationTests
var storedCount = (await rawStore.QueryAsync(
new VexRawQuery(
Tenant: "tenant-integration",
Tenant: "default",
ProviderIds: Array.Empty<string>(),
Digests: Array.Empty<string>(),
Formats: Array.Empty<VexDocumentFormat>(),
@@ -134,7 +134,7 @@ public sealed class DefaultVexProviderRunnerIntegrationTests
var finalCount = (await rawStore.QueryAsync(
new VexRawQuery(
Tenant: "tenant-integration",
Tenant: "default",
ProviderIds: Array.Empty<string>(),
Digests: Array.Empty<string>(),
Formats: Array.Empty<VexDocumentFormat>(),

View File

@@ -69,8 +69,8 @@ public sealed class EndToEndIngestJobTests
// Assert - documents stored
connector.FetchInvoked.Should().BeTrue("Connector should have been fetched");
rawStore.StoredDocuments.Should().HaveCount(2, "Both VEX documents should be stored");
rawStore.StoredDocuments.Should().ContainKey("sha256:e2e-001");
rawStore.StoredDocuments.Should().ContainKey("sha256:e2e-002");
rawStore.StoredDocuments.Should().ContainKey("sha256:2024e2e001");
rawStore.StoredDocuments.Should().ContainKey("sha256:2024e2e002");
// Assert - state updated
var state = stateRepository.Get("excititor:e2e-test");
@@ -226,7 +226,7 @@ public sealed class EndToEndIngestJobTests
{
var services = new ServiceCollection();
services.AddSingleton(connector);
services.AddSingleton<IVexConnector>(connector);
services.AddSingleton<IVexConnectorStateRepository>(stateRepository);
services.AddSingleton<IVexRawStore>(rawStore ?? new InMemoryRawStore());
services.AddSingleton<IVexProviderStore>(new InMemoryVexProviderStore());

View File

@@ -263,7 +263,7 @@ public class VexWorkerOrchestratorClientTests
var result = new VexWorkerJobResult(
DocumentsProcessed: 10,
ClaimsGenerated: 25,
LastCheckpoint: "checkpoint-new",
LastCheckpoint: "2025-11-27T12:00:00+00:00",
LastArtifactHash: "sha256:final",
CompletedAt: completedAt);