Add SBOM, symbols, traces, and VEX files for CVE-2022-21661 SQLi case
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Created CycloneDX and SPDX SBOM files for both reachable and unreachable images.
- Added symbols.json detailing function entry and sink points in the WordPress code.
- Included runtime traces for function calls in both reachable and unreachable scenarios.
- Developed OpenVEX files indicating vulnerability status and justification for both cases.
- Updated README for evaluator harness to guide integration with scanner output.
This commit is contained in:
master
2025-11-08 20:53:45 +02:00
parent 515975edc5
commit 536f6249a6
837 changed files with 37279 additions and 14675 deletions

View File

@@ -1,22 +1,36 @@
using System.Diagnostics.Metrics;
namespace StellaOps.Concelier.WebService.Diagnostics;
internal static class IngestionMetrics
{
internal const string MeterName = "StellaOps.Concelier.WebService.Ingestion";
private static readonly Meter Meter = new(MeterName);
internal static readonly Counter<long> WriteCounter = Meter.CreateCounter<long>(
"ingestion_write_total",
description: "Counts raw advisory ingestion attempts, segmented by tenant, source, and result.");
internal static readonly Counter<long> ViolationCounter = Meter.CreateCounter<long>(
"aoc_violation_total",
description: "Counts Aggregation-Only Contract violations detected during ingestion.");
internal static readonly Counter<long> VerificationCounter = Meter.CreateCounter<long>(
"verify_runs_total",
description: "Counts AOC verification runs initiated via the API.");
}
using System.Collections.Generic;
using System.Diagnostics.Metrics;
namespace StellaOps.Concelier.WebService.Diagnostics;
internal static class IngestionMetrics
{
internal const string MeterName = "StellaOps.Concelier.WebService.Ingestion";
private static readonly Meter Meter = new(MeterName);
internal static readonly Counter<long> IngestionWriteCounter = Meter.CreateCounter<long>(
"ingestion_write_total",
unit: "count",
description: "Number of advisory ingestion attempts processed by the web service.");
internal static readonly Counter<long> VerificationCounter = Meter.CreateCounter<long>(
"verify_runs_total",
unit: "count",
description: "Number of AOC verification requests processed by the web service.");
internal static KeyValuePair<string, object?>[] BuildWriteTags(string tenant, string source, string result) =>
new[]
{
new KeyValuePair<string, object?>("tenant", tenant),
new KeyValuePair<string, object?>("source", source),
new KeyValuePair<string, object?>("result", result),
};
internal static KeyValuePair<string, object?>[] BuildVerifyTags(string tenant, string result) =>
new[]
{
new KeyValuePair<string, object?>("tenant", tenant),
new KeyValuePair<string, object?>("result", result),
};
}

View File

@@ -1,9 +1,10 @@
using System.Collections.Immutable;
using System.Text.Json;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.WebService.Contracts;
namespace StellaOps.Concelier.WebService.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text.Json;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.WebService.Contracts;
namespace StellaOps.Concelier.WebService.Extensions;
internal static class AdvisoryRawRequestMapper
{
@@ -14,13 +15,13 @@ internal static class AdvisoryRawRequestMapper
ArgumentNullException.ThrowIfNull(timeProvider);
var sourceRequest = request.Source ?? throw new ArgumentException("source section is required.", nameof(request));
var upstreamRequest = request.Upstream ?? throw new ArgumentException("upstream section is required.", nameof(request));
var contentRequest = request.Content ?? throw new ArgumentException("content section is required.", nameof(request));
var identifiersRequest = request.Identifiers ?? throw new ArgumentException("identifiers section is required.", nameof(request));
var source = new RawSourceMetadata(
sourceRequest.Vendor,
sourceRequest.Connector,
var upstreamRequest = request.Upstream ?? throw new ArgumentException("upstream section is required.", nameof(request));
var contentRequest = request.Content ?? throw new ArgumentException("content section is required.", nameof(request));
var identifiersRequest = request.Identifiers ?? throw new ArgumentException("identifiers section is required.", nameof(request));
var source = new RawSourceMetadata(
sourceRequest.Vendor,
sourceRequest.Connector,
sourceRequest.Version,
string.IsNullOrWhiteSpace(sourceRequest.Stream) ? null : sourceRequest.Stream);
@@ -33,22 +34,21 @@ internal static class AdvisoryRawRequestMapper
string.IsNullOrWhiteSpace(signatureRequest.Certificate) ? null : signatureRequest.Certificate,
string.IsNullOrWhiteSpace(signatureRequest.Digest) ? null : signatureRequest.Digest);
var retrievedAt = upstreamRequest.RetrievedAt ?? timeProvider.GetUtcNow();
var upstream = new RawUpstreamMetadata(
upstreamRequest.UpstreamId,
string.IsNullOrWhiteSpace(upstreamRequest.DocumentVersion) ? null : upstreamRequest.DocumentVersion,
retrievedAt,
upstreamRequest.ContentHash,
signature,
NormalizeDictionary(upstreamRequest.Provenance));
var rawContent = NormalizeRawContent(contentRequest.Raw);
var content = new RawContent(
contentRequest.Format,
string.IsNullOrWhiteSpace(contentRequest.SpecVersion) ? null : contentRequest.SpecVersion,
rawContent,
string.IsNullOrWhiteSpace(contentRequest.Encoding) ? null : contentRequest.Encoding);
var retrievedAt = upstreamRequest.RetrievedAt ?? timeProvider.GetUtcNow();
var upstream = new RawUpstreamMetadata(
upstreamRequest.UpstreamId,
string.IsNullOrWhiteSpace(upstreamRequest.DocumentVersion) ? null : upstreamRequest.DocumentVersion,
retrievedAt,
upstreamRequest.ContentHash,
signature,
NormalizeDictionary(upstreamRequest.Provenance));
var rawContent = NormalizeRawContent(contentRequest.Raw);
var content = new RawContent(
contentRequest.Format,
string.IsNullOrWhiteSpace(contentRequest.SpecVersion) ? null : contentRequest.SpecVersion,
rawContent,
string.IsNullOrWhiteSpace(contentRequest.Encoding) ? null : contentRequest.Encoding);
var aliases = NormalizeStrings(identifiersRequest.Aliases);
if (aliases.IsDefault)
@@ -56,11 +56,15 @@ internal static class AdvisoryRawRequestMapper
aliases = ImmutableArray<string>.Empty;
}
var identifiers = new RawIdentifiers(
aliases,
identifiersRequest.Primary);
var linksetRequest = request.Linkset;
var identifiers = new RawIdentifiers(
aliases,
identifiersRequest.Primary);
var advisoryKey = NormalizeAdvisoryKey(
identifiersRequest.Primary,
aliases,
upstreamRequest.UpstreamId);
var linksetRequest = request.Linkset;
var linkset = new RawLinkset
{
Aliases = NormalizeStrings(linksetRequest?.Aliases),
@@ -71,6 +75,8 @@ internal static class AdvisoryRawRequestMapper
Notes = NormalizeDictionary(linksetRequest?.Notes)
};
var links = BuildLinks(advisoryKey, aliases, upstreamRequest.UpstreamId);
return new AdvisoryRawDocument(
tenant.Trim().ToLowerInvariant(),
source,
@@ -78,8 +84,8 @@ internal static class AdvisoryRawRequestMapper
content,
identifiers,
linkset,
AdvisoryKey: string.Empty,
Links: ImmutableArray<RawLink>.Empty);
AdvisoryKey: advisoryKey,
Links: links);
}
internal static ImmutableArray<string> NormalizeStrings(IEnumerable<string>? values)
@@ -124,11 +130,11 @@ internal static class AdvisoryRawRequestMapper
return builder.ToImmutable();
}
private static ImmutableArray<RawReference> NormalizeReferences(IEnumerable<AdvisoryLinksetReferenceRequest>? references)
{
if (references is null)
{
return ImmutableArray<RawReference>.Empty;
private static ImmutableArray<RawReference> NormalizeReferences(IEnumerable<AdvisoryLinksetReferenceRequest>? references)
{
if (references is null)
{
return ImmutableArray<RawReference>.Empty;
}
var builder = ImmutableArray.CreateBuilder<RawReference>();
@@ -150,10 +156,59 @@ internal static class AdvisoryRawRequestMapper
return builder.Count == 0 ? ImmutableArray<RawReference>.Empty : builder.ToImmutable();
}
private static JsonElement NormalizeRawContent(JsonElement element)
{
var json = element.ValueKind == JsonValueKind.Undefined ? "{}" : element.GetRawText();
using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(json) ? "{}" : json);
return document.RootElement.Clone();
}
}
private static JsonElement NormalizeRawContent(JsonElement element)
{
var json = element.ValueKind == JsonValueKind.Undefined ? "{}" : element.GetRawText();
using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(json) ? "{}" : json);
return document.RootElement.Clone();
}
private static string NormalizeAdvisoryKey(string? primaryId, ImmutableArray<string> aliases, string upstreamId)
{
if (!string.IsNullOrWhiteSpace(primaryId))
{
return primaryId.Trim();
}
foreach (var alias in aliases)
{
if (!string.IsNullOrWhiteSpace(alias))
{
return alias.Trim();
}
}
return string.IsNullOrWhiteSpace(upstreamId) ? string.Empty : upstreamId.Trim();
}
private static ImmutableArray<RawLink> BuildLinks(string advisoryKey, ImmutableArray<string> aliases, string upstreamId)
{
var builder = ImmutableArray.CreateBuilder<RawLink>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void AddLink(string scheme, string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
var normalized = value.Trim();
var key = $"{scheme}:{normalized}";
if (seen.Add(key))
{
builder.Add(new RawLink(scheme, normalized));
}
}
AddLink("PRIMARY", advisoryKey);
foreach (var alias in aliases)
{
AddLink("ALIAS", alias);
}
AddLink("UPSTREAM", upstreamId);
return builder.Count == 0 ? ImmutableArray<RawLink>.Empty : builder.ToImmutable();
}
}

View File

@@ -10,10 +10,11 @@ using OpenTelemetry.Trace;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Connector.Common.Telemetry;
using StellaOps.Concelier.WebService.Diagnostics;
using StellaOps.Concelier.WebService.Options;
using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Connector.Common.Telemetry;
using StellaOps.Concelier.WebService.Diagnostics;
using StellaOps.Concelier.WebService.Options;
using StellaOps.Ingestion.Telemetry;
namespace StellaOps.Concelier.WebService.Extensions;
@@ -65,13 +66,14 @@ public static class TelemetryExtensions
if (telemetry.EnableTracing)
{
openTelemetry.WithTracing(tracing =>
{
tracing
.AddSource(JobDiagnostics.ActivitySourceName)
.AddSource(SourceDiagnostics.ActivitySourceName)
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation();
openTelemetry.WithTracing(tracing =>
{
tracing
.AddSource(JobDiagnostics.ActivitySourceName)
.AddSource(SourceDiagnostics.ActivitySourceName)
.AddSource(IngestionTelemetry.ActivitySourceName)
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation();
ConfigureExporters(telemetry, tracing);
});
@@ -84,7 +86,7 @@ public static class TelemetryExtensions
metrics
.AddMeter(JobDiagnostics.MeterName)
.AddMeter(SourceDiagnostics.MeterName)
.AddMeter(IngestionMetrics.MeterName)
.AddMeter(IngestionTelemetry.MeterName)
.AddMeter("StellaOps.Concelier.Connector.CertBund")
.AddMeter("StellaOps.Concelier.Connector.Nvd")
.AddMeter("StellaOps.Concelier.Connector.Vndr.Chromium")

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using StellaOps.Configuration;
namespace StellaOps.Concelier.WebService.Options;
@@ -19,6 +20,8 @@ public sealed class ConcelierOptions
public FeaturesOptions Features { get; set; } = new();
public AdvisoryChunkOptions AdvisoryChunks { get; set; } = new();
public StellaOpsCryptoOptions Crypto { get; } = new();
public sealed class StorageOptions
{

View File

@@ -82,6 +82,8 @@ builder.Services.AddOptions<ConcelierOptions>()
})
.ValidateOnStart();
builder.Services.AddStellaOpsCrypto(concelierOptions.Crypto);
builder.ConfigureConcelierTelemetry(concelierOptions);
builder.Services.TryAddSingleton<TimeProvider>(_ => TimeProvider.System);
@@ -387,6 +389,14 @@ var advisoryIngestEndpoint = app.MapPost("/ingest/advisory", async (
return authorizationError;
}
using var ingestScope = logger.BeginScope(new Dictionary<string, object?>(StringComparer.Ordinal)
{
["tenant"] = tenant,
["source.vendor"] = ingestRequest.Source.Vendor,
["upstream.upstreamId"] = ingestRequest.Upstream.UpstreamId,
["contentHash"] = ingestRequest.Upstream.ContentHash ?? "(null)"
});
AdvisoryRawDocument document;
try
{
@@ -423,12 +433,12 @@ var advisoryIngestEndpoint = app.MapPost("/ingest/advisory", async (
context.Response.Headers.Location = $"/advisories/raw/{Uri.EscapeDataString(result.Record.Id)}";
}
IngestionMetrics.WriteCounter.Add(1, new[]
{
new KeyValuePair<string, object?>("tenant", tenant),
new KeyValuePair<string, object?>("source", result.Record.Document.Source.Vendor),
new KeyValuePair<string, object?>("result", result.Inserted ? "inserted" : "duplicate")
});
IngestionMetrics.IngestionWriteCounter.Add(
1,
IngestionMetrics.BuildWriteTags(
tenant,
ingestRequest.Source.Vendor ?? "(unknown)",
result.Inserted ? "inserted" : "duplicate"));
return JsonResult(response, statusCode);
}
@@ -443,12 +453,12 @@ var advisoryIngestEndpoint = app.MapPost("/ingest/advisory", async (
string.IsNullOrWhiteSpace(document.Upstream.ContentHash) ? "(empty)" : document.Upstream.ContentHash,
string.Join(',', guardException.Violations.Select(static violation => violation.ErrorCode)));
IngestionMetrics.ViolationCounter.Add(1, new[]
{
new KeyValuePair<string, object?>("tenant", tenant),
new KeyValuePair<string, object?>("source", document.Source.Vendor),
new KeyValuePair<string, object?>("code", guardException.PrimaryErrorCode)
});
IngestionMetrics.IngestionWriteCounter.Add(
1,
IngestionMetrics.BuildWriteTags(
tenant,
ingestRequest.Source.Vendor ?? "(unknown)",
"rejected"));
return MapAocGuardException(context, guardException);
}
@@ -467,25 +477,8 @@ advisoryIngestEndpoint.RequireAocGuard<AdvisoryIngestRequest>(request =>
return Array.Empty<object?>();
}
var linkset = request.Linkset ?? new AdvisoryLinksetRequest(
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<AdvisoryLinksetReferenceRequest>(),
Array.Empty<string>(),
new Dictionary<string, string>(StringComparer.Ordinal));
var payload = new
{
tenant = "guard-tenant",
source = request.Source,
upstream = request.Upstream,
content = request.Content,
identifiers = request.Identifiers,
linkset
};
return new object?[] { payload };
var guardDocument = AdvisoryRawRequestMapper.Map(request, "guard-tenant", TimeProvider.System);
return new object?[] { guardDocument };
}, guardOptions: advisoryIngestGuardOptions);
if (authorityConfigured)
@@ -796,11 +789,9 @@ var aocVerifyEndpoint = app.MapPost("/aoc/verify", async (
var verificationOutcome = response.Truncated
? "truncated"
: (violationResponses.Length == 0 ? "ok" : "violations");
IngestionMetrics.VerificationCounter.Add(1, new[]
{
new KeyValuePair<string, object?>("tenant", tenant),
new KeyValuePair<string, object?>("result", verificationOutcome)
});
IngestionMetrics.VerificationCounter.Add(
1,
IngestionMetrics.BuildVerifyTags(tenant, verificationOutcome));
return JsonResult(response);
});

View File

@@ -1,7 +1,6 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
@@ -10,6 +9,7 @@ using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using StellaOps.Cryptography;
namespace StellaOps.Concelier.WebService.Services;
@@ -28,14 +28,18 @@ internal sealed class OpenApiDiscoveryDocumentProvider
];
private readonly EndpointDataSource _endpointDataSource;
private readonly ICryptoHash _hash;
private readonly object _syncRoot = new();
private string? _cachedDocumentJson;
private string? _cachedEtag;
public OpenApiDiscoveryDocumentProvider(EndpointDataSource endpointDataSource)
public OpenApiDiscoveryDocumentProvider(
EndpointDataSource endpointDataSource,
ICryptoHash hash)
{
_endpointDataSource = endpointDataSource;
_endpointDataSource = endpointDataSource ?? throw new ArgumentNullException(nameof(endpointDataSource));
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
}
public (string Payload, string ETag) GetDocument()
@@ -58,7 +62,7 @@ internal sealed class OpenApiDiscoveryDocumentProvider
});
var bytes = Encoding.UTF8.GetBytes(json);
var hash = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
var hash = _hash.ComputeHashHex(bytes);
var computedEtag = $"\"{hash}\"";
_cachedDocumentJson = json;

View File

@@ -30,6 +30,8 @@
<ProjectReference Include="../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />

View File

@@ -1,94 +1,98 @@
# TASKS — Epic 1: Aggregation-Only Contract
> **AOC Reminder:** service links and exposes raw data only—no precedence, severity, or hint computation inside Concelier APIs.
| ID | Status | Owner(s) | Depends on | Notes |
|---|---|---|---|---|
> Docs alignment (2025-10-26): Endpoint expectations + scope requirements detailed in `docs/ingestion/aggregation-only-contract.md` and `docs/security/authority-scopes.md`.
> 2025-10-28: Added coverage for pagination, tenancy enforcement, and ingestion/verification metrics; verified guard handling paths end-to-end.
| CONCELIER-WEB-AOC-19-002 `AOC observability` | TODO | Concelier WebService Guild, Observability Guild | CONCELIER-WEB-AOC-19-001 | Emit `ingestion_write_total`, `aoc_violation_total`, latency histograms, and tracing spans (`ingest.fetch/transform/write`, `aoc.guard`). Wire structured logging to include tenant, source vendor, upstream id, and content hash. |
> Docs alignment (2025-10-26): Metrics/traces/log schema in `docs/observability/observability.md`.
| CONCELIER-WEB-AOC-19-003 `Schema/guard unit tests` | TODO | QA Guild | CONCELIER-WEB-AOC-19-001 | Add unit tests covering schema validation failures, forbidden field rejections (`ERR_AOC_001/002/006/007`), idempotent upserts, and supersedes chains using deterministic fixtures. |
> Docs alignment (2025-10-26): Guard rules + error codes documented in AOC reference §5 and CLI guide.
| CONCELIER-WEB-AOC-19-004 `End-to-end ingest verification` | TODO | Concelier WebService Guild, QA Guild | CONCELIER-WEB-AOC-19-003, CONCELIER-CORE-AOC-19-002 | Create integration tests ingesting large advisory batches (cold/warm) validating linkset enrichment, metrics emission, and reproducible outputs. Capture load-test scripts + doc notes for Offline Kit dry runs. |
> Docs alignment (2025-10-26): Offline verification workflow referenced in `docs/deploy/containers.md` §5.
## Policy Engine v2
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-POLICY-20-001 `Policy selection endpoints` | TODO | Concelier WebService Guild | WEB-POLICY-20-001, CONCELIER-CORE-AOC-19-004 | Add batch advisory lookup APIs (`/policy/select/advisories`, `/policy/select/vex`) optimized for PURL/ID lists with pagination, tenant scoping, and explain metadata. |
## StellaOps Console (Sprint 23)
| ID | Status | Owner(s) | Depends on | Notes |
# TASKS — Epic 1: Aggregation-Only Contract
> **AOC Reminder:** service links and exposes raw data only—no precedence, severity, or hint computation inside Concelier APIs.
| ID | Status | Owner(s) | Depends on | Notes |
|---|---|---|---|---|
> Docs alignment (2025-10-26): Endpoint expectations + scope requirements detailed in `docs/ingestion/aggregation-only-contract.md` and `docs/security/authority-scopes.md`.
> 2025-10-28: Added coverage for pagination, tenancy enforcement, and ingestion/verification metrics; verified guard handling paths end-to-end.
| CONCELIER-WEB-AOC-19-002 `AOC observability` | DONE (2025-11-07) | Concelier WebService Guild, Observability Guild | CONCELIER-WEB-AOC-19-001 | Emit `ingestion_write_total`, `aoc_violation_total`, latency histograms, and tracing spans (`ingest.fetch/transform/write`, `aoc.guard`). Wire structured logging to include tenant, source vendor, upstream id, and content hash. |
> Docs alignment (2025-10-26): Metrics/traces/log schema in `docs/observability/observability.md`.
| CONCELIER-WEB-AOC-19-003 `Schema/guard unit tests` | TODO | QA Guild | CONCELIER-WEB-AOC-19-001 | Add unit tests covering schema validation failures, forbidden field rejections (`ERR_AOC_001/002/006/007`), idempotent upserts, and supersedes chains using deterministic fixtures. |
> Docs alignment (2025-10-26): Guard rules + error codes documented in AOC reference §5 and CLI guide.
| CONCELIER-WEB-AOC-19-004 `End-to-end ingest verification` | TODO | Concelier WebService Guild, QA Guild | CONCELIER-WEB-AOC-19-003, CONCELIER-CORE-AOC-19-002 | Create integration tests ingesting large advisory batches (cold/warm) validating linkset enrichment, metrics emission, and reproducible outputs. Capture load-test scripts + doc notes for Offline Kit dry runs. |
> Docs alignment (2025-10-26): Offline verification workflow referenced in `docs/deploy/containers.md` §5.
| CONCELIER-WEB-AOC-19-005 `Chunk evidence regression` | TODO (2025-11-08) | Concelier WebService Guild, QA Guild | CONCELIER-WEB-AOC-19-002 | Fix `/advisories/{key}/chunks` seeded fixtures so AdvisoryChunksEndpoint tests stop returning 404/not-found when raw documents are pre-populated; ensure Mongo migrations no longer emit “Unable to locate advisory_raw documents” during test boot. |
| CONCELIER-WEB-AOC-19-006 `Allowlist ingest auth parity` | TODO (2025-11-08) | Concelier WebService Guild | CONCELIER-WEB-AOC-19-002 | Align WebService auth defaults with the test tokens so the allowlisted tenant can create an advisory before forbidden tenants are rejected in `AdvisoryIngestEndpoint_RejectsTenantOutsideAllowlist`. |
| CONCELIER-WEB-AOC-19-007 `AOC verify violation codes` | TODO (2025-11-08) | Concelier WebService Guild, QA Guild | CONCELIER-WEB-AOC-19-002 | Update AOC verify logic/fixtures so guard failures produce the expected `ERR_AOC_001` payload (current regression returns `ERR_AOC_004`) while keeping the mapper/guard parity exercised by the new tests. |
| CONCELIER-CRYPTO-90-001 `Crypto provider adoption` | DOING (2025-11-08) | Concelier WebService Guild, Security Guild | SEC-CRYPTO-90-003, SEC-CRYPTO-90-004 | Route hashing/signing in OpenAPI discovery, Mirror connectors, and RU advisory adapters through `ICryptoProviderRegistry` so RootPack_RU uses CryptoPro/PKCS#11 keys. Reference `docs/security/crypto-routing-audit-2025-11-07.md`. |
## Policy Engine v2
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-POLICY-20-001 `Policy selection endpoints` | TODO | Concelier WebService Guild | WEB-POLICY-20-001, CONCELIER-CORE-AOC-19-004 | Add batch advisory lookup APIs (`/policy/select/advisories`, `/policy/select/vex`) optimized for PURL/ID lists with pagination, tenant scoping, and explain metadata. |
## StellaOps Console (Sprint 23)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-CONSOLE-23-001 `Advisory aggregation views` | TODO | Concelier WebService Guild, BE-Base Platform Guild | CONCELIER-LNM-21-201, CONCELIER-LNM-21-202 | Expose `/console/advisories` endpoints returning aggregation groups (per linkset) with source chips, provider-reported severity columns (no local consensus), and provenance metadata for Console list + dashboard cards. Support filters by source, ecosystem, published/modified window, tenant enforcement. |
| CONCELIER-CONSOLE-23-002 `Dashboard deltas API` | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-001, CONCELIER-LNM-21-203 | Provide aggregated advisory delta counts (new, modified, conflicting) for Console dashboard + live status ticker; emit structured events for queue lag metrics. Ensure deterministic counts across repeated queries. |
| CONCELIER-CONSOLE-23-003 `Search fan-out helpers` | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-001 | Deliver fast lookup endpoints for CVE/GHSA/purl search (linksets, observations) returning evidence fragments for Console global search; implement caching + scope guards. |
## Graph Explorer v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
## Link-Not-Merge v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-LNM-21-201 `Observation APIs` | TODO | Concelier WebService Guild, BE-Base Platform Guild | CONCELIER-LNM-21-001 | Add REST endpoints for advisory observations (`GET /advisories/observations`) with filters (alias, purl, source), pagination, and tenancy enforcement. |
| CONCELIER-LNM-21-202 `Linkset APIs` | TODO | Concelier WebService Guild | CONCELIER-LNM-21-002, CONCELIER-LNM-21-003 | Implement linkset read/export endpoints (`/advisories/linksets/{id}`, `/advisories/by-purl/{purl}`, `/advisories/linksets/{id}/export`, `/evidence`) with correlation/conflict payloads and `ERR_AGG_*` mapping. |
| CONCELIER-LNM-21-203 `Ingest events` | TODO | Concelier WebService Guild, Platform Events Guild | CONCELIER-LNM-21-005 | Publish NATS/Redis events for new observations/linksets and ensure idempotent consumer contracts; document event schemas. |
## Graph & Vuln Explorer v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-GRAPH-24-101 `Advisory summary API` | TODO | Concelier WebService Guild | CONCELIER-GRAPH-24-001 | Expose `/advisories/summary` returning raw linkset/observation metadata for overlay services; no derived severity or fix hints. |
| CONCELIER-GRAPH-28-102 `Evidence batch API` | TODO | Concelier WebService Guild | CONCELIER-LNM-21-201 | Add batch fetch for advisory observations/linksets keyed by component sets to feed Graph overlay tooltips efficiently. |
## VEX Lens (Sprint 30)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-VEXLENS-30-001 `Advisory rationale bridges` | TODO | Concelier WebService Guild, VEX Lens Guild | CONCELIER-VULN-29-001, VEXLENS-30-005 | Guarantee advisory key consistency and cross-links for consensus rationale; Label: VEX-Lens. |
## Vulnerability Explorer (Sprint 29)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-CONSOLE-23-002 `Dashboard deltas API` | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-001, CONCELIER-LNM-21-203 | Provide aggregated advisory delta counts (new, modified, conflicting) for Console dashboard + live status ticker; emit structured events for queue lag metrics. Ensure deterministic counts across repeated queries. |
| CONCELIER-CONSOLE-23-003 `Search fan-out helpers` | TODO | Concelier WebService Guild | CONCELIER-CONSOLE-23-001 | Deliver fast lookup endpoints for CVE/GHSA/purl search (linksets, observations) returning evidence fragments for Console global search; implement caching + scope guards. |
## Graph Explorer v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
## Link-Not-Merge v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-LNM-21-201 `Observation APIs` | TODO | Concelier WebService Guild, BE-Base Platform Guild | CONCELIER-LNM-21-001 | Add REST endpoints for advisory observations (`GET /advisories/observations`) with filters (alias, purl, source), pagination, and tenancy enforcement. |
| CONCELIER-LNM-21-202 `Linkset APIs` | TODO | Concelier WebService Guild | CONCELIER-LNM-21-002, CONCELIER-LNM-21-003 | Implement linkset read/export endpoints (`/advisories/linksets/{id}`, `/advisories/by-purl/{purl}`, `/advisories/linksets/{id}/export`, `/evidence`) with correlation/conflict payloads and `ERR_AGG_*` mapping. |
| CONCELIER-LNM-21-203 `Ingest events` | TODO | Concelier WebService Guild, Platform Events Guild | CONCELIER-LNM-21-005 | Publish NATS/Redis events for new observations/linksets and ensure idempotent consumer contracts; document event schemas. |
## Graph & Vuln Explorer v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-GRAPH-24-101 `Advisory summary API` | TODO | Concelier WebService Guild | CONCELIER-GRAPH-24-001 | Expose `/advisories/summary` returning raw linkset/observation metadata for overlay services; no derived severity or fix hints. |
| CONCELIER-GRAPH-28-102 `Evidence batch API` | TODO | Concelier WebService Guild | CONCELIER-LNM-21-201 | Add batch fetch for advisory observations/linksets keyed by component sets to feed Graph overlay tooltips efficiently. |
## VEX Lens (Sprint 30)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-VEXLENS-30-001 `Advisory rationale bridges` | TODO | Concelier WebService Guild, VEX Lens Guild | CONCELIER-VULN-29-001, VEXLENS-30-005 | Guarantee advisory key consistency and cross-links for consensus rationale; Label: VEX-Lens. |
## Vulnerability Explorer (Sprint 29)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-VULN-29-001 `Advisory key canonicalization` | DONE (2025-11-07) | Concelier WebService Guild, Data Integrity Guild | CONCELIER-LNM-21-001 | Canonicalize (lossless) advisory identifiers (CVE/GHSA/vendor) into `advisory_key`, persist `links[]`, expose raw payload snapshots for Explorer evidence tabs; AOC-compliant: no merge, no derived fields, no suppression. Include migration/backfill scripts. |
| CONCELIER-VULN-29-002 `Evidence retrieval API` | DOING (2025-11-07) | Concelier WebService Guild | CONCELIER-VULN-29-001, VULN-API-29-003 | Provide `/vuln/evidence/advisories/{advisory_key}` returning raw advisory docs with provenance, filtering by tenant and source. |
| CONCELIER-VULN-29-004 `Observability enhancements` | TODO | Concelier WebService Guild, Observability Guild | CONCELIER-VULN-29-001 | Instrument metrics/logs for observation + linkset pipelines (identifier collisions, withdrawn flags) and emit events consumed by Vuln Explorer resolver. |
## Advisory AI (Sprint 31)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
## Advisory AI (Sprint 31)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-AIAI-31-001 `Paragraph anchors` | DONE | Concelier WebService Guild | CONCELIER-VULN-29-001 | Expose advisory chunk API returning paragraph anchors, section metadata, and token-safe text for Advisory AI retrieval. See docs/updates/2025-11-07-concelier-advisory-chunks.md. |
| CONCELIER-AIAI-31-002 `Structured fields` | TODO | Concelier WebService Guild | CONCELIER-AIAI-31-001 | Ensure observation APIs expose upstream workaround/fix/CVSS fields with provenance; add caching for summary queries. |
| CONCELIER-AIAI-31-003 `Advisory AI telemetry` | TODO | Concelier WebService Guild, Observability Guild | CONCELIER-AIAI-31-001 | Emit metrics/logs for chunk requests, cache hits, and guardrail blocks triggered by advisory payloads. |
## Observability & Forensics (Epic 15)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-WEB-OBS-50-001 `Telemetry adoption` | TODO | Concelier WebService Guild | TELEMETRY-OBS-50-001, CONCELIER-OBS-50-001 | Adopt telemetry core in web service host, ensure ingest + read endpoints emit trace/log fields (`tenant_id`, `route`, `decision_effect`), and add correlation IDs to responses. |
| CONCELIER-WEB-OBS-51-001 `Observability APIs` | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-50-001, WEB-OBS-51-001 | Surface ingest health metrics, queue depth, and SLO status via `/obs/concelier/health` endpoint for Console widgets, with caching and tenant partitioning. |
| CONCELIER-WEB-OBS-52-001 `Timeline streaming` | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-50-001, TIMELINE-OBS-52-003 | Provide SSE stream `/obs/concelier/timeline` bridging to Timeline Indexer with paging tokens, guardrails, and audit logging. |
| CONCELIER-WEB-OBS-53-001 `Evidence locker integration` | TODO | Concelier WebService Guild, Evidence Locker Guild | CONCELIER-OBS-53-001, EVID-OBS-53-003 | Add `/evidence/advisories/*` routes invoking evidence locker snapshots, verifying tenant scopes (`evidence:read`), and returning signed manifest metadata. |
| CONCELIER-WEB-OBS-54-001 `Attestation exposure` | TODO | Concelier WebService Guild | CONCELIER-OBS-54-001, PROV-OBS-54-001 | Provide `/attestations/advisories/*` read APIs surfacing DSSE status, verification summary, and provenance chain for Console/CLI. |
| CONCELIER-WEB-OBS-55-001 `Incident mode toggles` | TODO | Concelier WebService Guild, DevOps Guild | CONCELIER-OBS-55-001, WEB-OBS-55-001 | Implement incident mode toggle endpoints, propagate to orchestrator/locker, and document cooldown/backoff semantics. |
## Air-Gapped Mode (Epic 16)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-WEB-AIRGAP-56-001 `Mirror import APIs` | TODO | Concelier WebService Guild | AIRGAP-IMP-58-001, CONCELIER-AIRGAP-56-001 | Extend ingestion endpoints to register mirror bundle sources, expose bundle catalog queries, and block external feed URLs in sealed mode. |
| CONCELIER-WEB-AIRGAP-56-002 `Airgap status surfaces` | TODO | Concelier WebService Guild | CONCELIER-AIRGAP-57-002, AIRGAP-CTL-56-002 | Add staleness metadata and bundle provenance to advisory APIs (`/advisories/observations`, `/advisories/linksets`). |
| CONCELIER-WEB-AIRGAP-57-001 `Error remediation` | TODO | Concelier WebService Guild, AirGap Policy Guild | AIRGAP-POL-56-001 | Map sealed-mode violations to `AIRGAP_EGRESS_BLOCKED` responses with user guidance. |
| CONCELIER-WEB-AIRGAP-58-001 `Import timeline emission` | TODO | Concelier WebService Guild, AirGap Importer Guild | CONCELIER-WEB-AIRGAP-56-001, TIMELINE-OBS-53-001 | Emit timeline events for bundle ingestion operations with bundle ID, scope, and actor metadata. |
## SDKs & OpenAPI (Epic 17)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-AIAI-31-003 `Advisory AI telemetry` | TODO | Concelier WebService Guild, Observability Guild | CONCELIER-AIAI-31-001 | Emit metrics/logs for chunk requests, cache hits, and guardrail blocks triggered by advisory payloads. |
## Observability & Forensics (Epic 15)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-WEB-OBS-50-001 `Telemetry adoption` | DONE (2025-11-07) | Concelier WebService Guild | TELEMETRY-OBS-50-001, CONCELIER-OBS-50-001 | Adopt telemetry core in web service host, ensure ingest + read endpoints emit trace/log fields (`tenant_id`, `route`, `decision_effect`), and add correlation IDs to responses. |
| CONCELIER-WEB-OBS-51-001 `Observability APIs` | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-50-001, WEB-OBS-51-001 | Surface ingest health metrics, queue depth, and SLO status via `/obs/concelier/health` endpoint for Console widgets, with caching and tenant partitioning. |
| CONCELIER-WEB-OBS-52-001 `Timeline streaming` | TODO | Concelier WebService Guild | CONCELIER-WEB-OBS-50-001, TIMELINE-OBS-52-003 | Provide SSE stream `/obs/concelier/timeline` bridging to Timeline Indexer with paging tokens, guardrails, and audit logging. |
| CONCELIER-WEB-OBS-53-001 `Evidence locker integration` | TODO | Concelier WebService Guild, Evidence Locker Guild | CONCELIER-OBS-53-001, EVID-OBS-53-003 | Add `/evidence/advisories/*` routes invoking evidence locker snapshots, verifying tenant scopes (`evidence:read`), and returning signed manifest metadata. |
| CONCELIER-WEB-OBS-54-001 `Attestation exposure` | TODO | Concelier WebService Guild | CONCELIER-OBS-54-001, PROV-OBS-54-001 | Provide `/attestations/advisories/*` read APIs surfacing DSSE status, verification summary, and provenance chain for Console/CLI. |
| CONCELIER-WEB-OBS-55-001 `Incident mode toggles` | TODO | Concelier WebService Guild, DevOps Guild | CONCELIER-OBS-55-001, WEB-OBS-55-001 | Implement incident mode toggle endpoints, propagate to orchestrator/locker, and document cooldown/backoff semantics. |
## Air-Gapped Mode (Epic 16)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-WEB-AIRGAP-56-001 `Mirror import APIs` | TODO | Concelier WebService Guild | AIRGAP-IMP-58-001, CONCELIER-AIRGAP-56-001 | Extend ingestion endpoints to register mirror bundle sources, expose bundle catalog queries, and block external feed URLs in sealed mode. |
| CONCELIER-WEB-AIRGAP-56-002 `Airgap status surfaces` | TODO | Concelier WebService Guild | CONCELIER-AIRGAP-57-002, AIRGAP-CTL-56-002 | Add staleness metadata and bundle provenance to advisory APIs (`/advisories/observations`, `/advisories/linksets`). |
| CONCELIER-WEB-AIRGAP-57-001 `Error remediation` | TODO | Concelier WebService Guild, AirGap Policy Guild | AIRGAP-POL-56-001 | Map sealed-mode violations to `AIRGAP_EGRESS_BLOCKED` responses with user guidance. |
| CONCELIER-WEB-AIRGAP-58-001 `Import timeline emission` | TODO | Concelier WebService Guild, AirGap Importer Guild | CONCELIER-WEB-AIRGAP-56-001, TIMELINE-OBS-53-001 | Emit timeline events for bundle ingestion operations with bundle ID, scope, and actor metadata. |
## SDKs & OpenAPI (Epic 17)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-WEB-OAS-61-001 `/.well-known/openapi` | DONE (2025-11-02) | Concelier WebService Guild | OAS-61-001 | Implement discovery endpoint emitting Concelier spec with version metadata and ETag. |
| CONCELIER-WEB-OAS-61-002 `Error envelope migration` | TODO | Concelier WebService Guild | APIGOV-61-001 | Ensure all API responses use standardized error envelope; update controllers/tests. |
| CONCELIER-WEB-OAS-62-001 `Examples expansion` | TODO | Concelier WebService Guild | CONCELIER-OAS-61-002 | Add curated examples for advisory observations/linksets/conflicts; integrate into dev portal. |
| CONCELIER-WEB-OAS-63-001 `Deprecation headers` | TODO | Concelier WebService Guild, API Governance Guild | APIGOV-63-001 | Add Sunset/Deprecation headers for retiring endpoints and update documentation/notifications. |
| CONCELIER-WEB-OAS-61-002 `Error envelope migration` | TODO | Concelier WebService Guild | APIGOV-61-001 | Ensure all API responses use standardized error envelope; update controllers/tests. |
| CONCELIER-WEB-OAS-62-001 `Examples expansion` | TODO | Concelier WebService Guild | CONCELIER-OAS-61-002 | Add curated examples for advisory observations/linksets/conflicts; integrate into dev portal. |
| CONCELIER-WEB-OAS-63-001 `Deprecation headers` | TODO | Concelier WebService Guild, API Governance Guild | APIGOV-63-001 | Add Sunset/Deprecation headers for retiring endpoints and update documentation/notifications. |

View File

@@ -187,6 +187,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Analyzers", "__Analyzers"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Analyzers", "__Analyzers\StellaOps.Concelier.Analyzers\StellaOps.Concelier.Analyzers.csproj", "{39C1D44C-389F-4502-ADCF-E4AC359E8F8F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Ingestion.Telemetry", "..\__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj", "{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1265,6 +1267,18 @@ Global
{39C1D44C-389F-4502-ADCF-E4AC359E8F8F}.Release|x64.Build.0 = Release|Any CPU
{39C1D44C-389F-4502-ADCF-E4AC359E8F8F}.Release|x86.ActiveCfg = Release|Any CPU
{39C1D44C-389F-4502-ADCF-E4AC359E8F8F}.Release|x86.Build.0 = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|x64.ActiveCfg = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|x64.Build.0 = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|x86.ActiveCfg = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Debug|x86.Build.0 = Debug|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|Any CPU.Build.0 = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|x64.ActiveCfg = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|x64.Build.0 = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|x86.ActiveCfg = Release|Any CPU
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1349,5 +1363,6 @@ Global
{9006A5A2-01D8-4A70-AEA7-B7B1987C4A62} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{664A2577-6DA1-42DA-A213-3253017FA4BF} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{39C1D44C-389F-4502-ADCF-E4AC359E8F8F} = {176B5A8A-7857-3ECD-1128-3C721BC7F5C6}
{85D215EC-DCFE-4F7F-BB07-540DCF66BE8C} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
EndGlobalSection
EndGlobal

View File

@@ -25,17 +25,18 @@ namespace StellaOps.Concelier.Connector.Cccs;
public sealed class CccsConnector : IFeedConnector
{
private static readonly JsonSerializerOptions RawSerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
private static readonly JsonSerializerOptions DtoSerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
private const string DtoSchemaVersion = "cccs.dto.v1";
private static readonly JsonSerializerOptions RawSerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
private static readonly JsonSerializerOptions DtoSerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
private static readonly Uri CanonicalBaseUri = new("https://www.cyber.gc.ca", UriKind.Absolute);
private const string DtoSchemaVersion = "cccs.dto.v1";
private readonly CccsFeedClient _feedClient;
private readonly RawDocumentStorage _rawDocumentStorage;
@@ -482,24 +483,37 @@ public sealed class CccsConnector : IFeedConnector
}
}
private static string BuildDocumentUri(CccsFeedItem item, CccsFeedEndpoint feed)
{
if (!string.IsNullOrWhiteSpace(item.Url))
{
if (Uri.TryCreate(item.Url, UriKind.Absolute, out var absolute))
{
return absolute.ToString();
}
var baseUri = new Uri("https://www.cyber.gc.ca", UriKind.Absolute);
if (Uri.TryCreate(baseUri, item.Url, out var combined))
{
return combined.ToString();
}
}
return $"https://www.cyber.gc.ca/api/cccs/threats/{feed.Language}/{item.Nid}";
}
private static string BuildDocumentUri(CccsFeedItem item, CccsFeedEndpoint feed)
{
var candidate = item.Url?.Trim();
if (!string.IsNullOrWhiteSpace(candidate))
{
if (Uri.TryCreate(candidate, UriKind.Absolute, out var absolute))
{
if (IsHttpScheme(absolute.Scheme))
{
return absolute.ToString();
}
candidate = absolute.PathAndQuery;
if (!string.IsNullOrEmpty(absolute.Fragment))
{
candidate += absolute.Fragment;
}
}
if (!string.IsNullOrWhiteSpace(candidate) && Uri.TryCreate(CanonicalBaseUri, candidate, out var combined))
{
return combined.ToString();
}
}
return new Uri(CanonicalBaseUri, $"/api/cccs/threats/{feed.Language}/{item.Nid}").ToString();
}
private static bool IsHttpScheme(string? scheme)
=> string.Equals(scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
|| string.Equals(scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
private static CccsRawAdvisoryDocument CreateRawDocument(CccsFeedItem item, CccsFeedEndpoint feed, IReadOnlyDictionary<int, string> taxonomy)
{

View File

@@ -125,11 +125,16 @@ public sealed class CccsFeedEndpoint
throw new InvalidOperationException("Feed endpoint URI must be configured before building taxonomy URI.");
}
var language = Uri.GetQueryParameterValueOrDefault("lang", Language);
var builder = $"https://www.cyber.gc.ca/api/cccs/taxonomy/v1/get?lang={language}&vocabulary=cccs_alert_type";
return new Uri(builder, UriKind.Absolute);
}
}
var language = Uri.GetQueryParameterValueOrDefault("lang", Language);
var taxonomyBuilder = new UriBuilder(Uri)
{
Path = "/api/cccs/taxonomy/v1/get",
Query = $"lang={language}&vocabulary=cccs_alert_type"
};
return taxonomyBuilder.Uri;
}
}
internal static class CccsUriExtensions
{

View File

@@ -348,19 +348,21 @@ public sealed class CccsHtmlParser
private static string? NormalizeReferenceUrl(string? href, Uri? baseUri, string language)
{
if (string.IsNullOrWhiteSpace(href))
{
return null;
}
if (!Uri.TryCreate(href, UriKind.Absolute, out var absolute))
{
if (baseUri is null || !Uri.TryCreate(baseUri, href, out absolute))
{
return null;
}
}
if (string.IsNullOrWhiteSpace(href))
{
return null;
}
var candidate = href.Trim();
var hasAbsolute = Uri.TryCreate(candidate, UriKind.Absolute, out var absolute);
if (!hasAbsolute || string.Equals(absolute.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
{
if (baseUri is null || !Uri.TryCreate(baseUri, candidate, out absolute))
{
return null;
}
}
var builder = new UriBuilder(absolute)
{
Fragment = string.Empty,

View File

@@ -319,12 +319,19 @@ public sealed class KisaDetailParser
}
var headerRow = labelCell.ParentElement as IHtmlTableRowElement;
var columnIndex = labelCell.CellIndex;
var columnIndex = headerRow is null
? -1
: Array.FindIndex(headerRow.Cells.ToArray(), cell => ReferenceEquals(cell, labelCell));
if (headerRow is null)
{
return null;
}
if (columnIndex < 0)
{
return null;
}
var rows = ownerTable.Rows.ToArray();
var headerIndex = Array.FindIndex(rows, row => ReferenceEquals(row, headerRow));
if (headerIndex < 0)

View File

@@ -2,10 +2,9 @@ using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
@@ -17,10 +16,11 @@ using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Ru.Bdu.Configuration;
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Plugin;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Plugin;
using StellaOps.Cryptography;
namespace StellaOps.Concelier.Connector.Ru.Bdu;
@@ -44,8 +44,9 @@ public sealed class RuBduConnector : IFeedConnector
private readonly TimeProvider _timeProvider;
private readonly ILogger<RuBduConnector> _logger;
private readonly string _cacheDirectory;
private readonly string _archiveCachePath;
private readonly string _cacheDirectory;
private readonly string _archiveCachePath;
private readonly ICryptoHash _hash;
public RuBduConnector(
SourceFetchService fetchService,
@@ -55,9 +56,10 @@ public sealed class RuBduConnector : IFeedConnector
IAdvisoryStore advisoryStore,
ISourceStateRepository stateRepository,
IOptions<RuBduOptions> options,
RuBduDiagnostics diagnostics,
TimeProvider? timeProvider,
ILogger<RuBduConnector> logger)
RuBduDiagnostics diagnostics,
TimeProvider? timeProvider,
ILogger<RuBduConnector> logger,
ICryptoHash cryptoHash)
{
_fetchService = fetchService ?? throw new ArgumentNullException(nameof(fetchService));
_rawDocumentStorage = rawDocumentStorage ?? throw new ArgumentNullException(nameof(rawDocumentStorage));
@@ -69,8 +71,9 @@ public sealed class RuBduConnector : IFeedConnector
_options.Validate();
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_cacheDirectory = ResolveCacheDirectory(_options.CacheDirectory);
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_hash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
_cacheDirectory = ResolveCacheDirectory(_options.CacheDirectory);
_archiveCachePath = Path.Combine(_cacheDirectory, "vulxml.zip");
EnsureCacheDirectory();
}
@@ -398,7 +401,7 @@ public sealed class RuBduConnector : IFeedConnector
}
var payload = JsonSerializer.SerializeToUtf8Bytes(dto, SerializerOptions);
var sha = Convert.ToHexString(SHA256.HashData(payload)).ToLowerInvariant();
var sha = _hash.ComputeHashHex(payload);
var documentUri = BuildDocumentUri(dto.Identifier);
var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, documentUri, cancellationToken).ConfigureAwait(false);

View File

@@ -14,6 +14,7 @@
<ProjectReference Include="../StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -4,23 +4,23 @@ using System.IO;
using System.IO.Compression;
using System.Net;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using AngleSharp.Html.Parser;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration;
using StellaOps.Concelier.Connector.Ru.Nkcki.Internal;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Plugin;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using AngleSharp.Html.Parser;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration;
using StellaOps.Concelier.Connector.Ru.Nkcki.Internal;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Plugin;
using StellaOps.Cryptography;
namespace StellaOps.Concelier.Connector.Ru.Nkcki;
@@ -55,11 +55,12 @@ public sealed class RuNkckiConnector : IFeedConnector
private readonly ISourceStateRepository _stateRepository;
private readonly RuNkckiOptions _options;
private readonly TimeProvider _timeProvider;
private readonly RuNkckiDiagnostics _diagnostics;
private readonly ILogger<RuNkckiConnector> _logger;
private readonly string _cacheDirectory;
private readonly HtmlParser _htmlParser = new();
private readonly RuNkckiDiagnostics _diagnostics;
private readonly ILogger<RuNkckiConnector> _logger;
private readonly string _cacheDirectory;
private readonly ICryptoHash _hash;
private readonly HtmlParser _htmlParser = new();
public RuNkckiConnector(
SourceFetchService fetchService,
@@ -69,9 +70,10 @@ public sealed class RuNkckiConnector : IFeedConnector
IAdvisoryStore advisoryStore,
ISourceStateRepository stateRepository,
IOptions<RuNkckiOptions> options,
RuNkckiDiagnostics diagnostics,
TimeProvider? timeProvider,
ILogger<RuNkckiConnector> logger)
RuNkckiDiagnostics diagnostics,
TimeProvider? timeProvider,
ILogger<RuNkckiConnector> logger,
ICryptoHash cryptoHash)
{
_fetchService = fetchService ?? throw new ArgumentNullException(nameof(fetchService));
_rawDocumentStorage = rawDocumentStorage ?? throw new ArgumentNullException(nameof(rawDocumentStorage));
@@ -79,12 +81,13 @@ public sealed class RuNkckiConnector : IFeedConnector
_dtoStore = dtoStore ?? throw new ArgumentNullException(nameof(dtoStore));
_advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore));
_stateRepository = stateRepository ?? throw new ArgumentNullException(nameof(stateRepository));
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value ?? throw new ArgumentNullException(nameof(options));
_options.Validate();
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_cacheDirectory = ResolveCacheDirectory(_options.CacheDirectory);
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value ?? throw new ArgumentNullException(nameof(options));
_options.Validate();
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_hash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
_cacheDirectory = ResolveCacheDirectory(_options.CacheDirectory);
EnsureCacheDirectory();
}
@@ -597,7 +600,7 @@ public sealed class RuNkckiConnector : IFeedConnector
}
var payload = JsonSerializer.SerializeToUtf8Bytes(dto, SerializerOptions);
var sha = Convert.ToHexString(SHA256.HashData(payload)).ToLowerInvariant();
var sha = _hash.ComputeHashHex(payload);
var documentUri = BuildDocumentUri(dto);
var existing = await _documentStore.FindBySourceAndUriAsync(SourceName, documentUri, cancellationToken).ConfigureAwait(false);

View File

@@ -18,6 +18,7 @@
<ProjectReference Include="../StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -8,10 +8,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0-rc.2.25502.107" />
<ProjectReference Include="../StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Normalization/StellaOps.Concelier.Normalization.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.StellaOpsMirror.Client;
@@ -15,9 +14,10 @@ using StellaOps.Concelier.Connector.StellaOpsMirror.Settings;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Plugin;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Plugin;
using StellaOps.Cryptography;
namespace StellaOps.Concelier.Connector.StellaOpsMirror;
@@ -30,12 +30,13 @@ public sealed class StellaOpsMirrorConnector : IFeedConnector
private readonly MirrorSignatureVerifier _signatureVerifier;
private readonly RawDocumentStorage _rawDocumentStorage;
private readonly IDocumentStore _documentStore;
private readonly IDtoStore _dtoStore;
private readonly IAdvisoryStore _advisoryStore;
private readonly ISourceStateRepository _stateRepository;
private readonly TimeProvider _timeProvider;
private readonly ILogger<StellaOpsMirrorConnector> _logger;
private readonly StellaOpsMirrorConnectorOptions _options;
private readonly IDtoStore _dtoStore;
private readonly IAdvisoryStore _advisoryStore;
private readonly ISourceStateRepository _stateRepository;
private readonly TimeProvider _timeProvider;
private readonly ILogger<StellaOpsMirrorConnector> _logger;
private readonly StellaOpsMirrorConnectorOptions _options;
private readonly ICryptoHash _hash;
public StellaOpsMirrorConnector(
MirrorManifestClient client,
@@ -45,20 +46,22 @@ public sealed class StellaOpsMirrorConnector : IFeedConnector
IDtoStore dtoStore,
IAdvisoryStore advisoryStore,
ISourceStateRepository stateRepository,
IOptions<StellaOpsMirrorConnectorOptions> options,
TimeProvider? timeProvider,
ILogger<StellaOpsMirrorConnector> logger)
IOptions<StellaOpsMirrorConnectorOptions> options,
TimeProvider? timeProvider,
ICryptoHash cryptoHash,
ILogger<StellaOpsMirrorConnector> logger)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_signatureVerifier = signatureVerifier ?? throw new ArgumentNullException(nameof(signatureVerifier));
_rawDocumentStorage = rawDocumentStorage ?? throw new ArgumentNullException(nameof(rawDocumentStorage));
_documentStore = documentStore ?? throw new ArgumentNullException(nameof(documentStore));
_dtoStore = dtoStore ?? throw new ArgumentNullException(nameof(dtoStore));
_advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore));
_stateRepository = stateRepository ?? throw new ArgumentNullException(nameof(stateRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value ?? throw new ArgumentNullException(nameof(options));
_advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore));
_stateRepository = stateRepository ?? throw new ArgumentNullException(nameof(stateRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
_hash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value ?? throw new ArgumentNullException(nameof(options));
ValidateOptions(_options);
}
@@ -280,7 +283,7 @@ public sealed class StellaOpsMirrorConnector : IFeedConnector
await _stateRepository.UpdateCursorAsync(Source, document, now, cancellationToken).ConfigureAwait(false);
}
private static void VerifyDigest(string expected, ReadOnlySpan<byte> payload, string path)
private void VerifyDigest(string expected, ReadOnlySpan<byte> payload, string path)
{
if (string.IsNullOrWhiteSpace(expected))
{
@@ -292,19 +295,16 @@ public sealed class StellaOpsMirrorConnector : IFeedConnector
throw new InvalidOperationException($"Unsupported digest '{expected}' for '{path}'.");
}
var actualHash = SHA256.HashData(payload);
var actual = "sha256:" + Convert.ToHexString(actualHash).ToLowerInvariant();
var actualHash = _hash.ComputeHashHex(payload, HashAlgorithms.Sha256);
var actual = "sha256:" + actualHash;
if (!string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Digest mismatch for '{path}'. Expected {expected}, computed {actual}.");
}
}
private static string ComputeSha256(ReadOnlySpan<byte> payload)
{
var hash = SHA256.HashData(payload);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private string ComputeSha256(ReadOnlySpan<byte> payload)
=> _hash.ComputeHashHex(payload, HashAlgorithms.Sha256);
private static string NormalizeDigest(string digest)
{

View File

@@ -5,6 +5,7 @@ using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common.Packages;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Concelier.Normalization.SemVer;
namespace StellaOps.Concelier.Connector.Vndr.Cisco.Internal;
@@ -142,8 +143,9 @@ public static class CiscoMapper
continue;
}
var range = BuildVersionRange(product, recordedAt);
var ranges = BuildVersionRanges(product, recordedAt);
var statuses = BuildStatuses(product, recordedAt);
var normalizedVersions = BuildNormalizedVersions(product, ranges);
var provenance = new[]
{
new AdvisoryProvenance(
@@ -157,10 +159,10 @@ public static class CiscoMapper
type: AffectedPackageTypes.Vendor,
identifier: product.Name,
platform: null,
versionRanges: range is null ? Array.Empty<AffectedVersionRange>() : new[] { range },
versionRanges: ranges,
statuses: statuses,
provenance: provenance,
normalizedVersions: Array.Empty<NormalizedVersionRule>()));
normalizedVersions: normalizedVersions));
}
return packages.Count == 0
@@ -168,14 +170,46 @@ public static class CiscoMapper
: packages.OrderBy(static p => p.Identifier, StringComparer.OrdinalIgnoreCase).ToArray();
}
private static AffectedVersionRange? BuildVersionRange(CiscoAffectedProductDto product, DateTimeOffset recordedAt)
private static IReadOnlyList<AffectedVersionRange> BuildVersionRanges(CiscoAffectedProductDto product, DateTimeOffset recordedAt)
{
if (string.IsNullOrWhiteSpace(product.Version))
{
return null;
return Array.Empty<AffectedVersionRange>();
}
var version = product.Version.Trim();
var provenance = new AdvisoryProvenance(
VndrCiscoConnectorPlugin.SourceName,
"range",
product.ProductId ?? product.Name,
recordedAt);
var vendorExtensions = BuildVendorExtensions(product, includeVersion: true);
var semVerResults = SemVerRangeRuleBuilder.Build(version, patchedVersion: null, provenanceNote: BuildNormalizedVersionNote(product));
if (semVerResults.Count > 0)
{
var ranges = new List<AffectedVersionRange>(semVerResults.Count);
foreach (var result in semVerResults)
{
var semVerPrimitives = new RangePrimitives(
SemVer: result.Primitive,
Nevra: null,
Evr: null,
VendorExtensions: vendorExtensions);
ranges.Add(new AffectedVersionRange(
rangeKind: NormalizedVersionSchemes.SemVer,
introducedVersion: result.Primitive.Introduced,
fixedVersion: result.Primitive.Fixed,
lastAffectedVersion: result.Primitive.LastAffected,
rangeExpression: result.Expression ?? version,
provenance: provenance,
primitives: semVerPrimitives));
}
return ranges;
}
RangePrimitives? primitives = null;
string rangeKind = "vendor";
string? rangeExpression = version;
@@ -198,23 +232,20 @@ public static class CiscoMapper
}
else
{
primitives = new RangePrimitives(null, null, null, BuildVendorExtensions(product, includeVersion: true));
primitives = new RangePrimitives(null, null, null, vendorExtensions);
}
var provenance = new AdvisoryProvenance(
VndrCiscoConnectorPlugin.SourceName,
"range",
product.ProductId ?? product.Name,
recordedAt);
return new AffectedVersionRange(
return new[]
{
new AffectedVersionRange(
rangeKind: rangeKind,
introducedVersion: null,
fixedVersion: null,
lastAffectedVersion: null,
rangeExpression: rangeExpression,
provenance: provenance,
primitives: primitives);
primitives: primitives),
};
}
private static IReadOnlyDictionary<string, string>? BuildVendorExtensions(CiscoAffectedProductDto product, bool includeVersion = false)
@@ -233,6 +264,48 @@ public static class CiscoMapper
return dictionary.Count == 0 ? null : dictionary;
}
private static IReadOnlyList<NormalizedVersionRule> BuildNormalizedVersions(
CiscoAffectedProductDto product,
IReadOnlyList<AffectedVersionRange> ranges)
{
if (ranges.Count == 0)
{
return Array.Empty<NormalizedVersionRule>();
}
var note = BuildNormalizedVersionNote(product);
var rules = new List<NormalizedVersionRule>(ranges.Count);
foreach (var range in ranges)
{
var rule = range.ToNormalizedVersionRule(note);
if (rule is not null)
{
rules.Add(rule);
}
}
return rules.Count == 0 ? Array.Empty<NormalizedVersionRule>() : rules.ToArray();
}
private static string? BuildNormalizedVersionNote(CiscoAffectedProductDto product)
{
if (!string.IsNullOrWhiteSpace(product.ProductId))
{
return $"cisco:{product.ProductId.Trim().ToLowerInvariant()}";
}
if (!string.IsNullOrWhiteSpace(product.Name))
{
var normalized = product.Name
.Trim()
.ToLowerInvariant()
.Replace(' ', '-');
return $"cisco:{normalized}";
}
return null;
}
private static IReadOnlyList<AffectedPackageStatus> BuildStatuses(CiscoAffectedProductDto product, DateTimeOffset recordedAt)
{
if (product.Statuses is null || product.Statuses.Count == 0)

View File

@@ -1,4 +1,4 @@
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-CISCO-02-009 SemVer range provenance|BE-Conn-Cisco|CONCELIER-LNM-21-001|**TODO (due 2025-10-21)** Emit Cisco SemVer ranges into `advisory_observations.affected.versions[]` with provenance identifiers (`cisco:{productId}`) and deterministic comparison keys. Update mapper/tests for the Link-Not-Merge schema and replace legacy merge counter checks with observation/linkset validation.|
|FEEDCONN-CISCO-02-009 SemVer range provenance|BE-Conn-Cisco|CONCELIER-LNM-21-001|**DOING (2025-11-08)** Emitting Cisco SemVer ranges into `advisory_observations.affected.versions[]` with provenance identifiers (`cisco:{productId}`) and deterministic comparison keys. Updating mapper/tests for the Link-Not-Merge schema and replacing legacy merge counter checks with observation/linkset validation.|

View File

@@ -1,35 +1,94 @@
using System.Text.Json;
using Microsoft.Extensions.Options;
using StellaOps.Aoc;
using StellaOps.Concelier.RawModels;
namespace StellaOps.Concelier.Core.Aoc;
/// <summary>
/// Aggregation-Only Contract guard applied to raw advisory documents prior to persistence.
/// </summary>
public sealed class AdvisoryRawWriteGuard : IAdvisoryRawWriteGuard
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private readonly IAocGuard _guard;
private readonly AocGuardOptions _options;
public AdvisoryRawWriteGuard(IAocGuard guard, IOptions<AocGuardOptions>? options = null)
{
_guard = guard ?? throw new ArgumentNullException(nameof(guard));
_options = options?.Value ?? AocGuardOptions.Default;
}
public void EnsureValid(AdvisoryRawDocument document)
{
ArgumentNullException.ThrowIfNull(document);
using var payload = JsonDocument.Parse(JsonSerializer.Serialize(document, SerializerOptions));
var result = _guard.Validate(payload.RootElement, _options);
if (!result.IsValid)
{
throw new ConcelierAocGuardException(result);
}
}
}
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text.Json;
using Microsoft.Extensions.Options;
using StellaOps.Aoc;
using StellaOps.Concelier.RawModels;
using StellaOps.Ingestion.Telemetry;
namespace StellaOps.Concelier.Core.Aoc;
/// <summary>
/// Aggregation-Only Contract guard applied to raw advisory documents prior to persistence.
/// </summary>
public sealed class AdvisoryRawWriteGuard : IAdvisoryRawWriteGuard
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private readonly IAocGuard _guard;
private readonly AocGuardOptions _options;
public AdvisoryRawWriteGuard(IAocGuard guard, IOptions<AocGuardOptions>? options = null)
{
_guard = guard ?? throw new ArgumentNullException(nameof(guard));
_options = options?.Value ?? AocGuardOptions.Default;
}
public void EnsureValid(AdvisoryRawDocument document)
{
ArgumentNullException.ThrowIfNull(document);
var normalized = NormalizeDocument(document);
var serialized = JsonSerializer.Serialize(normalized, SerializerOptions);
using var guardActivity = IngestionTelemetry.StartGuardActivity(
normalized.Tenant,
normalized.Source.Vendor,
normalized.Upstream.UpstreamId,
normalized.Upstream.ContentHash,
normalized.Supersedes);
using var payload = JsonDocument.Parse(serialized);
var result = _guard.Validate(payload.RootElement, _options);
if (!result.IsValid)
{
var violationCount = result.Violations.IsDefaultOrEmpty ? 0 : result.Violations.Length;
var primaryCode = violationCount > 0 ? result.Violations[0].ErrorCode : string.Empty;
guardActivity?.SetTag("violationCount", violationCount);
if (!string.IsNullOrWhiteSpace(primaryCode))
{
guardActivity?.SetTag("code", primaryCode);
}
guardActivity?.SetStatus(ActivityStatusCode.Error, primaryCode);
throw new ConcelierAocGuardException(result);
}
guardActivity?.SetTag("violationCount", 0);
guardActivity?.SetStatus(ActivityStatusCode.Ok);
}
private static AdvisoryRawDocument NormalizeDocument(AdvisoryRawDocument document)
{
var identifiers = document.Identifiers with
{
Aliases = Normalize(document.Identifiers.Aliases)
};
var linkset = document.Linkset with
{
Aliases = Normalize(document.Linkset.Aliases),
PackageUrls = Normalize(document.Linkset.PackageUrls),
Cpes = Normalize(document.Linkset.Cpes),
References = Normalize(document.Linkset.References),
ReconciledFrom = Normalize(document.Linkset.ReconciledFrom),
Notes = Normalize(document.Linkset.Notes)
};
return document with
{
Identifiers = identifiers,
Linkset = linkset,
Links = Normalize(document.Links)
};
}
private static ImmutableArray<T> Normalize<T>(ImmutableArray<T> value) =>
value.IsDefault ? ImmutableArray<T>.Empty : value;
private static ImmutableDictionary<TKey, TValue> Normalize<TKey, TValue>(ImmutableDictionary<TKey, TValue> value)
where TKey : notnull =>
value == default ? ImmutableDictionary<TKey, TValue>.Empty : value;
}

View File

@@ -1,12 +1,14 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Linq;
using Microsoft.Extensions.Logging;
using StellaOps.Aoc;
using StellaOps.Concelier.Core.Aoc;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.Aoc;
using StellaOps.Ingestion.Telemetry;
using StellaOps.Concelier.Core.Aoc;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.Models;
@@ -40,55 +42,104 @@ internal sealed class AdvisoryRawService : IAdvisoryRawService
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<AdvisoryRawUpsertResult> IngestAsync(AdvisoryRawDocument document, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(document);
var clientSupersedes = string.IsNullOrWhiteSpace(document.Supersedes)
? null
: document.Supersedes.Trim();
var normalized = Normalize(document);
var enriched = normalized with { Linkset = _linksetMapper.Map(normalized) };
if (!string.IsNullOrEmpty(clientSupersedes))
{
_logger.LogWarning(
"Ignoring client-supplied supersedes pointer for advisory_raw tenant={Tenant} source={Vendor} upstream={UpstreamId} pointer={Supersedes}",
enriched.Tenant,
enriched.Source.Vendor,
enriched.Upstream.UpstreamId,
clientSupersedes);
}
_writeGuard.EnsureValid(enriched);
var result = await _repository.UpsertAsync(enriched, cancellationToken).ConfigureAwait(false);
if (result.Inserted)
{
_logger.LogInformation(
"Ingested advisory_raw document id={DocumentId} tenant={Tenant} source={Vendor} upstream={UpstreamId} hash={Hash} supersedes={Supersedes}",
result.Record.Id,
result.Record.Document.Tenant,
result.Record.Document.Source.Vendor,
result.Record.Document.Upstream.UpstreamId,
result.Record.Document.Upstream.ContentHash,
string.IsNullOrWhiteSpace(result.Record.Document.Supersedes)
? "(none)"
: result.Record.Document.Supersedes);
}
else
{
_logger.LogDebug(
"Skipped advisory_raw duplicate tenant={Tenant} source={Vendor} upstream={UpstreamId} hash={Hash}",
result.Record.Document.Tenant,
result.Record.Document.Source.Vendor,
result.Record.Document.Upstream.UpstreamId,
result.Record.Document.Upstream.ContentHash);
}
return result;
}
public async Task<AdvisoryRawUpsertResult> IngestAsync(AdvisoryRawDocument document, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(document);
var clientSupersedes = string.IsNullOrWhiteSpace(document.Supersedes)
? null
: document.Supersedes.Trim();
var transformWatch = Stopwatch.StartNew();
var initialPayloadBytes = EstimatePayloadBytes(document.Content.Raw);
using var transformActivity = IngestionTelemetry.StartTransformActivity(
document.Tenant,
document.Source.Vendor,
document.Upstream.UpstreamId,
document.Upstream.ContentHash,
document.Content.Format,
initialPayloadBytes);
var normalized = Normalize(document);
var enriched = normalized with { Linkset = _linksetMapper.Map(normalized) };
transformWatch.Stop();
var tenant = enriched.Tenant;
var source = enriched.Source.Vendor;
var upstreamId = enriched.Upstream.UpstreamId;
var contentHash = enriched.Upstream.ContentHash;
if (!string.IsNullOrEmpty(clientSupersedes))
{
_logger.LogWarning(
"Ignoring client-supplied supersedes pointer for advisory_raw tenant={Tenant} source={Vendor} upstream={UpstreamId} pointer={Supersedes}",
tenant,
source,
upstreamId,
clientSupersedes);
}
transformActivity?.SetTag("tenant", tenant);
transformActivity?.SetTag("source", source);
transformActivity?.SetTag("upstream.id", upstreamId);
transformActivity?.SetTag("contentHash", contentHash);
transformActivity?.SetTag("documentType", enriched.Content.Format);
transformActivity?.SetTag("payloadBytes", initialPayloadBytes);
IngestionTelemetry.RecordLatency(tenant, source, IngestionTelemetry.PhaseTransform, transformWatch.Elapsed);
try
{
_writeGuard.EnsureValid(enriched);
}
catch (ConcelierAocGuardException guardException)
{
IngestionTelemetry.RecordViolation(tenant, source, guardException.PrimaryErrorCode);
IngestionTelemetry.RecordWriteAttempt(tenant, source, IngestionTelemetry.ResultReject);
throw;
}
var result = await _repository.UpsertAsync(enriched, cancellationToken).ConfigureAwait(false);
IngestionTelemetry.RecordWriteAttempt(tenant, source, result.Inserted ? IngestionTelemetry.ResultOk : IngestionTelemetry.ResultNoop);
if (result.Inserted)
{
_logger.LogInformation(
"Ingested advisory_raw document id={DocumentId} tenant={Tenant} source={Vendor} upstream={UpstreamId} hash={Hash} supersedes={Supersedes}",
result.Record.Id,
tenant,
source,
upstreamId,
contentHash,
string.IsNullOrWhiteSpace(result.Record.Document.Supersedes)
? "(none)"
: result.Record.Document.Supersedes);
}
else
{
_logger.LogDebug(
"Skipped advisory_raw duplicate tenant={Tenant} source={Vendor} upstream={UpstreamId} hash={Hash}",
tenant,
source,
upstreamId,
contentHash);
}
return result;
}
private static long EstimatePayloadBytes(JsonElement element)
{
try
{
var text = element.GetRawText();
return Encoding.UTF8.GetByteCount(text);
}
catch (InvalidOperationException)
{
return 0;
}
}
public Task<AdvisoryRawRecord?> FindByIdAsync(string tenant, string id, CancellationToken cancellationToken)
{

View File

@@ -18,7 +18,8 @@
<ProjectReference Include="..\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="..\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
<ProjectReference Include="..\StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
<ProjectReference Include="../../../Aoc/__Libraries/StellaOps.Aoc/StellaOps.Aoc.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -78,7 +78,7 @@
## Observability & Forensics (Epic 15)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| CONCELIER-OBS-50-001 `Telemetry adoption` | TODO | Concelier Core Guild, Observability Guild | TELEMETRY-OBS-50-001, TELEMETRY-OBS-50-002 | Replace ad-hoc logging with telemetry core across ingestion/linking pipelines; ensure spans/logs include tenant, source vendor, upstream id, content hash, and trace IDs. |
| CONCELIER-OBS-50-001 `Telemetry adoption` | DONE (2025-11-07) | Concelier Core Guild, Observability Guild | TELEMETRY-OBS-50-001, TELEMETRY-OBS-50-002 | Replace ad-hoc logging with telemetry core across ingestion/linking pipelines; ensure spans/logs include tenant, source vendor, upstream id, content hash, and trace IDs. |
| CONCELIER-OBS-51-001 `Metrics & SLOs` | TODO | Concelier Core Guild, DevOps Guild | CONCELIER-OBS-50-001, TELEMETRY-OBS-51-001 | Emit metrics for ingest latency (cold/warm), queue depth, aoc violation rate, and publish SLO burn-rate alerts (ingest P95 <30s cold / <5s warm). Ship dashboards + alert configs. |
| CONCELIER-OBS-52-001 `Timeline events` | TODO | Concelier Core Guild | CONCELIER-OBS-50-001, TIMELINE-OBS-52-002 | Emit `timeline_event` records for advisory ingest/normalization/linkset creation with provenance, trace IDs, conflict summaries, and evidence placeholders. |
| CONCELIER-OBS-53-001 `Evidence snapshots` | TODO | Concelier Core Guild, Evidence Locker Guild | CONCELIER-OBS-52-001, EVID-OBS-53-002 | Produce advisory evaluation bundle payloads (raw doc, linkset, normalization diff) for evidence locker; ensure Merkle manifests seeded with content hashes. |

View File

@@ -1,28 +1,33 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Exporter.Json;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using StellaOps.Concelier.Models;
using StellaOps.Cryptography;
namespace StellaOps.Concelier.Exporter.Json;
/// <summary>
/// Writes canonical advisory snapshots into a vuln-list style directory tree with deterministic ordering.
/// </summary>
public sealed class JsonExportSnapshotBuilder
{
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private readonly JsonExportOptions _options;
private readonly IJsonExportPathResolver _pathResolver;
public JsonExportSnapshotBuilder(JsonExportOptions options, IJsonExportPathResolver pathResolver)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_pathResolver = pathResolver ?? throw new ArgumentNullException(nameof(pathResolver));
}
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private readonly JsonExportOptions _options;
private readonly IJsonExportPathResolver _pathResolver;
private readonly ICryptoHash _hash;
public JsonExportSnapshotBuilder(
JsonExportOptions options,
IJsonExportPathResolver pathResolver,
ICryptoHash? hash = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_pathResolver = pathResolver ?? throw new ArgumentNullException(nameof(pathResolver));
_hash = hash ?? CryptoHashFactory.CreateDefault();
}
public Task<JsonExportResult> WriteAsync(
IReadOnlyCollection<Advisory> advisories,
@@ -97,7 +102,7 @@ public sealed class JsonExportSnapshotBuilder
await File.WriteAllBytesAsync(destination, bytes, cancellationToken).ConfigureAwait(false);
File.SetLastWriteTimeUtc(destination, exportedAt.UtcDateTime);
var digest = ComputeDigest(bytes);
var digest = ComputeDigest(bytes);
files.Add(new JsonExportFile(entry.RelativePath, bytes.LongLength, digest));
totalBytes += bytes.LongLength;
}
@@ -232,10 +237,9 @@ public sealed class JsonExportSnapshotBuilder
private sealed record PathResolution(Advisory Advisory, string RelativePath, IReadOnlyList<string> Segments);
private static string ComputeDigest(ReadOnlySpan<byte> payload)
{
var hash = SHA256.HashData(payload);
var hex = Convert.ToHexString(hash).ToLowerInvariant();
return $"sha256:{hex}";
}
}
private string ComputeDigest(ReadOnlySpan<byte> payload)
{
var hex = _hash.ComputeHashHex(payload, HashAlgorithms.Sha256);
return $"sha256:{hex}";
}
}

View File

@@ -4,12 +4,14 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.Core.Events;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Exporting;
using StellaOps.Cryptography;
using StellaOps.Plugin;
namespace StellaOps.Concelier.Exporter.Json;
@@ -51,15 +53,16 @@ public sealed class JsonFeedExporter : IFeedExporter
public async Task ExportAsync(IServiceProvider services, CancellationToken cancellationToken)
{
var exportedAt = _timeProvider.GetUtcNow();
var exportId = exportedAt.ToString(_options.DirectoryNameFormat, CultureInfo.InvariantCulture);
var exportRoot = Path.GetFullPath(_options.OutputRoot);
var exportedAt = _timeProvider.GetUtcNow();
var exportId = exportedAt.ToString(_options.DirectoryNameFormat, CultureInfo.InvariantCulture);
var exportRoot = Path.GetFullPath(_options.OutputRoot);
_logger.LogInformation("Starting JSON export {ExportId}", exportId);
var existingState = await _stateManager.GetAsync(ExporterId, cancellationToken).ConfigureAwait(false);
var builder = new JsonExportSnapshotBuilder(_options, _pathResolver);
var existingState = await _stateManager.GetAsync(ExporterId, cancellationToken).ConfigureAwait(false);
var cryptoHash = services.GetRequiredService<ICryptoHash>();
var builder = new JsonExportSnapshotBuilder(_options, _pathResolver, cryptoHash);
var canonicalAdvisories = await MaterializeCanonicalAdvisoriesAsync(cancellationToken).ConfigureAwait(false);
var result = await builder.WriteAsync(canonicalAdvisories, exportedAt, exportId, cancellationToken).ConfigureAwait(false);
result = await JsonMirrorBundleWriter.WriteAsync(result, _options, services, _timeProvider, _logger, cancellationToken).ConfigureAwait(false);

View File

@@ -50,7 +50,8 @@ internal static class JsonMirrorBundleWriter
ArgumentNullException.ThrowIfNull(timeProvider);
ArgumentNullException.ThrowIfNull(logger);
var mirrorOptions = options.Mirror ?? new JsonExportOptions.JsonMirrorOptions();
var cryptoHash = services.GetRequiredService<ICryptoHash>();
var mirrorOptions = options.Mirror ?? new JsonExportOptions.JsonMirrorOptions();
if (!mirrorOptions.Enabled || mirrorOptions.Domains.Count == 0)
{
return result;
@@ -123,7 +124,7 @@ internal static class JsonMirrorBundleWriter
await WriteFileAsync(bundlePath, bundleBytes, exportedAtUtc, cancellationToken).ConfigureAwait(false);
var bundleRelativePath = ToRelativePath(result.ExportDirectory, bundlePath);
var bundleDigest = ComputeDigest(bundleBytes);
var bundleDigest = ComputeDigest(cryptoHash, bundleBytes);
var bundleLength = (long)bundleBytes.LongLength;
additionalFiles.Add(new JsonExportFile(bundleRelativePath, bundleLength, bundleDigest));
@@ -142,7 +143,7 @@ internal static class JsonMirrorBundleWriter
await WriteFileAsync(signaturePath, signatureBytes, exportedAtUtc, cancellationToken).ConfigureAwait(false);
var signatureRelativePath = ToRelativePath(result.ExportDirectory, signaturePath);
var signatureDigest = ComputeDigest(signatureBytes);
var signatureDigest = ComputeDigest(cryptoHash, signatureBytes);
var signatureLength = (long)signatureBytes.LongLength;
additionalFiles.Add(new JsonExportFile(signatureRelativePath, signatureLength, signatureDigest));
@@ -170,7 +171,7 @@ internal static class JsonMirrorBundleWriter
await WriteFileAsync(manifestPath, manifestBytes, exportedAtUtc, cancellationToken).ConfigureAwait(false);
var manifestRelativePath = ToRelativePath(result.ExportDirectory, manifestPath);
var manifestDigest = ComputeDigest(manifestBytes);
var manifestDigest = ComputeDigest(cryptoHash, manifestBytes);
var manifestLength = (long)manifestBytes.LongLength;
additionalFiles.Add(new JsonExportFile(manifestRelativePath, manifestLength, manifestDigest));
@@ -198,7 +199,7 @@ internal static class JsonMirrorBundleWriter
await WriteFileAsync(indexPath, indexBytes, exportedAtUtc, cancellationToken).ConfigureAwait(false);
var indexRelativePath = ToRelativePath(result.ExportDirectory, indexPath);
var indexDigest = ComputeDigest(indexBytes);
var indexDigest = ComputeDigest(cryptoHash, indexBytes);
var indexLength = (long)indexBytes.LongLength;
additionalFiles.Add(new JsonExportFile(indexRelativePath, indexLength, indexDigest));
@@ -490,11 +491,11 @@ internal static class JsonMirrorBundleWriter
return relative.Replace(Path.DirectorySeparatorChar, '/');
}
private static string ComputeDigest(ReadOnlySpan<byte> payload)
{
var hash = SHA256.HashData(payload);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static string ComputeDigest(ICryptoHash hash, ReadOnlySpan<byte> payload)
{
var hex = hash.ComputeHashHex(payload, HashAlgorithms.Sha256);
return $"sha256:{hex}";
}
private static void TrySetDirectoryTimestamp(string directory, DateTime exportedAtUtc)
{

View File

@@ -436,14 +436,14 @@ public sealed class VulnListJsonExportPathResolver : IJsonExportPathResolver
var invalid = Path.GetInvalidFileNameChars();
Span<char> buffer = stackalloc char[name.Length];
var count = 0;
foreach (var ch in name)
{
if (ch == '/' || ch == '\\' || Array.IndexOf(invalid, ch) >= 0)
{
buffer[count++] = '_';
}
else
{
foreach (var ch in name)
{
if (ch == '/' || ch == '\\' || ch == ':' || Array.IndexOf(invalid, ch) >= 0)
{
buffer[count++] = '_';
}
else
{
buffer[count++] = ch;
}
}

View File

@@ -12,4 +12,4 @@
|MERGE-LNM-21-001 Migration plan authoring|BE-Merge, Architecture Guild|CONCELIER-LNM-21-101|**DONE (2025-11-03)** Authored `docs/migration/no-merge.md` with rollout phases, backfill/validation checklists, rollback guidance, and ownership matrix for the Link-Not-Merge cutover.|
|MERGE-LNM-21-002 Merge service deprecation|BE-Merge|MERGE-LNM-21-001|**DONE (2025-11-07)** Feature flag now defaults to Link-Not-Merge mode (`NoMergeEnabled=true`) across options/config, analyzers enforce deprecation, and WebService option tests cover the regression; dotnet CLI validation still queued for a workstation with preview SDK.<br>2025-11-05 14:42Z: Implemented `concelier:features:noMergeEnabled` gate, merge job allowlist checks, `[Obsolete]` markings, and analyzer scaffolding to steer consumers toward linkset APIs.<br>2025-11-06 16:10Z: Introduced Roslyn analyzer (`CONCELIER0002`) referenced by Concelier WebService + tests, documented suppression guidance, and updated migration playbook.<br>2025-11-07 03:25Z: Default-on toggle + job gating surfacing ingestion test brittleness; guard logs capture requests missing `upstream.contentHash`.<br>2025-11-07 19:45Z: Set `ConcelierOptions.Features.NoMergeEnabled` default to `true`, added regression coverage (`Features_NoMergeEnabled_DefaultsToTrue`), and rechecked ingest helpers to carry canonical links before closing the task.|
> 2025-11-03: Catalogued call sites (WebService Program `AddMergeModule`, built-in job registration `merge:reconcile`, `MergeReconcileJob`) and confirmed unit tests are the only direct `MergeAsync` callers; next step is to define analyzer + replacement observability coverage.
|MERGE-LNM-21-003 Determinism/test updates|QA Guild, BE-Merge|MERGE-LNM-21-002|**DOING (2025-11-07)** Replacing legacy merge determinism harness with observation/linkset regression plan; tracking scenarios in `docs/dev/lnm-determinism-tests.md` before porting fixtures.<br>2025-11-07 20:05Z: Ported merge determinism fixture into `AdvisoryObservationFactoryTests.Create_IsDeterministicAcrossRuns` and removed the redundant merge integration test.|
|MERGE-LNM-21-003 Determinism/test updates|QA Guild, BE-Merge|MERGE-LNM-21-002|**DONE (2025-11-07)** Legacy merge determinism suite replaced by observation/linkset/export regressions. Added coverage across `AdvisoryObservationFactoryTests` (raw references + conflict notes), `AdvisoryEventLogTests` (sorted statement IDs), and `JsonExportSnapshotBuilderTests` (order-independent digests). `docs/dev/lnm-determinism-tests.md` updated to reflect parity.|

View File

@@ -21,14 +21,14 @@ public static class RawDocumentFactory
return new AdvisoryRawDocument(tenant, source, upstream, clonedContent, identifiers, linkset, advisoryKey, normalizedLinks, supersedes);
}
public static VexRawDocument CreateVex(
string tenant,
RawSourceMetadata source,
RawUpstreamMetadata upstream,
RawContent content,
RawLinkset linkset,
ImmutableArray<VexStatementSummary> statements,
string? supersedes = null)
public static VexRawDocument CreateVex(
string tenant,
RawSourceMetadata source,
RawUpstreamMetadata upstream,
RawContent content,
RawLinkset linkset,
ImmutableArray<VexStatementSummary>? statements = null,
string? supersedes = null)
{
var clonedContent = content with { Raw = Clone(content.Raw) };
return new VexRawDocument(tenant, source, upstream, clonedContent, linkset, statements, supersedes);

View File

@@ -3,15 +3,17 @@ using System.Text.Json.Serialization;
namespace StellaOps.Concelier.RawModels;
public sealed record VexRawDocument(
[property: JsonPropertyName("tenant")] string Tenant,
[property: JsonPropertyName("source")] RawSourceMetadata Source,
[property: JsonPropertyName("upstream")] RawUpstreamMetadata Upstream,
[property: JsonPropertyName("content")] RawContent Content,
[property: JsonPropertyName("linkset")] RawLinkset Linkset,
[property: JsonPropertyName("statements")] ImmutableArray<VexStatementSummary> Statements,
[property: JsonPropertyName("supersedes")] string? Supersedes = null)
{
public sealed record VexRawDocument(
[property: JsonPropertyName("tenant")] string Tenant,
[property: JsonPropertyName("source")] RawSourceMetadata Source,
[property: JsonPropertyName("upstream")] RawUpstreamMetadata Upstream,
[property: JsonPropertyName("content")] RawContent Content,
[property: JsonPropertyName("linkset")] RawLinkset Linkset,
[property: JsonPropertyName("statements")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
ImmutableArray<VexStatementSummary>? Statements = null,
[property: JsonPropertyName("supersedes")] string? Supersedes = null)
{
public VexRawDocument WithSupersedes(string supersedes)
=> this with { Supersedes = supersedes };
}

View File

@@ -126,7 +126,7 @@ public sealed class EnsureAdvisoryCanonicalKeyBackfillMigration : IMongoMigratio
return string.Empty;
}
return value.IsString ? value.AsString : value.ToString();
return value.IsString ? value.AsString : value.ToString() ?? string.Empty;
}
private static string? GetOptionalString(BsonDocument document, string name)
@@ -150,7 +150,7 @@ public sealed class EnsureAdvisoryCanonicalKeyBackfillMigration : IMongoMigratio
BsonInt32 i => i.AsInt32.ToString(CultureInfo.InvariantCulture),
BsonInt64 l => l.AsInt64.ToString(CultureInfo.InvariantCulture),
BsonDouble d => d.AsDouble.ToString(CultureInfo.InvariantCulture),
_ => value.ToString()
_ => value?.ToString() ?? string.Empty
};
}

View File

@@ -157,7 +157,7 @@ public sealed class EnsureAdvisoryObservationsRawLinksetMigration : IMongoMigrat
content,
identifiers,
linkset,
supersedes.IsBsonNull ? null : supersedes.AsString);
Supersedes: supersedes.IsBsonNull ? null : supersedes.AsString);
}
private static RawSourceMetadata MapSource(BsonDocument source)

View File

@@ -90,12 +90,27 @@ public sealed class MongoBootstrapper
_logger.LogInformation("Mongo bootstrapper completed");
}
private async Task<HashSet<string>> ListCollectionsAsync(CancellationToken cancellationToken)
{
using var cursor = await _database.ListCollectionNamesAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
var list = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false);
return new HashSet<string>(list, StringComparer.Ordinal);
}
private async Task<HashSet<string>> ListCollectionsAsync(CancellationToken cancellationToken)
{
using var cursor = await _database.ListCollectionNamesAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
var list = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false);
return new HashSet<string>(list, StringComparer.Ordinal);
}
private async Task<bool> CollectionIsViewAsync(string collectionName, CancellationToken cancellationToken)
{
var filter = Builders<BsonDocument>.Filter.Eq("name", collectionName);
var options = new ListCollectionsOptions { Filter = filter };
using var cursor = await _database.ListCollectionsAsync(options, cancellationToken).ConfigureAwait(false);
var collections = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false);
if (collections.Count == 0)
{
return false;
}
var typeValue = collections[0].GetValue("type", BsonString.Empty).AsString;
return string.Equals(typeValue, "view", StringComparison.OrdinalIgnoreCase);
}
private Task EnsureLocksIndexesAsync(CancellationToken cancellationToken)
{
@@ -129,9 +144,15 @@ public sealed class MongoBootstrapper
return collection.Indexes.CreateManyAsync(indexes, cancellationToken);
}
private Task EnsureAdvisoryIndexesAsync(CancellationToken cancellationToken)
{
var collection = _database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Advisory);
private async Task EnsureAdvisoryIndexesAsync(CancellationToken cancellationToken)
{
if (await CollectionIsViewAsync(MongoStorageDefaults.Collections.Advisory, cancellationToken).ConfigureAwait(false))
{
_logger.LogDebug("Skipping advisory index creation because {Collection} is a view", MongoStorageDefaults.Collections.Advisory);
return;
}
var collection = _database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Advisory);
var indexes = new List<CreateIndexModel<BsonDocument>>
{
new(
@@ -159,7 +180,7 @@ public sealed class MongoBootstrapper
new CreateIndexOptions { Name = "advisory_normalizedVersions_value", Sparse = true }));
}
return collection.Indexes.CreateManyAsync(indexes, cancellationToken);
await collection.Indexes.CreateManyAsync(indexes, cancellationToken).ConfigureAwait(false);
}
private Task EnsureDocumentsIndexesAsync(CancellationToken cancellationToken)

View File

@@ -1,16 +1,18 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Text;
using System.Linq;
using System.Text.Json;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Bson.IO;
using Microsoft.Extensions.Logging;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.RawModels;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Driver;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.RawModels;
using StellaOps.Ingestion.Telemetry;
namespace StellaOps.Concelier.Storage.Mongo.Raw;
@@ -34,76 +36,115 @@ internal sealed class MongoAdvisoryRawRepository : IAdvisoryRawRepository
_collection = database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.AdvisoryRaw);
}
public async Task<AdvisoryRawUpsertResult> UpsertAsync(AdvisoryRawDocument document, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(document);
var tenant = document.Tenant;
var vendor = document.Source.Vendor;
var upstreamId = document.Upstream.UpstreamId;
var contentHash = document.Upstream.ContentHash;
var baseFilter = Builders<BsonDocument>.Filter.Eq("tenant", tenant) &
Builders<BsonDocument>.Filter.Eq("source.vendor", vendor) &
Builders<BsonDocument>.Filter.Eq("upstream.upstream_id", upstreamId);
public async Task<AdvisoryRawUpsertResult> UpsertAsync(AdvisoryRawDocument document, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(document);
var tenant = document.Tenant;
var vendor = document.Source.Vendor;
var upstreamId = document.Upstream.UpstreamId;
var contentHash = document.Upstream.ContentHash;
var sourceUri = ResolveProvenanceUri(document);
var baseFilter = Builders<BsonDocument>.Filter.Eq("tenant", tenant) &
Builders<BsonDocument>.Filter.Eq("source.vendor", vendor) &
Builders<BsonDocument>.Filter.Eq("upstream.upstream_id", upstreamId);
var duplicateFilter = baseFilter &
Builders<BsonDocument>.Filter.Eq("upstream.content_hash", contentHash);
var duplicate = await _collection
.Find(duplicateFilter)
.Limit(1)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (duplicate is not null)
{
var existing = MapToRecord(duplicate);
return new AdvisoryRawUpsertResult(false, existing);
}
var previous = await _collection
.Find(baseFilter)
.Sort(Builders<BsonDocument>.Sort.Descending("ingested_at").Descending("_id"))
.Limit(1)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
var supersedesId = previous?["_id"]?.AsString;
var recordDocument = CreateBsonDocument(document, supersedesId);
try
{
await _collection.InsertOneAsync(recordDocument, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
_logger.LogWarning(
ex,
"Duplicate key detected while inserting advisory_raw document tenant={Tenant} vendor={Vendor} upstream={Upstream} hash={Hash}",
tenant,
vendor,
upstreamId,
contentHash);
var existingDoc = await _collection
.Find(duplicateFilter)
.Limit(1)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (existingDoc is not null)
{
var existing = MapToRecord(existingDoc);
return new AdvisoryRawUpsertResult(false, existing);
}
throw;
}
var inserted = MapToRecord(recordDocument);
return new AdvisoryRawUpsertResult(true, inserted);
}
using var fetchActivity = IngestionTelemetry.StartFetchActivity(tenant, vendor, upstreamId, contentHash, sourceUri);
var fetchWatch = Stopwatch.StartNew();
var duplicate = await _collection
.Find(duplicateFilter)
.Limit(1)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (duplicate is not null)
{
fetchWatch.Stop();
fetchActivity?.SetTag("result", "duplicate");
fetchActivity?.SetStatus(ActivityStatusCode.Ok);
IngestionTelemetry.RecordLatency(tenant, vendor, IngestionTelemetry.PhaseFetch, fetchWatch.Elapsed);
var existing = MapToRecord(duplicate);
return new AdvisoryRawUpsertResult(false, existing);
}
var previous = await _collection
.Find(baseFilter)
.Sort(Builders<BsonDocument>.Sort.Descending("ingested_at").Descending("_id"))
.Limit(1)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
fetchWatch.Stop();
fetchActivity?.SetTag("result", previous is null ? "new" : "supersede");
fetchActivity?.SetStatus(ActivityStatusCode.Ok);
IngestionTelemetry.RecordLatency(tenant, vendor, IngestionTelemetry.PhaseFetch, fetchWatch.Elapsed);
var supersedesId = previous?["_id"]?.AsString;
var recordDocument = CreateBsonDocument(document, supersedesId);
var writeWatch = Stopwatch.StartNew();
using var writeActivity = IngestionTelemetry.StartWriteActivity(tenant, vendor, upstreamId, contentHash, MongoStorageDefaults.Collections.AdvisoryRaw);
try
{
await _collection.InsertOneAsync(recordDocument, cancellationToken: cancellationToken).ConfigureAwait(false);
writeActivity?.SetTag("result", IngestionTelemetry.ResultOk);
writeActivity?.SetStatus(ActivityStatusCode.Ok);
}
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
writeActivity?.SetTag("result", IngestionTelemetry.ResultNoop);
writeActivity?.SetStatus(ActivityStatusCode.Error, "duplicate_key");
_logger.LogWarning(
ex,
"Duplicate key detected while inserting advisory_raw document tenant={Tenant} vendor={Vendor} upstream={Upstream} hash={Hash}",
tenant,
vendor,
upstreamId,
contentHash);
var existingDoc = await _collection
.Find(duplicateFilter)
.Limit(1)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (existingDoc is not null)
{
var existing = MapToRecord(existingDoc);
return new AdvisoryRawUpsertResult(false, existing);
}
throw;
}
finally
{
writeWatch.Stop();
IngestionTelemetry.RecordLatency(tenant, vendor, IngestionTelemetry.PhaseWrite, writeWatch.Elapsed);
}
var inserted = MapToRecord(recordDocument);
return new AdvisoryRawUpsertResult(true, inserted);
}
private static string? ResolveProvenanceUri(AdvisoryRawDocument document)
{
if (document.Upstream?.Provenance is null)
{
return null;
}
return document.Upstream.Provenance.TryGetValue("uri", out var uri) && !string.IsNullOrWhiteSpace(uri)
? uri
: null;
}
public async Task<AdvisoryRawRecord?> FindByIdAsync(string tenant, string id, CancellationToken cancellationToken)
{

View File

@@ -11,8 +11,9 @@
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="..\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj" />
</ItemGroup>
</Project>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="..\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,207 @@
[
{
"advisoryKey": "acsc/multi/https-origin-example-advisories-info-bulletin",
"affectedPackages": [],
"aliases": [
"ACSC-2025-011",
"Bulletin",
"https://origin.example/advisories/info-bulletin"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": null,
"provenance": [
{
"source": "acsc",
"kind": "document",
"value": "https://origin.example/feeds/multi/rss",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
},
{
"source": "acsc",
"kind": "feed",
"value": "multi",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"summary"
]
},
{
"source": "acsc",
"kind": "mapping",
"value": "https://origin.example/advisories/info-bulletin",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
}
],
"published": "2025-10-12T02:30:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://origin.example/advisories/info-bulletin",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "multi",
"summary": "Information bulletin",
"url": "https://origin.example/advisories/info-bulletin"
}
],
"severity": null,
"summary": "Serial number: ACSC-2025-011\n\nAdvisory type: Bulletin\n\nGeneral guidance bulletin.",
"title": "Information bulletin"
},
{
"advisoryKey": "acsc/multi/https-origin-example-advisories-router-critical",
"affectedPackages": [
{
"type": "vendor",
"identifier": "ExampleCo Router X",
"platform": null,
"versionRanges": [],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "acsc",
"kind": "affected",
"value": "ExampleCo Router X",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages"
]
}
]
},
{
"type": "vendor",
"identifier": "ExampleCo Router Y",
"platform": null,
"versionRanges": [],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "acsc",
"kind": "affected",
"value": "ExampleCo Router Y",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages"
]
}
]
}
],
"aliases": [
"ACSC-2025-010",
"CVE-2025-0001",
"https://origin.example/advisories/router-critical"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": null,
"provenance": [
{
"source": "acsc",
"kind": "document",
"value": "https://origin.example/feeds/multi/rss",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
},
{
"source": "acsc",
"kind": "feed",
"value": "multi",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"summary"
]
},
{
"source": "acsc",
"kind": "mapping",
"value": "https://origin.example/advisories/router-critical",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
}
],
"published": "2025-10-12T04:45:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://origin.example/advisories/router-critical",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "multi",
"summary": "Critical router vulnerability",
"url": "https://origin.example/advisories/router-critical"
},
{
"kind": "reference",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://vendor.example/router/patch",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": "vendor patch",
"url": "https://vendor.example/router/patch"
}
],
"severity": "critical",
"summary": "Serial number: ACSC-2025-010\n\nSeverity: Critical\n\nSystems affected: ExampleCo Router X, ExampleCo Router Y\n\nRemote code execution on ExampleCo routers. See vendor patch.\n\nCVE references: CVE-2025-0001",
"title": "Critical router vulnerability"
}
]

View File

@@ -1,201 +1,207 @@
[
{
"advisoryKey": "acsc/multi/https-origin-example-advisories-info-bulletin",
"affectedPackages": [],
"aliases": [
"ACSC-2025-011",
"Bulletin",
"https://origin.example/advisories/info-bulletin"
],
"credits": [],
"cvssMetrics": [],
"exploitKnown": false,
"language": "en",
"modified": null,
"provenance": [
{
"source": "acsc",
"kind": "document",
"value": "https://origin.example/feeds/multi/rss",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
},
{
"source": "acsc",
"kind": "feed",
"value": "multi",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"summary"
]
},
{
"source": "acsc",
"kind": "mapping",
"value": "https://origin.example/advisories/info-bulletin",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
}
],
"published": "2025-10-12T02:30:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://origin.example/advisories/info-bulletin",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "multi",
"summary": "Information bulletin",
"url": "https://origin.example/advisories/info-bulletin"
}
],
"severity": null,
"summary": "Serial number: ACSC-2025-011\n\nAdvisory type: Bulletin\n\nGeneral guidance bulletin.",
"title": "Information bulletin"
},
{
"advisoryKey": "acsc/multi/https-origin-example-advisories-router-critical",
"affectedPackages": [
{
"type": "vendor",
"identifier": "ExampleCo Router X",
"platform": null,
"versionRanges": [],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "acsc",
"kind": "affected",
"value": "ExampleCo Router X",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages"
]
}
]
},
{
"type": "vendor",
"identifier": "ExampleCo Router Y",
"platform": null,
"versionRanges": [],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "acsc",
"kind": "affected",
"value": "ExampleCo Router Y",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages"
]
}
]
}
],
"aliases": [
"ACSC-2025-010",
"CVE-2025-0001",
"https://origin.example/advisories/router-critical"
],
"credits": [],
"cvssMetrics": [],
"exploitKnown": false,
"language": "en",
"modified": null,
"provenance": [
{
"source": "acsc",
"kind": "document",
"value": "https://origin.example/feeds/multi/rss",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
},
{
"source": "acsc",
"kind": "feed",
"value": "multi",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"summary"
]
},
{
"source": "acsc",
"kind": "mapping",
"value": "https://origin.example/advisories/router-critical",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
}
],
"published": "2025-10-12T04:45:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://origin.example/advisories/router-critical",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "multi",
"summary": "Critical router vulnerability",
"url": "https://origin.example/advisories/router-critical"
},
{
"kind": "reference",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://vendor.example/router/patch",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": "vendor patch",
"url": "https://vendor.example/router/patch"
}
],
"severity": "critical",
"summary": "Serial number: ACSC-2025-010\n\nSeverity: Critical\n\nSystems affected: ExampleCo Router X, ExampleCo Router Y\n\nRemote code execution on ExampleCo routers. See vendor patch.\n\nCVE references: CVE-2025-0001",
"title": "Critical router vulnerability"
}
[
{
"advisoryKey": "acsc/multi/https-origin-example-advisories-info-bulletin",
"affectedPackages": [],
"aliases": [
"ACSC-2025-011",
"Bulletin",
"https://origin.example/advisories/info-bulletin"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": null,
"provenance": [
{
"source": "acsc",
"kind": "document",
"value": "https://origin.example/feeds/multi/rss",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
},
{
"source": "acsc",
"kind": "feed",
"value": "multi",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"summary"
]
},
{
"source": "acsc",
"kind": "mapping",
"value": "https://origin.example/advisories/info-bulletin",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
}
],
"published": "2025-10-12T02:30:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://origin.example/advisories/info-bulletin",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "multi",
"summary": "Information bulletin",
"url": "https://origin.example/advisories/info-bulletin"
}
],
"severity": null,
"summary": "Serial number: ACSC-2025-011\n\nAdvisory type: Bulletin\n\nGeneral guidance bulletin.",
"title": "Information bulletin"
},
{
"advisoryKey": "acsc/multi/https-origin-example-advisories-router-critical",
"affectedPackages": [
{
"type": "vendor",
"identifier": "ExampleCo Router X",
"platform": null,
"versionRanges": [],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "acsc",
"kind": "affected",
"value": "ExampleCo Router X",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages"
]
}
]
},
{
"type": "vendor",
"identifier": "ExampleCo Router Y",
"platform": null,
"versionRanges": [],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "acsc",
"kind": "affected",
"value": "ExampleCo Router Y",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages"
]
}
]
}
],
"aliases": [
"ACSC-2025-010",
"CVE-2025-0001",
"https://origin.example/advisories/router-critical"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": null,
"provenance": [
{
"source": "acsc",
"kind": "document",
"value": "https://origin.example/feeds/multi/rss",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
},
{
"source": "acsc",
"kind": "feed",
"value": "multi",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"summary"
]
},
{
"source": "acsc",
"kind": "mapping",
"value": "https://origin.example/advisories/router-critical",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
}
],
"published": "2025-10-12T04:45:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://origin.example/advisories/router-critical",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "multi",
"summary": "Critical router vulnerability",
"url": "https://origin.example/advisories/router-critical"
},
{
"kind": "reference",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://vendor.example/router/patch",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": "vendor patch",
"url": "https://vendor.example/router/patch"
}
],
"severity": "critical",
"summary": "Serial number: ACSC-2025-010\n\nSeverity: Critical\n\nSystems affected: ExampleCo Router X, ExampleCo Router Y\n\nRemote code execution on ExampleCo routers. See vendor patch.\n\nCVE references: CVE-2025-0001",
"title": "Critical router vulnerability"
}
]

View File

@@ -0,0 +1,91 @@
[
{
"advisoryKey": "acsc/alerts/https-origin-example-advisories-example",
"affectedPackages": [],
"aliases": [
"ACSC-2025-001",
"Alert",
"https://origin.example/advisories/example"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": null,
"provenance": [
{
"source": "acsc",
"kind": "document",
"value": "https://origin.example/feeds/alerts/rss",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
},
{
"source": "acsc",
"kind": "feed",
"value": "alerts",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"summary"
]
},
{
"source": "acsc",
"kind": "mapping",
"value": "https://origin.example/advisories/example",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
}
],
"published": "2025-10-12T03:00:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://origin.example/advisories/example",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "alerts",
"summary": "ACSC-2025-001 Example Advisory",
"url": "https://origin.example/advisories/example"
},
{
"kind": "reference",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://vendor.example/patch",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": "Vendor patch",
"url": "https://vendor.example/patch"
}
],
"severity": null,
"summary": "Serial number: ACSC-2025-001\n\nAdvisory type: Alert\n\nFirst paragraph describing issue.\n\nSecond paragraph with Vendor patch.",
"title": "ACSC-2025-001 Example Advisory"
}
]

View File

@@ -1,88 +1,91 @@
[
{
"advisoryKey": "acsc/alerts/https-origin-example-advisories-example",
"affectedPackages": [],
"aliases": [
"ACSC-2025-001",
"Alert",
"https://origin.example/advisories/example"
],
"credits": [],
"cvssMetrics": [],
"exploitKnown": false,
"language": "en",
"modified": null,
"provenance": [
{
"source": "acsc",
"kind": "document",
"value": "https://origin.example/feeds/alerts/rss",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
},
{
"source": "acsc",
"kind": "feed",
"value": "alerts",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"summary"
]
},
{
"source": "acsc",
"kind": "mapping",
"value": "https://origin.example/advisories/example",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
}
],
"published": "2025-10-12T03:00:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://origin.example/advisories/example",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "alerts",
"summary": "ACSC-2025-001 Example Advisory",
"url": "https://origin.example/advisories/example"
},
{
"kind": "reference",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://vendor.example/patch",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": "Vendor patch",
"url": "https://vendor.example/patch"
}
],
"severity": null,
"summary": "Serial number: ACSC-2025-001\n\nAdvisory type: Alert\n\nFirst paragraph describing issue.\n\nSecond paragraph with Vendor patch.",
"title": "ACSC-2025-001 Example Advisory"
}
[
{
"advisoryKey": "acsc/alerts/https-origin-example-advisories-example",
"affectedPackages": [],
"aliases": [
"ACSC-2025-001",
"Alert",
"https://origin.example/advisories/example"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": null,
"provenance": [
{
"source": "acsc",
"kind": "document",
"value": "https://origin.example/feeds/alerts/rss",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
},
{
"source": "acsc",
"kind": "feed",
"value": "alerts",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"summary"
]
},
{
"source": "acsc",
"kind": "mapping",
"value": "https://origin.example/advisories/example",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages",
"aliases",
"references",
"summary"
]
}
],
"published": "2025-10-12T03:00:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://origin.example/advisories/example",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "alerts",
"summary": "ACSC-2025-001 Example Advisory",
"url": "https://origin.example/advisories/example"
},
{
"kind": "reference",
"provenance": {
"source": "acsc",
"kind": "reference",
"value": "https://vendor.example/patch",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": "Vendor patch",
"url": "https://vendor.example/patch"
}
],
"severity": null,
"summary": "Serial number: ACSC-2025-001\n\nAdvisory type: Alert\n\nFirst paragraph describing issue.\n\nSecond paragraph with Vendor patch.",
"title": "ACSC-2025-001 Example Advisory"
}
]

View File

@@ -10,7 +10,8 @@ using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Concelier.Connector.Cccs;
using StellaOps.Concelier.Connector.Cccs.Configuration;
using StellaOps.Concelier.Connector.Common;
@@ -79,11 +80,19 @@ public sealed class CccsConnectorTests : IAsyncLifetime
await using var provider = await BuildServiceProviderAsync();
SeedFeedResponses();
var connector = provider.GetRequiredService<CccsConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
var documentStore = provider.GetRequiredService<IDocumentStore>();
var document = await documentStore.FindBySourceAndUriAsync(CccsConnectorPlugin.SourceName, "https://www.cyber.gc.ca/en/alerts-advisories/test-advisory", CancellationToken.None);
var connector = provider.GetRequiredService<CccsConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
var mongo = provider.GetRequiredService<IMongoDatabase>();
var docCollection = mongo.GetCollection<BsonDocument>("document");
var documentsSnapshot = await docCollection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync();
System.IO.Directory.CreateDirectory(System.IO.Path.Combine(AppContext.BaseDirectory, "tmp"));
var debugPath = System.IO.Path.Combine(AppContext.BaseDirectory, "tmp", "cccs-documents.json");
await System.IO.File.WriteAllTextAsync(debugPath, documentsSnapshot.ToJson(new MongoDB.Bson.IO.JsonWriterSettings { Indent = true }));
var documentStore = provider.GetRequiredService<IDocumentStore>();
var document = await documentStore.FindBySourceAndUriAsync(CccsConnectorPlugin.SourceName, "https://www.cyber.gc.ca/en/alerts-advisories/test-advisory", CancellationToken.None);
document.Should().NotBeNull();
document!.Status.Should().Be(DocumentStatuses.PendingParse);
document.Metadata.Should().ContainKey("cccs.language").WhoseValue.Should().Be("en");

View File

@@ -1,205 +1,226 @@
[
{
"advisoryKey": "cert-fr/AV-2024.001",
"affectedPackages": [
{
"identifier": "AV-2024.001",
"platform": null,
"provenance": [
{
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-10-03T00:01:00+00:00",
"source": "cert-fr",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/"
}
],
"statuses": [],
"type": "vendor",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"certfr.summary": "Résumé de la première alerte.",
"certfr.content": "AV-2024.001 Alerte CERT-FR AV-2024.001 L'exploitation active de la vulnérabilité est surveillée. Consultez les indications du fournisseur .",
"certfr.reference.count": "1"
}
},
"provenance": {
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-10-03T00:01:00+00:00",
"source": "cert-fr",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/"
},
"rangeExpression": null,
"rangeKind": "vendor"
}
]
}
],
"aliases": [
"CERT-FR:AV-2024.001"
],
"cvssMetrics": [],
"exploitKnown": false,
"language": "fr",
"modified": null,
"provenance": [
{
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-10-03T00:01:00+00:00",
"source": "cert-fr",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/"
}
],
"published": "2024-10-03T00:00:00+00:00",
"references": [
{
"kind": "reference",
"provenance": {
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-10-03T00:01:00+00:00",
"source": "cert-fr",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/"
},
"sourceTag": null,
"summary": null,
"url": "https://vendor.example.com/patch"
},
{
"kind": "advisory",
"provenance": {
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-10-03T00:01:00+00:00",
"source": "cert-fr",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/"
},
"sourceTag": "cert-fr",
"summary": "Résumé de la première alerte.",
"url": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/"
}
],
"severity": null,
"summary": "Résumé de la première alerte.",
"title": "AV-2024.001 - Première alerte"
},
{
"advisoryKey": "cert-fr/AV-2024.002",
"affectedPackages": [
{
"identifier": "AV-2024.002",
"platform": null,
"provenance": [
{
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-10-03T00:01:00+00:00",
"source": "cert-fr",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/"
}
],
"statuses": [],
"type": "vendor",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"certfr.summary": "Résumé de la deuxième alerte.",
"certfr.content": "AV-2024.002 Alerte CERT-FR AV-2024.002 Des correctifs sont disponibles pour plusieurs produits. Note de mise à jour Correctif",
"certfr.reference.count": "2"
}
},
"provenance": {
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-10-03T00:01:00+00:00",
"source": "cert-fr",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/"
},
"rangeExpression": null,
"rangeKind": "vendor"
}
]
}
],
"aliases": [
"CERT-FR:AV-2024.002"
],
"cvssMetrics": [],
"exploitKnown": false,
"language": "fr",
"modified": null,
"provenance": [
{
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-10-03T00:01:00+00:00",
"source": "cert-fr",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/"
}
],
"published": "2024-10-03T00:00:00+00:00",
"references": [
{
"kind": "reference",
"provenance": {
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-10-03T00:01:00+00:00",
"source": "cert-fr",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/"
},
"sourceTag": null,
"summary": null,
"url": "https://support.example.com/kb/KB-1234"
},
{
"kind": "reference",
"provenance": {
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-10-03T00:01:00+00:00",
"source": "cert-fr",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/"
},
"sourceTag": null,
"summary": null,
"url": "https://support.example.com/kb/KB-5678"
},
{
"kind": "advisory",
"provenance": {
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-10-03T00:01:00+00:00",
"source": "cert-fr",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/"
},
"sourceTag": "cert-fr",
"summary": "Résumé de la deuxième alerte.",
"url": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/"
}
],
"severity": null,
"summary": "Résumé de la deuxième alerte.",
"title": "AV-2024.002 - Deuxième alerte"
}
[
{
"advisoryKey": "cert-fr/AV-2024.001",
"affectedPackages": [
{
"type": "vendor",
"identifier": "AV-2024.001",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"certfr.summary": "Résumé de la première alerte.",
"certfr.content": "AV-2024.001 Alerte CERT-FR AV-2024.001 L'exploitation active de la vulnérabilité est surveillée. Consultez les indications du fournisseur .",
"certfr.reference.count": "1"
}
},
"provenance": {
"source": "cert-fr",
"kind": "document",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/",
"decisionReason": null,
"recordedAt": "2024-10-03T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "cert-fr",
"kind": "document",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/",
"decisionReason": null,
"recordedAt": "2024-10-03T00:01:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CERT-FR:AV-2024.001"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "fr",
"modified": null,
"provenance": [
{
"source": "cert-fr",
"kind": "document",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/",
"decisionReason": null,
"recordedAt": "2024-10-03T00:01:00+00:00",
"fieldMask": []
}
],
"published": "2024-10-03T00:00:00+00:00",
"references": [
{
"kind": "reference",
"provenance": {
"source": "cert-fr",
"kind": "document",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/",
"decisionReason": null,
"recordedAt": "2024-10-03T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": null,
"url": "https://vendor.example.com/patch"
},
{
"kind": "advisory",
"provenance": {
"source": "cert-fr",
"kind": "document",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/",
"decisionReason": null,
"recordedAt": "2024-10-03T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "cert-fr",
"summary": "Résumé de la première alerte.",
"url": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/"
}
],
"severity": null,
"summary": "Résumé de la première alerte.",
"title": "AV-2024.001 - Première alerte"
},
{
"advisoryKey": "cert-fr/AV-2024.002",
"affectedPackages": [
{
"type": "vendor",
"identifier": "AV-2024.002",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"certfr.summary": "Résumé de la deuxième alerte.",
"certfr.content": "AV-2024.002 Alerte CERT-FR AV-2024.002 Des correctifs sont disponibles pour plusieurs produits. Note de mise à jour Correctif",
"certfr.reference.count": "2"
}
},
"provenance": {
"source": "cert-fr",
"kind": "document",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/",
"decisionReason": null,
"recordedAt": "2024-10-03T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "cert-fr",
"kind": "document",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/",
"decisionReason": null,
"recordedAt": "2024-10-03T00:01:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CERT-FR:AV-2024.002"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "fr",
"modified": null,
"provenance": [
{
"source": "cert-fr",
"kind": "document",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/",
"decisionReason": null,
"recordedAt": "2024-10-03T00:01:00+00:00",
"fieldMask": []
}
],
"published": "2024-10-03T00:00:00+00:00",
"references": [
{
"kind": "reference",
"provenance": {
"source": "cert-fr",
"kind": "document",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/",
"decisionReason": null,
"recordedAt": "2024-10-03T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": null,
"url": "https://support.example.com/kb/KB-1234"
},
{
"kind": "reference",
"provenance": {
"source": "cert-fr",
"kind": "document",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/",
"decisionReason": null,
"recordedAt": "2024-10-03T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": null,
"url": "https://support.example.com/kb/KB-5678"
},
{
"kind": "advisory",
"provenance": {
"source": "cert-fr",
"kind": "document",
"value": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/",
"decisionReason": null,
"recordedAt": "2024-10-03T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "cert-fr",
"summary": "Résumé de la deuxième alerte.",
"url": "https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/"
}
],
"severity": null,
"summary": "Résumé de la deuxième alerte.",
"title": "AV-2024.002 - Deuxième alerte"
}
]

View File

@@ -1,128 +1,141 @@
{
"advisoryKey": "CIAD-2024-0005",
"affectedPackages": [
{
"identifier": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the",
"platform": null,
"provenance": [
{
"fieldMask": [],
"kind": "affected",
"recordedAt": "2024-04-20T00:01:00+00:00",
"source": "cert-in",
"value": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the"
}
],
"statuses": [],
"type": "ics-vendor",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"certin.vendor": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the "
}
},
"provenance": {
"fieldMask": [],
"kind": "affected",
"recordedAt": "2024-04-20T00:01:00+00:00",
"source": "cert-in",
"value": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the"
},
"rangeExpression": null,
"rangeKind": "vendor"
}
]
}
],
"aliases": [
"CIAD-2024-0005",
"CVE-2024-9990",
"CVE-2024-9991"
],
"cvssMetrics": [],
"exploitKnown": false,
"language": "en",
"modified": "2024-04-15T10:00:00+00:00",
"provenance": [
{
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-04-20T00:00:00+00:00",
"source": "cert-in",
"value": "https://cert-in.example/advisory/CIAD-2024-0005"
},
{
"fieldMask": [],
"kind": "mapping",
"recordedAt": "2024-04-20T00:01:00+00:00",
"source": "cert-in",
"value": "CIAD-2024-0005"
}
],
"published": "2024-04-15T10:00:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"fieldMask": [],
"kind": "reference",
"recordedAt": "2024-04-20T00:01:00+00:00",
"source": "cert-in",
"value": "https://cert-in.example/advisory/CIAD-2024-0005"
},
"sourceTag": "cert-in",
"summary": null,
"url": "https://cert-in.example/advisory/CIAD-2024-0005"
},
{
"kind": "reference",
"provenance": {
"fieldMask": [],
"kind": "reference",
"recordedAt": "2024-04-20T00:01:00+00:00",
"source": "cert-in",
"value": "https://vendor.example.com/advisories/example-gateway-bulletin"
},
"sourceTag": null,
"summary": null,
"url": "https://vendor.example.com/advisories/example-gateway-bulletin"
},
{
"kind": "advisory",
"provenance": {
"fieldMask": [],
"kind": "reference",
"recordedAt": "2024-04-20T00:01:00+00:00",
"source": "cert-in",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-9990"
},
"sourceTag": "CVE-2024-9990",
"summary": null,
"url": "https://www.cve.org/CVERecord?id=CVE-2024-9990"
},
{
"kind": "advisory",
"provenance": {
"fieldMask": [],
"kind": "reference",
"recordedAt": "2024-04-20T00:01:00+00:00",
"source": "cert-in",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-9991"
},
"sourceTag": "CVE-2024-9991",
"summary": null,
"url": "https://www.cve.org/CVERecord?id=CVE-2024-9991"
}
],
"severity": "high",
"summary": "Example Gateway devices vulnerable to remote code execution (CVE-2024-9990).",
"title": "Multiple vulnerabilities in Example Gateway"
{
"advisoryKey": "CIAD-2024-0005",
"affectedPackages": [
{
"type": "ics-vendor",
"identifier": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"certin.vendor": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the "
}
},
"provenance": {
"source": "cert-in",
"kind": "affected",
"value": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "cert-in",
"kind": "affected",
"value": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CIAD-2024-0005",
"CVE-2024-9990",
"CVE-2024-9991"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": "2024-04-15T10:00:00+00:00",
"provenance": [
{
"source": "cert-in",
"kind": "document",
"value": "https://cert-in.example/advisory/CIAD-2024-0005",
"decisionReason": null,
"recordedAt": "2024-04-20T00:00:00+00:00",
"fieldMask": []
},
{
"source": "cert-in",
"kind": "mapping",
"value": "CIAD-2024-0005",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
}
],
"published": "2024-04-15T10:00:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://cert-in.example/advisory/CIAD-2024-0005",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "cert-in",
"summary": null,
"url": "https://cert-in.example/advisory/CIAD-2024-0005"
},
{
"kind": "reference",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://vendor.example.com/advisories/example-gateway-bulletin",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": null,
"url": "https://vendor.example.com/advisories/example-gateway-bulletin"
},
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-9990",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "CVE-2024-9990",
"summary": null,
"url": "https://www.cve.org/CVERecord?id=CVE-2024-9990"
},
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-9991",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "CVE-2024-9991",
"summary": null,
"url": "https://www.cve.org/CVERecord?id=CVE-2024-9991"
}
],
"severity": "high",
"summary": "Example Gateway devices vulnerable to remote code execution (CVE-2024-9990).",
"title": "Multiple vulnerabilities in Example Gateway"
}

View File

@@ -0,0 +1,141 @@
{
"advisoryKey": "CIAD-2024-0005",
"affectedPackages": [
{
"type": "ics-vendor",
"identifier": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"certin.vendor": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the "
}
},
"provenance": {
"source": "cert-in",
"kind": "affected",
"value": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "cert-in",
"kind": "affected",
"value": "Example Gateway Technologies Pvt Ltd Organisation: Partner Systems Inc. CVE-2024-9990 and CVE-2024-9991 allow remote attackers to execute arbitrary commands. Further information is available from the",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CIAD-2024-0005",
"CVE-2024-9990",
"CVE-2024-9991"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": "2024-04-15T10:00:00+00:00",
"provenance": [
{
"source": "cert-in",
"kind": "document",
"value": "https://cert-in.example/advisory/CIAD-2024-0005",
"decisionReason": null,
"recordedAt": "2024-04-20T00:00:00+00:00",
"fieldMask": []
},
{
"source": "cert-in",
"kind": "mapping",
"value": "CIAD-2024-0005",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
}
],
"published": "2024-04-15T10:00:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://cert-in.example/advisory/CIAD-2024-0005",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "cert-in",
"summary": null,
"url": "https://cert-in.example/advisory/CIAD-2024-0005"
},
{
"kind": "reference",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://vendor.example.com/advisories/example-gateway-bulletin",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": null,
"url": "https://vendor.example.com/advisories/example-gateway-bulletin"
},
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-9990",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "CVE-2024-9990",
"summary": null,
"url": "https://www.cve.org/CVERecord?id=CVE-2024-9990"
},
{
"kind": "advisory",
"provenance": {
"source": "cert-in",
"kind": "reference",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-9991",
"decisionReason": null,
"recordedAt": "2024-04-20T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "CVE-2024-9991",
"summary": null,
"url": "https://www.cve.org/CVERecord?id=CVE-2024-9991"
}
],
"severity": "high",
"summary": "Example Gateway devices vulnerable to remote code execution (CVE-2024-9990).",
"title": "Multiple vulnerabilities in Example Gateway"
}

View File

@@ -1,221 +1,224 @@
{
"advisoryKey": "CVE-2024-0001",
"affectedPackages": [
{
"type": "vendor",
"identifier": "examplevendor:exampleproduct",
"platform": "linux",
"versionRanges": [
{
"fixedVersion": "1.2.0",
"introducedVersion": "1.0.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": "version=1.0.0, < 1.2.0",
"exactValue": null,
"fixed": "1.2.0",
"fixedInclusive": false,
"introduced": "1.0.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": true,
"style": "range"
},
"vendorExtensions": {
"vendor": "ExampleVendor",
"product": "ExampleProduct",
"platform": "linux",
"version": "1.0.0",
"lessThan": "1.2.0",
"versionType": "semver"
}
},
"provenance": {
"source": "cve",
"kind": "affected-range",
"value": "examplevendor:exampleproduct",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"rangeExpression": "version=1.0.0, < 1.2.0",
"rangeKind": "semver"
},
{
"fixedVersion": "1.2.0",
"introducedVersion": "1.2.0",
"lastAffectedVersion": "1.2.0",
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": "version=1.2.0",
"exactValue": null,
"fixed": "1.2.0",
"fixedInclusive": false,
"introduced": "1.2.0",
"introducedInclusive": true,
"lastAffected": "1.2.0",
"lastAffectedInclusive": true,
"style": "range"
},
"vendorExtensions": {
"vendor": "ExampleVendor",
"product": "ExampleProduct",
"platform": "linux",
"version": "1.2.0",
"versionType": "semver"
}
},
"provenance": {
"source": "cve",
"kind": "affected-range",
"value": "examplevendor:exampleproduct",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"rangeExpression": "version=1.2.0",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "1.2.0",
"notes": "cve:cve-2024-0001:examplevendor:exampleproduct"
},
{
"scheme": "semver",
"type": "range",
"min": "1.0.0",
"minInclusive": true,
"max": "1.2.0",
"maxInclusive": false,
"value": null,
"notes": "cve:cve-2024-0001:examplevendor:exampleproduct"
}
],
"statuses": [
{
"provenance": {
"source": "cve",
"kind": "affected-status",
"value": "examplevendor:exampleproduct",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"status": "affected"
},
{
"provenance": {
"source": "cve",
"kind": "affected-status",
"value": "examplevendor:exampleproduct",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"status": "not_affected"
}
],
"provenance": [
{
"source": "cve",
"kind": "affected",
"value": "examplevendor:exampleproduct",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CVE-2024-0001",
"GHSA-xxxx-yyyy-zzzz"
],
"credits": [],
"cvssMetrics": [
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "cve",
"kind": "cvss",
"value": "cve/CVE-2024-0001",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"exploitKnown": false,
"language": "en",
"modified": "2024-09-15T12:00:00+00:00",
"provenance": [
{
"source": "cve",
"kind": "document",
"value": "cve/CVE-2024-0001",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
{
"source": "cve",
"kind": "mapping",
"value": "CVE-2024-0001",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
}
],
"published": "2024-09-10T12:00:00+00:00",
"references": [
{
"kind": "third-party-advisory",
"provenance": {
"source": "cve",
"kind": "reference",
"value": "https://cve.example.com/CVE-2024-0001",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": null,
"url": "https://cve.example.com/CVE-2024-0001"
},
{
"kind": "vendor-advisory",
"provenance": {
"source": "cve",
"kind": "reference",
"value": "https://example.com/security/advisory",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "Vendor Advisory",
"summary": null,
"url": "https://example.com/security/advisory"
}
],
"severity": "critical",
"summary": "An example vulnerability allowing remote attackers to execute arbitrary code.",
"title": "Example Product Remote Code Execution"
{
"advisoryKey": "CVE-2024-0001",
"affectedPackages": [
{
"type": "vendor",
"identifier": "examplevendor:exampleproduct",
"platform": "linux",
"versionRanges": [
{
"fixedVersion": "1.2.0",
"introducedVersion": "1.0.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": "version=1.0.0, < 1.2.0",
"exactValue": null,
"fixed": "1.2.0",
"fixedInclusive": false,
"introduced": "1.0.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": true,
"style": "range"
},
"vendorExtensions": {
"vendor": "ExampleVendor",
"product": "ExampleProduct",
"platform": "linux",
"version": "1.0.0",
"lessThan": "1.2.0",
"versionType": "semver"
}
},
"provenance": {
"source": "cve",
"kind": "affected-range",
"value": "examplevendor:exampleproduct",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"rangeExpression": "version=1.0.0, < 1.2.0",
"rangeKind": "semver"
},
{
"fixedVersion": "1.2.0",
"introducedVersion": "1.2.0",
"lastAffectedVersion": "1.2.0",
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": "version=1.2.0",
"exactValue": "1.2.0",
"fixed": "1.2.0",
"fixedInclusive": false,
"introduced": "1.2.0",
"introducedInclusive": true,
"lastAffected": "1.2.0",
"lastAffectedInclusive": true,
"style": "exact"
},
"vendorExtensions": {
"vendor": "ExampleVendor",
"product": "ExampleProduct",
"platform": "linux",
"version": "1.2.0",
"versionType": "semver"
}
},
"provenance": {
"source": "cve",
"kind": "affected-range",
"value": "examplevendor:exampleproduct",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"rangeExpression": "version=1.2.0",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "1.2.0",
"notes": "cve:cve-2024-0001:examplevendor:exampleproduct"
},
{
"scheme": "semver",
"type": "range",
"min": "1.0.0",
"minInclusive": true,
"max": "1.2.0",
"maxInclusive": false,
"value": null,
"notes": "cve:cve-2024-0001:examplevendor:exampleproduct"
}
],
"statuses": [
{
"provenance": {
"source": "cve",
"kind": "affected-status",
"value": "examplevendor:exampleproduct",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"status": "affected"
},
{
"provenance": {
"source": "cve",
"kind": "affected-status",
"value": "examplevendor:exampleproduct",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"status": "not_affected"
}
],
"provenance": [
{
"source": "cve",
"kind": "affected",
"value": "examplevendor:exampleproduct",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CVE-2024-0001",
"GHSA-xxxx-yyyy-zzzz"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "cve",
"kind": "cvss",
"value": "cve/CVE-2024-0001",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": "2024-09-15T12:00:00+00:00",
"provenance": [
{
"source": "cve",
"kind": "document",
"value": "cve/CVE-2024-0001",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
{
"source": "cve",
"kind": "mapping",
"value": "CVE-2024-0001",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
}
],
"published": "2024-09-10T12:00:00+00:00",
"references": [
{
"kind": "third-party-advisory",
"provenance": {
"source": "cve",
"kind": "reference",
"value": "https://cve.example.com/CVE-2024-0001",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": null,
"summary": null,
"url": "https://cve.example.com/CVE-2024-0001"
},
{
"kind": "vendor-advisory",
"provenance": {
"source": "cve",
"kind": "reference",
"value": "https://example.com/security/advisory",
"decisionReason": null,
"recordedAt": "2024-10-01T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "Vendor Advisory",
"summary": null,
"url": "https://example.com/security/advisory"
}
],
"severity": "critical",
"summary": "An example vulnerability allowing remote attackers to execute arbitrary code.",
"title": "Example Product Remote Code Execution"
}

View File

@@ -91,7 +91,7 @@ public class IcsCisaConnectorMappingTests
Assert.Equal("ControlSuite", productPackage.Identifier);
var range = Assert.Single(productPackage.VersionRanges);
Assert.Equal("product", range.RangeKind);
Assert.Equal("4.2.0", range.RangeExpression);
Assert.Equal("4.2", range.RangeExpression);
Assert.NotNull(range.Primitives);
Assert.Equal("Example Corp", range.Primitives!.VendorExtensions!["ics.vendors"]);
Assert.Equal("ControlSuite", range.Primitives.VendorExtensions!["ics.product"]);
@@ -129,7 +129,7 @@ public class IcsCisaConnectorMappingTests
var productPackage = Assert.Single(packages);
Assert.Equal("Control Suite Firmware", productPackage.Identifier);
var range = Assert.Single(productPackage.VersionRanges);
Assert.Equal("1.0.0 - 2.0.0", range.RangeExpression);
Assert.Equal("1.0 - 2.0", range.RangeExpression);
Assert.NotNull(range.Primitives);
Assert.Equal("ics-cisa:ICSA-25-789-03:control-suite-firmware", range.Provenance.Value);
var rule = Assert.Single(productPackage.NormalizedVersions);

View File

@@ -0,0 +1,557 @@
{
"advisoryKey": "acme-controller-2024",
"affectedPackages": [
{
"type": "ics-vendor",
"identifier": "2024",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"ics.vendor": "2024"
}
},
"provenance": {
"source": "ics-kaspersky",
"kind": "affected",
"value": "2024",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ics-kaspersky",
"kind": "affected",
"value": "2024",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
]
},
{
"type": "ics-vendor",
"identifier": "7777 can allow authenticated attackers to execute arbitrary commands",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"ics.vendor": "7777 can allow authenticated attackers to execute arbitrary commands"
}
},
"provenance": {
"source": "ics-kaspersky",
"kind": "affected",
"value": "7777 can allow authenticated attackers to execute arbitrary commands",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ics-kaspersky",
"kind": "affected",
"value": "7777 can allow authenticated attackers to execute arbitrary commands",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
]
},
{
"type": "ics-vendor",
"identifier": "7777)",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"ics.vendor": "7777)"
}
},
"provenance": {
"source": "ics-kaspersky",
"kind": "affected",
"value": "7777)",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ics-kaspersky",
"kind": "affected",
"value": "7777)",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
]
},
{
"type": "ics-vendor",
"identifier": "8888",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"ics.vendor": "8888"
}
},
"provenance": {
"source": "ics-kaspersky",
"kind": "affected",
"value": "8888",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ics-kaspersky",
"kind": "affected",
"value": "8888",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
]
},
{
"type": "ics-vendor",
"identifier": "ACME Corp",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"ics.vendor": "ACME Corp"
}
},
"provenance": {
"source": "ics-kaspersky",
"kind": "affected",
"value": "ACME Corp",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ics-kaspersky",
"kind": "affected",
"value": "ACME Corp",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
]
},
{
"type": "ics-vendor",
"identifier": "ACME Corp Affected models",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"ics.vendor": "ACME Corp Affected models"
}
},
"provenance": {
"source": "ics-kaspersky",
"kind": "affected",
"value": "ACME Corp Affected models",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ics-kaspersky",
"kind": "affected",
"value": "ACME Corp Affected models",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
]
},
{
"type": "ics-vendor",
"identifier": "ACME Corp industrial",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"ics.vendor": "ACME Corp industrial"
}
},
"provenance": {
"source": "ics-kaspersky",
"kind": "affected",
"value": "ACME Corp industrial",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ics-kaspersky",
"kind": "affected",
"value": "ACME Corp industrial",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
]
},
{
"type": "ics-vendor",
"identifier": "Additional details are provided in CVE",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"ics.vendor": "Additional details are provided in CVE"
}
},
"provenance": {
"source": "ics-kaspersky",
"kind": "affected",
"value": "Additional details are provided in CVE",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ics-kaspersky",
"kind": "affected",
"value": "Additional details are provided in CVE",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
]
},
{
"type": "ics-vendor",
"identifier": "Exploitation of CVE",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"ics.vendor": "Exploitation of CVE"
}
},
"provenance": {
"source": "ics-kaspersky",
"kind": "affected",
"value": "Exploitation of CVE",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ics-kaspersky",
"kind": "affected",
"value": "Exploitation of CVE",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
]
},
{
"type": "ics-vendor",
"identifier": "Vendor",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"ics.vendor": "Vendor"
}
},
"provenance": {
"source": "ics-kaspersky",
"kind": "affected",
"value": "Vendor",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ics-kaspersky",
"kind": "affected",
"value": "Vendor",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
]
},
{
"type": "ics-vendor",
"identifier": "X100, X200",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"ics.vendor": "X100, X200"
}
},
"provenance": {
"source": "ics-kaspersky",
"kind": "affected",
"value": "X100, X200",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ics-kaspersky",
"kind": "affected",
"value": "X100, X200",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CVE-2024-7777",
"CVE-2024-8888",
"acme-controller-2024"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": "2024-10-15T10:00:00+00:00",
"provenance": [
{
"source": "ics-kaspersky",
"kind": "document",
"value": "https://ics-cert.example/advisories/acme-controller-2024/",
"decisionReason": null,
"recordedAt": "2024-10-20T00:00:00+00:00",
"fieldMask": []
},
{
"source": "ics-kaspersky",
"kind": "mapping",
"value": "acme-controller-2024",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
}
],
"published": "2024-10-15T10:00:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "ics-kaspersky",
"kind": "reference",
"value": "https://ics-cert.example/advisories/acme-controller-2024/",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "kaspersky-ics",
"summary": null,
"url": "https://ics-cert.example/advisories/acme-controller-2024/"
},
{
"kind": "advisory",
"provenance": {
"source": "ics-kaspersky",
"kind": "reference",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-7777",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "CVE-2024-7777",
"summary": null,
"url": "https://www.cve.org/CVERecord?id=CVE-2024-7777"
},
{
"kind": "advisory",
"provenance": {
"source": "ics-kaspersky",
"kind": "reference",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-8888",
"decisionReason": null,
"recordedAt": "2024-10-20T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "CVE-2024-8888",
"summary": null,
"url": "https://www.cve.org/CVERecord?id=CVE-2024-8888"
}
],
"severity": null,
"summary": "ACME Corp industrial controllers allow remote compromise (CVE-2024-7777).",
"title": "ACME Corp controllers multiple vulnerabilities"
}

View File

@@ -1,87 +1,97 @@
{
"advisoryKey": "JVNDB-2024-123456",
"affectedPackages": [],
"aliases": [
"CVE-2024-5555",
"JVNDB-2024-123456"
],
"cvssMetrics": [
{
"baseScore": 8.8,
"baseSeverity": "high",
"provenance": {
"fieldMask": [],
"kind": "cvss",
"recordedAt": "2024-03-10T00:01:00+00:00",
"source": "jvn",
"value": "Base"
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"exploitKnown": false,
"language": "en",
"modified": "2024-03-10T02:30:00+00:00",
"provenance": [
{
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-03-10T00:00:00+00:00",
"source": "jvn",
"value": "https://jvndb.jvn.jp/myjvn?method=getVulnDetailInfo&feed=hnd&lang=en&vulnId=JVNDB-2024-123456"
},
{
"fieldMask": [],
"kind": "mapping",
"recordedAt": "2024-03-10T00:01:00+00:00",
"source": "jvn",
"value": "JVNDB-2024-123456"
}
],
"published": "2024-03-09T02:00:00+00:00",
"references": [
{
"kind": "weakness",
"provenance": {
"fieldMask": [],
"kind": "reference",
"recordedAt": "2024-03-10T00:01:00+00:00",
"source": "jvn",
"value": "https://cwe.mitre.org/data/definitions/287.html"
},
"sourceTag": "CWE-287",
"summary": "JVNDB",
"url": "https://cwe.mitre.org/data/definitions/287.html"
},
{
"kind": "advisory",
"provenance": {
"fieldMask": [],
"kind": "reference",
"recordedAt": "2024-03-10T00:01:00+00:00",
"source": "jvn",
"value": "https://vendor.example.com/advisories/EX-2024-01"
},
"sourceTag": "EX-2024-01",
"summary": "Example ICS Vendor Advisory",
"url": "https://vendor.example.com/advisories/EX-2024-01"
},
{
"kind": "advisory",
"provenance": {
"fieldMask": [],
"kind": "reference",
"recordedAt": "2024-03-10T00:01:00+00:00",
"source": "jvn",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-5555"
},
"sourceTag": "CVE-2024-5555",
"summary": "Common Vulnerabilities and Exposures (CVE)",
"url": "https://www.cve.org/CVERecord?id=CVE-2024-5555"
}
],
"severity": "high",
"summary": "Imaginary ICS Controller provided by Example Industrial Corporation contains an authentication bypass vulnerability.",
"title": "Example vulnerability in Imaginary ICS Controller"
{
"advisoryKey": "JVNDB-2024-123456",
"affectedPackages": [],
"aliases": [
"CVE-2024-5555",
"JVNDB-2024-123456"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [
{
"baseScore": 8.8,
"baseSeverity": "high",
"provenance": {
"source": "jvn",
"kind": "cvss",
"value": "Base",
"decisionReason": null,
"recordedAt": "2024-03-10T00:01:00+00:00",
"fieldMask": []
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": "2024-03-10T02:30:00+00:00",
"provenance": [
{
"source": "jvn",
"kind": "document",
"value": "https://jvndb.jvn.jp/myjvn?method=getVulnDetailInfo&feed=hnd&lang=en&vulnId=JVNDB-2024-123456",
"decisionReason": null,
"recordedAt": "2024-03-10T00:00:00+00:00",
"fieldMask": []
},
{
"source": "jvn",
"kind": "mapping",
"value": "JVNDB-2024-123456",
"decisionReason": null,
"recordedAt": "2024-03-10T00:01:00+00:00",
"fieldMask": []
}
],
"published": "2024-03-09T02:00:00+00:00",
"references": [
{
"kind": "weakness",
"provenance": {
"source": "jvn",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/287.html",
"decisionReason": null,
"recordedAt": "2024-03-10T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "CWE-287",
"summary": "JVNDB",
"url": "https://cwe.mitre.org/data/definitions/287.html"
},
{
"kind": "advisory",
"provenance": {
"source": "jvn",
"kind": "reference",
"value": "https://vendor.example.com/advisories/EX-2024-01",
"decisionReason": null,
"recordedAt": "2024-03-10T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "EX-2024-01",
"summary": "Example ICS Vendor Advisory",
"url": "https://vendor.example.com/advisories/EX-2024-01"
},
{
"kind": "advisory",
"provenance": {
"source": "jvn",
"kind": "reference",
"value": "https://www.cve.org/CVERecord?id=CVE-2024-5555",
"decisionReason": null,
"recordedAt": "2024-03-10T00:01:00+00:00",
"fieldMask": []
},
"sourceTag": "CVE-2024-5555",
"summary": "Common Vulnerabilities and Exposures (CVE)",
"url": "https://www.cve.org/CVERecord?id=CVE-2024-5555"
}
],
"severity": "high",
"summary": "Imaginary ICS Controller provided by Example Industrial Corporation contains an authentication bypass vulnerability.",
"title": "Example vulnerability in Imaginary ICS Controller"
}

View File

@@ -1,335 +1,338 @@
[
{
"advisoryKey": "BDU:2025-00001",
"affectedPackages": [
{
"type": "vendor",
"identifier": "ООО «1С-Софт» 1С:Предприятие",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": null,
"provenance": {
"source": "ru-bdu",
"kind": "package-range",
"value": "8.2.19.116",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "8.2.19.116",
"rangeKind": "string"
}
],
"normalizedVersions": [
{
"scheme": "ru-bdu.raw",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "8.2.19.116",
"notes": null
}
],
"statuses": [
{
"provenance": {
"source": "ru-bdu",
"kind": "package-status",
"value": "Подтверждена производителем",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "affected"
},
{
"provenance": {
"source": "ru-bdu",
"kind": "package-fix-status",
"value": "Уязвимость устранена",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ru-bdu",
"kind": "package",
"value": "ООО «1С-Софт» 1С:Предприятие",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
},
{
"type": "vendor",
"identifier": "ООО «1С-Софт» 1С:Предприятие",
"platform": "Windows",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": null,
"provenance": {
"source": "ru-bdu",
"kind": "package-range",
"value": "8.2.18.96",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "8.2.18.96",
"rangeKind": "string"
}
],
"normalizedVersions": [
{
"scheme": "ru-bdu.raw",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "8.2.18.96",
"notes": null
}
],
"statuses": [
{
"provenance": {
"source": "ru-bdu",
"kind": "package-status",
"value": "Подтверждена производителем",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "affected"
},
{
"provenance": {
"source": "ru-bdu",
"kind": "package-fix-status",
"value": "Уязвимость устранена",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ru-bdu",
"kind": "package",
"value": "ООО «1С-Софт» 1С:Предприятие",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"BDU:2025-00001",
"CVE-2009-3555",
"CVE-2015-0206",
"PT-2015-0206"
],
"credits": [],
"cvssMetrics": [
{
"baseScore": 7.5,
"baseSeverity": "high",
"provenance": {
"source": "ru-bdu",
"kind": "cvss",
"value": "CVSS:2.0/AV:N/AC:L/AU:N/C:P/I:P/A:P",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:2.0/AV:N/AC:L/AU:N/C:P/I:P/A:P",
"version": "2.0"
},
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "ru-bdu",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"exploitKnown": true,
"language": "ru",
"modified": "2013-01-12T00:00:00+00:00",
"provenance": [
{
"source": "ru-bdu",
"kind": "advisory",
"value": "BDU:2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2013-01-12T00:00:00+00:00",
"references": [
{
"kind": "source",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "http://mirror.example/ru-bdu/BDU-2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-bdu",
"summary": null,
"url": "http://mirror.example/ru-bdu/BDU-2025-00001"
},
{
"kind": "source",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://advisories.example/BDU-2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-bdu",
"summary": null,
"url": "https://advisories.example/BDU-2025-00001"
},
{
"kind": "details",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://bdu.fstec.ru/vul/2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-bdu",
"summary": null,
"url": "https://bdu.fstec.ru/vul/2025-00001"
},
{
"kind": "cwe",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/310.html",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "cwe",
"summary": "Проблемы использования криптографии",
"url": "https://cwe.mitre.org/data/definitions/310.html"
},
{
"kind": "cve",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://nvd.nist.gov/vuln/detail/CVE-2009-3555",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "cve",
"summary": "CVE-2009-3555",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2009-3555"
},
{
"kind": "cve",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://nvd.nist.gov/vuln/detail/CVE-2015-0206",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "cve",
"summary": "CVE-2015-0206",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2015-0206"
},
{
"kind": "external",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://ptsecurity.com/PT-2015-0206",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "positivetechnologiesadvisory",
"summary": "PT-2015-0206",
"url": "https://ptsecurity.com/PT-2015-0206"
}
],
"severity": "critical",
"summary": "Удалённый злоумышленник может вызвать отказ в обслуживании или получить доступ к данным.",
"title": "Множественные уязвимости криптопровайдера"
}
[
{
"advisoryKey": "BDU:2025-00001",
"affectedPackages": [
{
"type": "vendor",
"identifier": "ООО «1С-Софт» 1С:Предприятие",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": null,
"provenance": {
"source": "ru-bdu",
"kind": "package-range",
"value": "8.2.19.116",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "8.2.19.116",
"rangeKind": "string"
}
],
"normalizedVersions": [
{
"scheme": "ru-bdu.raw",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "8.2.19.116",
"notes": null
}
],
"statuses": [
{
"provenance": {
"source": "ru-bdu",
"kind": "package-status",
"value": "Подтверждена производителем",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "affected"
},
{
"provenance": {
"source": "ru-bdu",
"kind": "package-fix-status",
"value": "Уязвимость устранена",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ru-bdu",
"kind": "package",
"value": "ООО «1С-Софт» 1С:Предприятие",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
},
{
"type": "vendor",
"identifier": "ООО «1С-Софт» 1С:Предприятие",
"platform": "Windows",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": null,
"provenance": {
"source": "ru-bdu",
"kind": "package-range",
"value": "8.2.18.96",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "8.2.18.96",
"rangeKind": "string"
}
],
"normalizedVersions": [
{
"scheme": "ru-bdu.raw",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "8.2.18.96",
"notes": null
}
],
"statuses": [
{
"provenance": {
"source": "ru-bdu",
"kind": "package-status",
"value": "Подтверждена производителем",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "affected"
},
{
"provenance": {
"source": "ru-bdu",
"kind": "package-fix-status",
"value": "Уязвимость устранена",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ru-bdu",
"kind": "package",
"value": "ООО «1С-Софт» 1С:Предприятие",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"BDU:2025-00001",
"CVE-2009-3555",
"CVE-2015-0206",
"PT-2015-0206"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [
{
"baseScore": 7.5,
"baseSeverity": "high",
"provenance": {
"source": "ru-bdu",
"kind": "cvss",
"value": "CVSS:2.0/AV:N/AC:L/AU:N/C:P/I:P/A:P",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:2.0/AV:N/AC:L/AU:N/C:P/I:P/A:P",
"version": "2.0"
},
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "ru-bdu",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"cwes": [],
"description": null,
"exploitKnown": true,
"language": "ru",
"modified": "2013-01-12T00:00:00+00:00",
"provenance": [
{
"source": "ru-bdu",
"kind": "advisory",
"value": "BDU:2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2013-01-12T00:00:00+00:00",
"references": [
{
"kind": "source",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "http://mirror.example/ru-bdu/BDU-2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-bdu",
"summary": null,
"url": "http://mirror.example/ru-bdu/BDU-2025-00001"
},
{
"kind": "source",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://advisories.example/BDU-2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-bdu",
"summary": null,
"url": "https://advisories.example/BDU-2025-00001"
},
{
"kind": "details",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://bdu.fstec.ru/vul/2025-00001",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-bdu",
"summary": null,
"url": "https://bdu.fstec.ru/vul/2025-00001"
},
{
"kind": "cwe",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/310.html",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "cwe",
"summary": "Проблемы использования криптографии",
"url": "https://cwe.mitre.org/data/definitions/310.html"
},
{
"kind": "cve",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://nvd.nist.gov/vuln/detail/CVE-2009-3555",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "cve",
"summary": "CVE-2009-3555",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2009-3555"
},
{
"kind": "cve",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://nvd.nist.gov/vuln/detail/CVE-2015-0206",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "cve",
"summary": "CVE-2015-0206",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2015-0206"
},
{
"kind": "external",
"provenance": {
"source": "ru-bdu",
"kind": "reference",
"value": "https://ptsecurity.com/PT-2015-0206",
"decisionReason": null,
"recordedAt": "2025-10-14T08:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "positivetechnologiesadvisory",
"summary": "PT-2015-0206",
"url": "https://ptsecurity.com/PT-2015-0206"
}
],
"severity": "critical",
"summary": "Удалённый злоумышленник может вызвать отказ в обслуживании или получить доступ к данным.",
"title": "Множественные уязвимости криптопровайдера"
}
]

View File

@@ -1,11 +1,11 @@
[
{
"metadata": {
"ru-bdu.identifier": "BDU:2025-00001",
"ru-bdu.name": "Множественные уязвимости криптопровайдера"
},
"sha256": "c43df9c4a75a74b281ff09122bb8f63096a0a73b30df74d73c3bc997019bd4d4",
"status": "mapped",
"uri": "https://bdu.fstec.ru/vul/2025-00001"
}
[
{
"metadata": {
"ru-bdu.identifier": "BDU:2025-00001",
"ru-bdu.name": "Множественные уязвимости криптопровайдера"
},
"sha256": "c43df9c4a75a74b281ff09122bb8f63096a0a73b30df74d73c3bc997019bd4d4",
"status": "mapped",
"uri": "https://bdu.fstec.ru/vul/2025-00001"
}
]

View File

@@ -1,86 +1,86 @@
[
{
"documentUri": "https://bdu.fstec.ru/vul/2025-00001",
"payload": {
"identifier": "BDU:2025-00001",
"name": "Множественные уязвимости криптопровайдера",
"description": "Удалённый злоумышленник может вызвать отказ в обслуживании или получить доступ к данным.",
"solution": "Установить обновление 8.2.19.116 защищённого комплекса.",
"identifyDate": "2013-01-12T00:00:00+00:00",
"severityText": "Высокий уровень опасности (базовая оценка CVSS 2.0 составляет 7,5)",
"cvssVector": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
"cvssScore": 7.5,
"cvss3Vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"cvss3Score": 9.8,
"exploitStatus": "Существует в открытом доступе",
"incidentCount": 0,
"fixStatus": "Уязвимость устранена",
"vulStatus": "Подтверждена производителем",
"vulClass": "Уязвимость кода",
"vulState": "Опубликована",
"other": "Язык разработки ПО С",
"software": [
{
"vendor": "ООО «1С-Софт»",
"name": "1С:Предприятие",
"version": "8.2.18.96",
"platform": "Windows",
"types": [
"Прикладное ПО информационных систем"
]
},
{
"vendor": "ООО «1С-Софт»",
"name": "1С:Предприятие",
"version": "8.2.19.116",
"platform": "Не указана",
"types": [
"Прикладное ПО информационных систем"
]
}
],
"environment": [
{
"vendor": "Microsoft Corp",
"name": "Windows",
"version": "-",
"platform": "64-bit"
},
{
"vendor": "Microsoft Corp",
"name": "Windows",
"version": "-",
"platform": "32-bit"
}
],
"cwes": [
{
"identifier": "CWE-310",
"name": "Проблемы использования криптографии"
}
],
"sources": [
"https://advisories.example/BDU-2025-00001",
"http://mirror.example/ru-bdu/BDU-2025-00001"
],
"identifiers": [
{
"type": "CVE",
"value": "CVE-2015-0206",
"link": "https://nvd.nist.gov/vuln/detail/CVE-2015-0206"
},
{
"type": "CVE",
"value": "CVE-2009-3555",
"link": "https://nvd.nist.gov/vuln/detail/CVE-2009-3555"
},
{
"type": "Positive Technologies Advisory",
"value": "PT-2015-0206",
"link": "https://ptsecurity.com/PT-2015-0206"
}
]
},
"schemaVersion": "ru-bdu.v1"
}
[
{
"documentUri": "https://bdu.fstec.ru/vul/2025-00001",
"payload": {
"identifier": "BDU:2025-00001",
"name": "Множественные уязвимости криптопровайдера",
"description": "Удалённый злоумышленник может вызвать отказ в обслуживании или получить доступ к данным.",
"solution": "Установить обновление 8.2.19.116 защищённого комплекса.",
"identifyDate": "2013-01-12T00:00:00+00:00",
"severityText": "Высокий уровень опасности (базовая оценка CVSS 2.0 составляет 7,5)",
"cvssVector": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
"cvssScore": 7.5,
"cvss3Vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"cvss3Score": 9.8,
"exploitStatus": "Существует в открытом доступе",
"incidentCount": 0,
"fixStatus": "Уязвимость устранена",
"vulStatus": "Подтверждена производителем",
"vulClass": "Уязвимость кода",
"vulState": "Опубликована",
"other": "Язык разработки ПО С",
"software": [
{
"vendor": "ООО «1С-Софт»",
"name": "1С:Предприятие",
"version": "8.2.18.96",
"platform": "Windows",
"types": [
"Прикладное ПО информационных систем"
]
},
{
"vendor": "ООО «1С-Софт»",
"name": "1С:Предприятие",
"version": "8.2.19.116",
"platform": "Не указана",
"types": [
"Прикладное ПО информационных систем"
]
}
],
"environment": [
{
"vendor": "Microsoft Corp",
"name": "Windows",
"version": "-",
"platform": "64-bit"
},
{
"vendor": "Microsoft Corp",
"name": "Windows",
"version": "-",
"platform": "32-bit"
}
],
"cwes": [
{
"identifier": "CWE-310",
"name": "Проблемы использования криптографии"
}
],
"sources": [
"https://advisories.example/BDU-2025-00001",
"http://mirror.example/ru-bdu/BDU-2025-00001"
],
"identifiers": [
{
"type": "CVE",
"value": "CVE-2015-0206",
"link": "https://nvd.nist.gov/vuln/detail/CVE-2015-0206"
},
{
"type": "CVE",
"value": "CVE-2009-3555",
"link": "https://nvd.nist.gov/vuln/detail/CVE-2009-3555"
},
{
"type": "Positive Technologies Advisory",
"value": "PT-2015-0206",
"link": "https://ptsecurity.com/PT-2015-0206"
}
]
},
"schemaVersion": "ru-bdu.v1"
}
]

View File

@@ -1,11 +1,11 @@
[
{
"headers": {
"accept": "application/zip,application/octet-stream,application/x-zip-compressed",
"accept-Language": "ru-RU,ru; q=0.9,en-US; q=0.6,en; q=0.4",
"user-Agent": "StellaOps/Concelier,(+https://stella-ops.org)"
},
"method": "GET",
"uri": "https://bdu.fstec.ru/files/documents/vulxml.zip"
}
[
{
"headers": {
"accept": "application/zip,application/octet-stream,application/x-zip-compressed",
"accept-Language": "ru-RU,ru; q=0.9,en-US; q=0.6,en; q=0.4",
"user-Agent": "StellaOps/Concelier,(+https://stella-ops.org)"
},
"method": "GET",
"uri": "https://bdu.fstec.ru/files/documents/vulxml.zip"
}
]

View File

@@ -1,5 +1,5 @@
{
"lastSuccessfulFetch": "2025-10-14T08:00:00.0000000+00:00",
"pendingDocuments": [],
"pendingMappings": []
{
"lastSuccessfulFetch": "2025-10-14T08:00:00.0000000+00:00",
"pendingDocuments": [],
"pendingMappings": []
}

View File

@@ -1,303 +1,313 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.Ru.Bdu;
using StellaOps.Concelier.Connector.Ru.Bdu.Configuration;
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Concelier.Testing;
using Xunit;
using Xunit.Sdk;
namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests;
[Collection("mongo-fixture")]
public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime
{
private const string UpdateFixturesVariable = "UPDATE_BDU_FIXTURES";
private static readonly Uri ArchiveUri = new("https://bdu.fstec.ru/files/documents/vulxml.zip");
private readonly MongoIntegrationFixture _fixture;
private ConnectorTestHarness? _harness;
public RuBduConnectorSnapshotTests(MongoIntegrationFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task FetchParseMap_ProducesDeterministicSnapshots()
{
var harness = await EnsureHarnessAsync();
harness.Handler.AddResponse(ArchiveUri, BuildArchiveResponse);
var connector = harness.ServiceProvider.GetRequiredService<RuBduConnector>();
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
var initialState = await stateRepository.TryGetAsync(RuBduConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(initialState);
var cursorBeforeParse = initialState!.Cursor is null ? RuBduCursor.Empty : RuBduCursor.FromBson(initialState.Cursor);
Assert.NotEmpty(cursorBeforeParse.PendingDocuments);
var expectedDocumentIds = cursorBeforeParse.PendingDocuments.ToArray();
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
var documentsCollection = _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Document);
var documentCount = await documentsCollection.CountDocumentsAsync(Builders<BsonDocument>.Filter.Empty);
Assert.True(documentCount > 0, "Expected persisted documents after map stage");
var documentsSnapshot = await BuildDocumentsSnapshotAsync(harness.ServiceProvider, expectedDocumentIds);
WriteOrAssertSnapshot(documentsSnapshot, "ru-bdu-documents.snapshot.json");
var dtoSnapshot = await BuildDtoSnapshotAsync(harness.ServiceProvider);
WriteOrAssertSnapshot(dtoSnapshot, "ru-bdu-dtos.snapshot.json");
var advisoriesSnapshot = await BuildAdvisoriesSnapshotAsync(harness.ServiceProvider);
WriteOrAssertSnapshot(advisoriesSnapshot, "ru-bdu-advisories.snapshot.json");
var stateSnapshot = await BuildStateSnapshotAsync(harness.ServiceProvider);
WriteOrAssertSnapshot(stateSnapshot, "ru-bdu-state.snapshot.json");
var requestsSnapshot = BuildRequestsSnapshot(harness.Handler.Requests);
WriteOrAssertSnapshot(requestsSnapshot, "ru-bdu-requests.snapshot.json");
harness.Handler.AssertNoPendingResponses();
}
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
if (_harness is not null)
{
await _harness.DisposeAsync();
_harness = null;
}
}
private async Task<ConnectorTestHarness> EnsureHarnessAsync()
{
if (_harness is not null)
{
return _harness;
}
var initialTime = new DateTimeOffset(2025, 10, 14, 8, 0, 0, TimeSpan.Zero);
var harness = new ConnectorTestHarness(_fixture, initialTime, RuBduOptions.HttpClientName);
await harness.EnsureServiceProviderAsync(services =>
{
services.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddProvider(NullLoggerProvider.Instance);
});
services.AddRuBduConnector(options =>
{
options.BaseAddress = new Uri("https://bdu.fstec.ru/");
options.DataArchivePath = "files/documents/vulxml.zip";
options.MaxVulnerabilitiesPerFetch = 25;
options.RequestTimeout = TimeSpan.FromSeconds(30);
var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.Database.DatabaseNamespace.DatabaseName, "ru-bdu");
Directory.CreateDirectory(cacheRoot);
options.CacheDirectory = cacheRoot;
});
services.Configure<HttpClientFactoryOptions>(RuBduOptions.HttpClientName, options =>
{
options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = harness.Handler);
});
});
_harness = harness;
return harness;
}
private static HttpResponseMessage BuildArchiveResponse()
{
var archiveBytes = CreateArchiveBytes();
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent(archiveBytes),
};
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
response.Content.Headers.LastModified = new DateTimeOffset(2025, 10, 14, 9, 30, 0, TimeSpan.Zero);
response.Content.Headers.ContentLength = archiveBytes.Length;
return response;
}
private async Task<string> BuildDocumentsSnapshotAsync(IServiceProvider provider, IReadOnlyCollection<Guid> documentIds)
{
var documentStore = provider.GetRequiredService<IDocumentStore>();
var records = new List<object>(documentIds.Count);
foreach (var documentId in documentIds)
{
var record = await documentStore.FindAsync(documentId, CancellationToken.None);
if (record is null)
{
var existing = await _fixture.Database
.GetCollection<BsonDocument>("documents")
.Find(Builders<BsonDocument>.Filter.Empty)
.Project(Builders<BsonDocument>.Projection.Include("Uri"))
.ToListAsync(CancellationToken.None);
var uris = existing
.Select(document => document.GetValue("Uri", BsonValue.Create(string.Empty)).AsString)
.ToArray();
throw new XunitException($"Document id not found: {documentId}. Known URIs: {string.Join(", ", uris)}");
}
records.Add(new
{
record.Uri,
record.Status,
record.Sha256,
Metadata = record.Metadata is null
? null
: record.Metadata
.OrderBy(static pair => pair.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.OrdinalIgnoreCase)
});
}
var ordered = records
.OrderBy(static entry => entry?.GetType().GetProperty("Uri")?.GetValue(entry)?.ToString(), StringComparer.Ordinal)
.ToArray();
return SnapshotSerializer.ToSnapshot(ordered);
}
private async Task<string> BuildDtoSnapshotAsync(IServiceProvider provider)
{
var dtoStore = provider.GetRequiredService<IDtoStore>();
var documentStore = provider.GetRequiredService<IDocumentStore>();
var records = await dtoStore.GetBySourceAsync(RuBduConnectorPlugin.SourceName, 25, CancellationToken.None);
var entries = new List<object>(records.Count);
foreach (var record in records.OrderBy(static r => r.DocumentId))
{
var document = await documentStore.FindAsync(record.DocumentId, CancellationToken.None);
Assert.NotNull(document);
var payload = BsonTypeMapper.MapToDotNetValue(record.Payload);
entries.Add(new
{
DocumentUri = document!.Uri,
record.SchemaVersion,
Payload = payload,
});
}
return SnapshotSerializer.ToSnapshot(entries.OrderBy(static entry => entry.GetType().GetProperty("DocumentUri")!.GetValue(entry)?.ToString(), StringComparer.Ordinal).ToArray());
}
private async Task<string> BuildAdvisoriesSnapshotAsync(IServiceProvider provider)
{
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var advisories = await advisoryStore.GetRecentAsync(25, CancellationToken.None);
var ordered = advisories
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.Ordinal)
.ToArray();
return SnapshotSerializer.ToSnapshot(ordered);
}
private async Task<string> BuildStateSnapshotAsync(IServiceProvider provider)
{
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(RuBduConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var cursor = state!.Cursor is null ? RuBduCursor.Empty : RuBduCursor.FromBson(state.Cursor);
var snapshot = new
{
PendingDocuments = cursor.PendingDocuments.Select(static guid => guid.ToString()).OrderBy(static id => id, StringComparer.Ordinal).ToArray(),
PendingMappings = cursor.PendingMappings.Select(static guid => guid.ToString()).OrderBy(static id => id, StringComparer.Ordinal).ToArray(),
LastSuccessfulFetch = cursor.LastSuccessfulFetch?.ToUniversalTime().ToString("O"),
};
return SnapshotSerializer.ToSnapshot(snapshot);
}
private static string BuildRequestsSnapshot(IReadOnlyCollection<CannedHttpMessageHandler.CannedRequestRecord> requests)
{
var ordered = requests
.Select(record => new
{
Method = record.Method.Method,
Uri = record.Uri.ToString(),
Headers = record.Headers
.OrderBy(static kvp => kvp.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value, StringComparer.OrdinalIgnoreCase),
})
.OrderBy(static entry => entry.Uri, StringComparer.Ordinal)
.ToArray();
return SnapshotSerializer.ToSnapshot(ordered);
}
private static string ReadFixtureText(string filename)
{
var path = GetSourceFixturePath(filename);
return File.ReadAllText(path, Encoding.UTF8);
}
private static byte[] CreateArchiveBytes()
{
var xml = ReadFixtureText("export-sample.xml");
using var buffer = new MemoryStream();
using (var archive = new ZipArchive(buffer, ZipArchiveMode.Create, leaveOpen: true))
{
var entry = archive.CreateEntry("export/export.xml", CompressionLevel.NoCompression);
entry.LastWriteTime = new DateTimeOffset(2025, 10, 14, 9, 0, 0, TimeSpan.Zero);
using var entryStream = entry.Open();
using var writer = new StreamWriter(entryStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
writer.Write(xml);
}
return buffer.ToArray();
}
private static bool ShouldUpdateFixtures()
=> !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(UpdateFixturesVariable));
private static void WriteOrAssertSnapshot(string content, string filename)
{
var path = GetSourceFixturePath(filename);
if (ShouldUpdateFixtures())
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, content, Encoding.UTF8);
}
else
{
Assert.True(File.Exists(path), $"Snapshot '{filename}' is missing. Run {UpdateFixturesVariable}=1 dotnet test to regenerate fixtures.");
var expected = File.ReadAllText(path, Encoding.UTF8);
Assert.Equal(expected, content);
}
}
private static string GetSourceFixturePath(string relativeName)
=> Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Fixtures", relativeName));
}
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.Ru.Bdu;
using StellaOps.Concelier.Connector.Ru.Bdu.Configuration;
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Concelier.Testing;
using StellaOps.Cryptography.DependencyInjection;
using Xunit;
using Xunit.Sdk;
namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests;
[Collection("mongo-fixture")]
public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime
{
private const string UpdateFixturesVariable = "UPDATE_BDU_FIXTURES";
private static readonly Uri ArchiveUri = new("https://bdu.fstec.ru/files/documents/vulxml.zip");
private readonly MongoIntegrationFixture _fixture;
private ConnectorTestHarness? _harness;
public RuBduConnectorSnapshotTests(MongoIntegrationFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task FetchParseMap_ProducesDeterministicSnapshots()
{
var harness = await EnsureHarnessAsync();
harness.Handler.AddResponse(ArchiveUri, BuildArchiveResponse);
var connector = harness.ServiceProvider.GetRequiredService<RuBduConnector>();
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
var initialState = await stateRepository.TryGetAsync(RuBduConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(initialState);
var cursorBeforeParse = initialState!.Cursor is null ? RuBduCursor.Empty : RuBduCursor.FromBson(initialState.Cursor);
Assert.NotEmpty(cursorBeforeParse.PendingDocuments);
var expectedDocumentIds = cursorBeforeParse.PendingDocuments.ToArray();
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
var documentsCollection = _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Document);
var documentCount = await documentsCollection.CountDocumentsAsync(Builders<BsonDocument>.Filter.Empty);
Assert.True(documentCount > 0, "Expected persisted documents after map stage");
var documentsSnapshot = await BuildDocumentsSnapshotAsync(harness.ServiceProvider, expectedDocumentIds);
WriteOrAssertSnapshot(documentsSnapshot, "ru-bdu-documents.snapshot.json");
var dtoSnapshot = await BuildDtoSnapshotAsync(harness.ServiceProvider);
WriteOrAssertSnapshot(dtoSnapshot, "ru-bdu-dtos.snapshot.json");
var advisoriesSnapshot = await BuildAdvisoriesSnapshotAsync(harness.ServiceProvider);
WriteOrAssertSnapshot(advisoriesSnapshot, "ru-bdu-advisories.snapshot.json");
var stateSnapshot = await BuildStateSnapshotAsync(harness.ServiceProvider);
WriteOrAssertSnapshot(stateSnapshot, "ru-bdu-state.snapshot.json");
var requestsSnapshot = BuildRequestsSnapshot(harness.Handler.Requests);
WriteOrAssertSnapshot(requestsSnapshot, "ru-bdu-requests.snapshot.json");
harness.Handler.AssertNoPendingResponses();
}
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
if (_harness is not null)
{
await _harness.DisposeAsync();
_harness = null;
}
}
private async Task<ConnectorTestHarness> EnsureHarnessAsync()
{
if (_harness is not null)
{
return _harness;
}
var initialTime = new DateTimeOffset(2025, 10, 14, 8, 0, 0, TimeSpan.Zero);
var harness = new ConnectorTestHarness(_fixture, initialTime, RuBduOptions.HttpClientName);
await harness.EnsureServiceProviderAsync(services =>
{
services.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddProvider(NullLoggerProvider.Instance);
});
services.AddStellaOpsCrypto();
services.AddRuBduConnector(options =>
{
options.BaseAddress = new Uri("https://bdu.fstec.ru/");
options.DataArchivePath = "files/documents/vulxml.zip";
options.MaxVulnerabilitiesPerFetch = 25;
options.RequestTimeout = TimeSpan.FromSeconds(30);
var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.Database.DatabaseNamespace.DatabaseName, "ru-bdu");
Directory.CreateDirectory(cacheRoot);
options.CacheDirectory = cacheRoot;
});
services.Configure<HttpClientFactoryOptions>(RuBduOptions.HttpClientName, options =>
{
options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = harness.Handler);
});
});
_harness = harness;
return harness;
}
private static HttpResponseMessage BuildArchiveResponse()
{
var archiveBytes = CreateArchiveBytes();
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent(archiveBytes),
};
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
response.Content.Headers.LastModified = new DateTimeOffset(2025, 10, 14, 9, 30, 0, TimeSpan.Zero);
response.Content.Headers.ContentLength = archiveBytes.Length;
return response;
}
private async Task<string> BuildDocumentsSnapshotAsync(IServiceProvider provider, IReadOnlyCollection<Guid> documentIds)
{
var documentStore = provider.GetRequiredService<IDocumentStore>();
var records = new List<object>(documentIds.Count);
foreach (var documentId in documentIds)
{
var record = await documentStore.FindAsync(documentId, CancellationToken.None);
if (record is null)
{
var existing = await _fixture.Database
.GetCollection<BsonDocument>("documents")
.Find(Builders<BsonDocument>.Filter.Empty)
.Project(Builders<BsonDocument>.Projection.Include("Uri"))
.ToListAsync(CancellationToken.None);
var uris = existing
.Select(document => document.GetValue("Uri", BsonValue.Create(string.Empty)).AsString)
.ToArray();
throw new XunitException($"Document id not found: {documentId}. Known URIs: {string.Join(", ", uris)}");
}
records.Add(new
{
record.Uri,
record.Status,
record.Sha256,
Metadata = record.Metadata is null
? null
: record.Metadata
.OrderBy(static pair => pair.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.OrdinalIgnoreCase)
});
}
var ordered = records
.OrderBy(static entry => entry?.GetType().GetProperty("Uri")?.GetValue(entry)?.ToString(), StringComparer.Ordinal)
.ToArray();
return SnapshotSerializer.ToSnapshot(ordered);
}
private async Task<string> BuildDtoSnapshotAsync(IServiceProvider provider)
{
var dtoStore = provider.GetRequiredService<IDtoStore>();
var documentStore = provider.GetRequiredService<IDocumentStore>();
var records = await dtoStore.GetBySourceAsync(RuBduConnectorPlugin.SourceName, 25, CancellationToken.None);
var entries = new List<object>(records.Count);
foreach (var record in records.OrderBy(static r => r.DocumentId))
{
var document = await documentStore.FindAsync(record.DocumentId, CancellationToken.None);
Assert.NotNull(document);
var payload = BsonTypeMapper.MapToDotNetValue(record.Payload);
entries.Add(new
{
DocumentUri = document!.Uri,
record.SchemaVersion,
Payload = payload,
});
}
return SnapshotSerializer.ToSnapshot(entries.OrderBy(static entry => entry.GetType().GetProperty("DocumentUri")!.GetValue(entry)?.ToString(), StringComparer.Ordinal).ToArray());
}
private async Task<string> BuildAdvisoriesSnapshotAsync(IServiceProvider provider)
{
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var advisories = await advisoryStore.GetRecentAsync(25, CancellationToken.None);
var ordered = advisories
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.Ordinal)
.ToArray();
return SnapshotSerializer.ToSnapshot(ordered);
}
private async Task<string> BuildStateSnapshotAsync(IServiceProvider provider)
{
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(RuBduConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var cursor = state!.Cursor is null ? RuBduCursor.Empty : RuBduCursor.FromBson(state.Cursor);
var snapshot = new
{
PendingDocuments = cursor.PendingDocuments.Select(static guid => guid.ToString()).OrderBy(static id => id, StringComparer.Ordinal).ToArray(),
PendingMappings = cursor.PendingMappings.Select(static guid => guid.ToString()).OrderBy(static id => id, StringComparer.Ordinal).ToArray(),
LastSuccessfulFetch = cursor.LastSuccessfulFetch?.ToUniversalTime().ToString("O"),
};
return SnapshotSerializer.ToSnapshot(snapshot);
}
private static string BuildRequestsSnapshot(IReadOnlyCollection<CannedHttpMessageHandler.CannedRequestRecord> requests)
{
var ordered = requests
.Select(record => new
{
Method = record.Method.Method,
Uri = record.Uri.ToString(),
Headers = record.Headers
.OrderBy(static kvp => kvp.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value, StringComparer.OrdinalIgnoreCase),
})
.OrderBy(static entry => entry.Uri, StringComparer.Ordinal)
.ToArray();
return SnapshotSerializer.ToSnapshot(ordered);
}
private static string ReadFixtureText(string filename)
{
var path = GetSourceFixturePath(filename);
return File.ReadAllText(path, Encoding.UTF8);
}
private static byte[] CreateArchiveBytes()
{
var xml = ReadFixtureText("export-sample.xml");
using var buffer = new MemoryStream();
using (var archive = new ZipArchive(buffer, ZipArchiveMode.Create, leaveOpen: true))
{
var entry = archive.CreateEntry("export/export.xml", CompressionLevel.NoCompression);
entry.LastWriteTime = new DateTimeOffset(2025, 10, 14, 9, 0, 0, TimeSpan.Zero);
using var entryStream = entry.Open();
using var writer = new StreamWriter(entryStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
writer.Write(xml);
}
return buffer.ToArray();
}
private static bool ShouldUpdateFixtures()
=> !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(UpdateFixturesVariable));
private static void WriteOrAssertSnapshot(string content, string filename)
{
var path = GetSourceFixturePath(filename);
if (ShouldUpdateFixtures())
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, NormalizeLineEndings(content), Encoding.UTF8);
}
else
{
Assert.True(File.Exists(path), $"Snapshot '{filename}' is missing. Run {UpdateFixturesVariable}=1 dotnet test to regenerate fixtures.");
var expected = File.ReadAllText(path, Encoding.UTF8);
Assert.Equal(NormalizeLineEndings(expected), NormalizeLineEndings(content));
}
}
private static string GetSourceFixturePath(string relativeName)
=> Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Fixtures", relativeName));
private static string NormalizeLineEndings(string value)
{
var normalized = value.Replace("\r\n", "\n", StringComparison.Ordinal);
return normalized.Length > 0 && normalized[0] == '\ufeff'
? normalized[1..]
: normalized;
}
}

View File

@@ -10,5 +10,6 @@
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Normalization/StellaOps.Concelier.Normalization.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/StellaOps.Concelier.Connector.Ru.Bdu.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography.DependencyInjection/StellaOps.Cryptography.DependencyInjection.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,495 +1,501 @@
[
{
"advisoryKey": "BDU:2025-01001",
"affectedPackages": [
{
"type": "ics-vendor",
"identifier": "SampleVendor SampleGateway",
"platform": "Energy, ICS",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": "2.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": ">= 2.0",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": "2.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false,
"style": "greaterThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "SampleVendor SampleGateway >= 2.0 All platforms",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": ">= 2.0",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "gte",
"min": "2.0",
"minInclusive": true,
"max": null,
"maxInclusive": null,
"value": null,
"notes": "SampleVendor SampleGateway >= 2.0 All platforms"
}
],
"statuses": [
{
"provenance": {
"source": "ru-nkcki",
"kind": "package-status",
"value": "patch_available",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ru-nkcki",
"kind": "package",
"value": "SampleVendor SampleGateway >= 2.0 All platforms",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
},
{
"type": "ics-vendor",
"identifier": "SampleVendor SampleSCADA",
"platform": "Energy, ICS",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": "4.2",
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": "<= 4.2",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": null,
"introducedInclusive": true,
"lastAffected": "4.2",
"lastAffectedInclusive": true,
"style": "lessThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "SampleVendor SampleSCADA <= 4.2",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "<= 4.2",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "lte",
"min": null,
"minInclusive": null,
"max": "4.2",
"maxInclusive": true,
"value": null,
"notes": "SampleVendor SampleSCADA <= 4.2"
}
],
"statuses": [
{
"provenance": {
"source": "ru-nkcki",
"kind": "package-status",
"value": "patch_available",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ru-nkcki",
"kind": "package",
"value": "SampleVendor SampleSCADA <= 4.2",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"BDU:2025-01001",
"CVE-2025-0101"
],
"credits": [],
"cvssMetrics": [
{
"baseScore": 8.5,
"baseSeverity": "high",
"provenance": {
"source": "ru-nkcki",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H",
"version": "3.1"
},
{
"baseScore": 6.4,
"baseSeverity": "medium",
"provenance": {
"source": "ru-nkcki",
"kind": "cvss",
"value": "CVSS:4.0/AV:N/AC:H/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:4.0/AV:N/AC:H/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H",
"version": "4.0"
}
],
"exploitKnown": true,
"language": "ru",
"modified": "2025-09-22T00:00:00+00:00",
"provenance": [
{
"source": "ru-nkcki",
"kind": "advisory",
"value": "BDU:2025-01001",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2025-09-20T00:00:00+00:00",
"references": [
{
"kind": "details",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://bdu.fstec.ru/vul/2025-01001",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "bdu",
"summary": null,
"url": "https://bdu.fstec.ru/vul/2025-01001"
},
{
"kind": "details",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://cert.gov.ru/materialy/uyazvimosti/2025-01001",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-nkcki",
"summary": null,
"url": "https://cert.gov.ru/materialy/uyazvimosti/2025-01001"
},
{
"kind": "cwe",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/321.html",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "cwe",
"summary": "Use of Hard-coded Cryptographic Key",
"url": "https://cwe.mitre.org/data/definitions/321.html"
},
{
"kind": "external",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://vendor.example/advisories/sample-scada",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": null,
"summary": null,
"url": "https://vendor.example/advisories/sample-scada"
}
],
"severity": "critical",
"summary": "Authenticated RCE in Sample SCADA",
"title": "Authenticated RCE in Sample SCADA"
},
{
"advisoryKey": "BDU:2024-00011",
"affectedPackages": [
{
"type": "cpe",
"identifier": "LegacyPanel",
"platform": "Software",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": "2.5",
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": "<= 2.5",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": null,
"introducedInclusive": true,
"lastAffected": "2.5",
"lastAffectedInclusive": true,
"style": "lessThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "LegacyPanel 1.0 - 2.5",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "<= 2.5",
"rangeKind": "semver"
},
{
"fixedVersion": null,
"introducedVersion": "1.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": ">= 1.0",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": "1.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false,
"style": "greaterThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "LegacyPanel 1.0 - 2.5",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": ">= 1.0",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "gte",
"min": "1.0",
"minInclusive": true,
"max": null,
"maxInclusive": null,
"value": null,
"notes": "LegacyPanel 1.0 - 2.5"
},
{
"scheme": "semver",
"type": "lte",
"min": null,
"minInclusive": null,
"max": "2.5",
"maxInclusive": true,
"value": null,
"notes": "LegacyPanel 1.0 - 2.5"
}
],
"statuses": [
{
"provenance": {
"source": "ru-nkcki",
"kind": "package-status",
"value": "affected",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "affected"
}
],
"provenance": [
{
"source": "ru-nkcki",
"kind": "package",
"value": "LegacyPanel 1.0 - 2.5",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"BDU:2024-00011"
],
"credits": [],
"cvssMetrics": [
{
"baseScore": 8.8,
"baseSeverity": "high",
"provenance": {
"source": "ru-nkcki",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"exploitKnown": true,
"language": "ru",
"modified": "2024-08-02T00:00:00+00:00",
"provenance": [
{
"source": "ru-nkcki",
"kind": "advisory",
"value": "BDU:2024-00011",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2024-08-01T00:00:00+00:00",
"references": [
{
"kind": "details",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://bdu.fstec.ru/vul/2024-00011",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "bdu",
"summary": null,
"url": "https://bdu.fstec.ru/vul/2024-00011"
},
{
"kind": "details",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://cert.gov.ru/materialy/uyazvimosti/2024-00011",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-nkcki",
"summary": null,
"url": "https://cert.gov.ru/materialy/uyazvimosti/2024-00011"
}
],
"severity": "high",
"summary": "Legacy panel overflow",
"title": "Legacy panel overflow"
}
[
{
"advisoryKey": "BDU:2025-01001",
"affectedPackages": [
{
"type": "ics-vendor",
"identifier": "SampleVendor SampleGateway",
"platform": "Energy, ICS",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": "2.0.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": ">= 2.0.0",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": "2.0.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false,
"style": "greaterThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "SampleVendor SampleGateway >= 2.0 All platforms",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": ">= 2.0.0",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "gte",
"min": "2.0.0",
"minInclusive": true,
"max": null,
"maxInclusive": null,
"value": null,
"notes": "SampleVendor SampleGateway >= 2.0 All platforms"
}
],
"statuses": [
{
"provenance": {
"source": "ru-nkcki",
"kind": "package-status",
"value": "patch_available",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ru-nkcki",
"kind": "package",
"value": "SampleVendor SampleGateway >= 2.0 All platforms",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
},
{
"type": "ics-vendor",
"identifier": "SampleVendor SampleSCADA",
"platform": "Energy, ICS",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": "4.2.0",
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": "<= 4.2.0",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": null,
"introducedInclusive": true,
"lastAffected": "4.2.0",
"lastAffectedInclusive": true,
"style": "lessThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "SampleVendor SampleSCADA <= 4.2",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "<= 4.2.0",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "lte",
"min": null,
"minInclusive": null,
"max": "4.2.0",
"maxInclusive": true,
"value": null,
"notes": "SampleVendor SampleSCADA <= 4.2"
}
],
"statuses": [
{
"provenance": {
"source": "ru-nkcki",
"kind": "package-status",
"value": "patch_available",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ru-nkcki",
"kind": "package",
"value": "SampleVendor SampleSCADA <= 4.2",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"BDU:2025-01001",
"CVE-2025-0101"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [
{
"baseScore": 8.5,
"baseSeverity": "high",
"provenance": {
"source": "ru-nkcki",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H",
"version": "3.1"
},
{
"baseScore": 6.4,
"baseSeverity": "medium",
"provenance": {
"source": "ru-nkcki",
"kind": "cvss",
"value": "CVSS:4.0/AV:N/AC:H/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:4.0/AV:N/AC:H/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H",
"version": "4.0"
}
],
"cwes": [],
"description": null,
"exploitKnown": true,
"language": "ru",
"modified": "2025-09-22T00:00:00+00:00",
"provenance": [
{
"source": "ru-nkcki",
"kind": "advisory",
"value": "BDU:2025-01001",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2025-09-20T00:00:00+00:00",
"references": [
{
"kind": "details",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://bdu.fstec.ru/vul/2025-01001",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "bdu",
"summary": null,
"url": "https://bdu.fstec.ru/vul/2025-01001"
},
{
"kind": "details",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://cert.gov.ru/materialy/uyazvimosti/2025-01001",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-nkcki",
"summary": null,
"url": "https://cert.gov.ru/materialy/uyazvimosti/2025-01001"
},
{
"kind": "cwe",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/321.html",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "cwe",
"summary": "Use of Hard-coded Cryptographic Key",
"url": "https://cwe.mitre.org/data/definitions/321.html"
},
{
"kind": "external",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://vendor.example/advisories/sample-scada",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": null,
"summary": null,
"url": "https://vendor.example/advisories/sample-scada"
}
],
"severity": "critical",
"summary": "Authenticated RCE in Sample SCADA",
"title": "Authenticated RCE in Sample SCADA"
},
{
"advisoryKey": "BDU:2024-00011",
"affectedPackages": [
{
"type": "cpe",
"identifier": "LegacyPanel",
"platform": "Software",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": "2.5.0",
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": "<= 2.5.0",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": null,
"introducedInclusive": true,
"lastAffected": "2.5.0",
"lastAffectedInclusive": true,
"style": "lessThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "LegacyPanel 1.0 - 2.5",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "<= 2.5.0",
"rangeKind": "semver"
},
{
"fixedVersion": null,
"introducedVersion": "1.0.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": ">= 1.0.0",
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": "1.0.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false,
"style": "greaterThanOrEqual"
},
"vendorExtensions": null
},
"provenance": {
"source": "ru-nkcki",
"kind": "package-range",
"value": "LegacyPanel 1.0 - 2.5",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": ">= 1.0.0",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "gte",
"min": "1.0.0",
"minInclusive": true,
"max": null,
"maxInclusive": null,
"value": null,
"notes": "LegacyPanel 1.0 - 2.5"
},
{
"scheme": "semver",
"type": "lte",
"min": null,
"minInclusive": null,
"max": "2.5.0",
"maxInclusive": true,
"value": null,
"notes": "LegacyPanel 1.0 - 2.5"
}
],
"statuses": [
{
"provenance": {
"source": "ru-nkcki",
"kind": "package-status",
"value": "affected",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "affected"
}
],
"provenance": [
{
"source": "ru-nkcki",
"kind": "package",
"value": "LegacyPanel 1.0 - 2.5",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"BDU:2024-00011"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [
{
"baseScore": 8.8,
"baseSeverity": "high",
"provenance": {
"source": "ru-nkcki",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"cwes": [],
"description": null,
"exploitKnown": true,
"language": "ru",
"modified": "2024-08-02T00:00:00+00:00",
"provenance": [
{
"source": "ru-nkcki",
"kind": "advisory",
"value": "BDU:2024-00011",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2024-08-01T00:00:00+00:00",
"references": [
{
"kind": "details",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://bdu.fstec.ru/vul/2024-00011",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "bdu",
"summary": null,
"url": "https://bdu.fstec.ru/vul/2024-00011"
},
{
"kind": "details",
"provenance": {
"source": "ru-nkcki",
"kind": "reference",
"value": "https://cert.gov.ru/materialy/uyazvimosti/2024-00011",
"decisionReason": null,
"recordedAt": "2025-10-12T00:01:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "ru-nkcki",
"summary": null,
"url": "https://cert.gov.ru/materialy/uyazvimosti/2024-00011"
}
],
"severity": "high",
"summary": "Legacy panel overflow",
"title": "Legacy panel overflow"
}
]

View File

@@ -19,12 +19,13 @@ using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.Ru.Nkcki;
using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Testing;
using StellaOps.Concelier.Models;
using MongoDB.Driver;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Testing;
using StellaOps.Concelier.Models;
using MongoDB.Driver;
using StellaOps.Cryptography.DependencyInjection;
using Xunit;
namespace StellaOps.Concelier.Connector.Ru.Nkcki.Tests;
@@ -123,14 +124,15 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
services.AddSingleton<TimeProvider>(_timeProvider);
services.AddMongoStorage(options =>
{
options.ConnectionString = _fixture.Runner.ConnectionString;
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
options.CommandTimeout = TimeSpan.FromSeconds(5);
});
services.AddSourceCommon();
services.AddMongoStorage(options =>
{
options.ConnectionString = _fixture.Runner.ConnectionString;
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
options.CommandTimeout = TimeSpan.FromSeconds(5);
});
services.AddStellaOpsCrypto();
services.AddSourceCommon();
services.AddRuNkckiConnector(options =>
{
options.BaseAddress = new Uri("https://cert.gov.ru/");

View File

@@ -10,5 +10,6 @@
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Normalization/StellaOps.Concelier.Normalization.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/StellaOps.Concelier.Connector.Ru.Nkcki.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography.DependencyInjection/StellaOps.Cryptography.DependencyInjection.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -8,8 +8,9 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.StellaOpsMirror/StellaOps.Concelier.Connector.StellaOpsMirror.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography.DependencyInjection/StellaOps.Cryptography.DependencyInjection.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*.json" CopyToOutputDirectory="Always" />
</ItemGroup>
</Project>
</Project>

View File

@@ -23,7 +23,8 @@ using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Concelier.Testing;
using StellaOps.Cryptography;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Concelier.Models;
using Xunit;
@@ -287,9 +288,7 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
options.CommandTimeout = TimeSpan.FromSeconds(5);
});
services.AddSingleton<DefaultCryptoProvider>();
services.AddSingleton<ICryptoProvider>(sp => sp.GetRequiredService<DefaultCryptoProvider>());
services.AddSingleton<ICryptoProviderRegistry>(sp => new CryptoProviderRegistry(sp.GetServices<ICryptoProvider>()));
services.AddStellaOpsCrypto();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
@@ -20,23 +21,24 @@ public sealed class CiscoMapperTests
var published = new DateTimeOffset(2025, 10, 1, 0, 0, 0, TimeSpan.Zero);
var updated = published.AddDays(1);
var dto = new CiscoAdvisoryDto(
AdvisoryId: "CISCO-SA-TEST",
Title: "Test Advisory",
Summary: "Sample summary",
Severity: "High",
var dto = new CiscoAdvisoryDto(
AdvisoryId: "CISCO-SA-TEST",
Title: "Test Advisory",
Summary: "Sample summary",
Severity: "High",
Published: published,
Updated: updated,
PublicationUrl: "https://example.com/advisory",
CsafUrl: "https://sec.cloudapps.cisco.com/csaf/test.json",
CvrfUrl: "https://example.com/cvrf.xml",
CvssBaseScore: 9.8,
Cves: new List<string> { "CVE-2024-0001" },
BugIds: new List<string> { "BUG123" },
Products: new List<CiscoAffectedProductDto>
{
new("Cisco Widget", "PID-1", "1.2.3", new [] { AffectedPackageStatusCatalog.KnownAffected })
});
CvssBaseScore: 9.8,
Cves: new List<string> { "CVE-2024-0001" },
BugIds: new List<string> { "BUG123" },
Products: new List<CiscoAffectedProductDto>
{
new("Cisco Widget", "PID-1", "1.2.3", new [] { AffectedPackageStatusCatalog.KnownAffected }),
new("Cisco Router", "PID-2", ">=1.0.0 <1.4.0", new [] { AffectedPackageStatusCatalog.KnownAffected })
});
var document = new DocumentRecord(
Id: Guid.NewGuid(),
@@ -62,18 +64,38 @@ public sealed class CiscoMapperTests
advisory.Aliases.Should().Contain(new[] { "CISCO-SA-TEST", "CVE-2024-0001", "BUG123" });
advisory.References.Should().Contain(reference => reference.Url == "https://example.com/advisory");
advisory.References.Should().Contain(reference => reference.Url == "https://sec.cloudapps.cisco.com/csaf/test.json");
advisory.AffectedPackages.Should().HaveCount(1);
var package = advisory.AffectedPackages[0];
package.Type.Should().Be(AffectedPackageTypes.Vendor);
package.Identifier.Should().Be("Cisco Widget");
package.Statuses.Should().ContainSingle(status => status.Status == AffectedPackageStatusCatalog.KnownAffected);
package.VersionRanges.Should().ContainSingle();
var range = package.VersionRanges[0];
range.RangeKind.Should().Be("semver");
range.Provenance.Source.Should().Be(VndrCiscoConnectorPlugin.SourceName);
range.Primitives.Should().NotBeNull();
range.Primitives!.SemVer.Should().NotBeNull();
range.Primitives.SemVer!.ExactValue.Should().Be("1.2.3");
}
}
advisory.AffectedPackages.Should().HaveCount(2);
var package = advisory.AffectedPackages.Single(p => p.Identifier == "Cisco Widget");
package.Type.Should().Be(AffectedPackageTypes.Vendor);
package.Identifier.Should().Be("Cisco Widget");
package.Statuses.Should().ContainSingle(status => status.Status == AffectedPackageStatusCatalog.KnownAffected);
package.VersionRanges.Should().ContainSingle();
var range = package.VersionRanges[0];
range.RangeKind.Should().Be("semver");
range.Provenance.Source.Should().Be(VndrCiscoConnectorPlugin.SourceName);
range.Primitives.Should().NotBeNull();
range.Primitives!.SemVer.Should().NotBeNull();
range.Primitives.SemVer!.ExactValue.Should().Be("1.2.3");
package.NormalizedVersions.Should().ContainSingle();
var normalized = package.NormalizedVersions[0];
normalized.Scheme.Should().Be(NormalizedVersionSchemes.SemVer);
normalized.Type.Should().Be(NormalizedVersionRuleTypes.Exact);
normalized.Value.Should().Be("1.2.3");
normalized.Notes.Should().Be("cisco:pid-1");
var rangePackage = advisory.AffectedPackages.Single(p => p.Identifier == "Cisco Router");
rangePackage.VersionRanges.Should().ContainSingle();
var rangePackageRange = rangePackage.VersionRanges[0];
rangePackageRange.Primitives!.SemVer.Should().NotBeNull();
rangePackageRange.Primitives.SemVer!.Introduced.Should().Be("1.0.0");
rangePackageRange.Primitives.SemVer.Fixed.Should().Be("1.4.0");
rangePackage.NormalizedVersions.Should().ContainSingle(rule =>
rule.Min == "1.0.0" &&
rule.Max == "1.4.0" &&
rule.MinInclusive == true &&
rule.MaxInclusive == false &&
rule.Notes == "cisco:pid-2");
}
}

View File

@@ -1,275 +1,306 @@
[
{
"advisoryKey": "VMSA-2024-0001",
"affectedPackages": [
{
"identifier": "VMware ESXi 7.0",
"platform": null,
"provenance": [
{
"fieldMask": [],
"kind": "affected",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "VMware ESXi 7.0"
}
],
"statuses": [],
"type": "vendor",
"versionRanges": [
{
"fixedVersion": "7.0u3f",
"introducedVersion": "7.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": null,
"fixed": null,
"fixedInclusive": false,
"introduced": "7.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false
},
"vendorExtensions": {
"vmware.product": "VMware ESXi 7.0",
"vmware.version.raw": "7.0",
"vmware.fixedVersion.raw": "7.0u3f"
}
},
"provenance": {
"fieldMask": [],
"kind": "range",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "VMware ESXi 7.0"
},
"rangeExpression": "7.0",
"rangeKind": "vendor"
}
]
},
{
"identifier": "VMware vCenter Server 8.0",
"platform": null,
"provenance": [
{
"fieldMask": [],
"kind": "affected",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "VMware vCenter Server 8.0"
}
],
"statuses": [],
"type": "vendor",
"versionRanges": [
{
"fixedVersion": "8.0a",
"introducedVersion": "8.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": null,
"fixed": null,
"fixedInclusive": false,
"introduced": "8.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false
},
"vendorExtensions": {
"vmware.product": "VMware vCenter Server 8.0",
"vmware.version.raw": "8.0",
"vmware.fixedVersion.raw": "8.0a"
}
},
"provenance": {
"fieldMask": [],
"kind": "range",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "VMware vCenter Server 8.0"
},
"rangeExpression": "8.0",
"rangeKind": "vendor"
}
]
}
],
"aliases": [
"CVE-2024-1000",
"CVE-2024-1001",
"VMSA-2024-0001"
],
"cvssMetrics": [],
"exploitKnown": false,
"language": "en",
"modified": "2024-04-01T10:00:00+00:00",
"provenance": [
{
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "https://vmware.example/api/vmsa/VMSA-2024-0001.json"
},
{
"fieldMask": [],
"kind": "mapping",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "VMSA-2024-0001"
}
],
"published": "2024-04-01T10:00:00+00:00",
"references": [
{
"kind": "kb",
"provenance": {
"fieldMask": [],
"kind": "reference",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "https://kb.vmware.example/90234"
},
"sourceTag": "kb",
"summary": null,
"url": "https://kb.vmware.example/90234"
},
{
"kind": "advisory",
"provenance": {
"fieldMask": [],
"kind": "reference",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "https://www.vmware.com/security/advisories/VMSA-2024-0001.html"
},
"sourceTag": "advisory",
"summary": null,
"url": "https://www.vmware.com/security/advisories/VMSA-2024-0001.html"
}
],
"severity": null,
"summary": "Security updates for VMware ESXi 7.0 and vCenter Server 8.0 resolve multiple vulnerabilities.",
"title": "VMware ESXi and vCenter Server updates address vulnerabilities"
},
{
"advisoryKey": "VMSA-2024-0002",
"affectedPackages": [
{
"identifier": "VMware Cloud Foundation 5.x",
"platform": null,
"provenance": [
{
"fieldMask": [],
"kind": "affected",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "VMware Cloud Foundation 5.x"
}
],
"statuses": [],
"type": "vendor",
"versionRanges": [
{
"fixedVersion": "5.1.1",
"introducedVersion": "5.1",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": null,
"fixed": "5.1.1",
"fixedInclusive": false,
"introduced": "5.1",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false
},
"vendorExtensions": {
"vmware.product": "VMware Cloud Foundation 5.x",
"vmware.version.raw": "5.1",
"vmware.fixedVersion.raw": "5.1.1"
}
},
"provenance": {
"fieldMask": [],
"kind": "range",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "VMware Cloud Foundation 5.x"
},
"rangeExpression": "5.1",
"rangeKind": "vendor"
}
]
}
],
"aliases": [
"CVE-2024-2000",
"VMSA-2024-0002"
],
"cvssMetrics": [],
"exploitKnown": false,
"language": "en",
"modified": "2024-04-02T09:00:00+00:00",
"provenance": [
{
"fieldMask": [],
"kind": "document",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "https://vmware.example/api/vmsa/VMSA-2024-0002.json"
},
{
"fieldMask": [],
"kind": "mapping",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "VMSA-2024-0002"
}
],
"published": "2024-04-02T09:00:00+00:00",
"references": [
{
"kind": "kb",
"provenance": {
"fieldMask": [],
"kind": "reference",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "https://kb.vmware.example/91234"
},
"sourceTag": "kb",
"summary": null,
"url": "https://kb.vmware.example/91234"
},
{
"kind": "advisory",
"provenance": {
"fieldMask": [],
"kind": "reference",
"recordedAt": "2024-04-05T00:00:00+00:00",
"source": "vmware",
"value": "https://www.vmware.com/security/advisories/VMSA-2024-0002.html"
},
"sourceTag": "advisory",
"summary": null,
"url": "https://www.vmware.com/security/advisories/VMSA-2024-0002.html"
}
],
"severity": null,
"summary": "An update is available for VMware Cloud Foundation components to address a remote code execution vulnerability.",
"title": "VMware Cloud Foundation remote code execution vulnerability"
}
[
{
"advisoryKey": "VMSA-2024-0001",
"affectedPackages": [
{
"type": "vendor",
"identifier": "VMware ESXi 7.0",
"platform": null,
"versionRanges": [
{
"fixedVersion": "7.0u3f",
"introducedVersion": "7.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": null,
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": "7.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false,
"style": "greaterThanOrEqual"
},
"vendorExtensions": {
"vmware.product": "VMware ESXi 7.0",
"vmware.version.raw": "7.0",
"vmware.fixedVersion.raw": "7.0u3f"
}
},
"provenance": {
"source": "vmware",
"kind": "range",
"value": "VMware ESXi 7.0",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
},
"rangeExpression": "7.0",
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "vmware",
"kind": "affected",
"value": "VMware ESXi 7.0",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
}
]
},
{
"type": "vendor",
"identifier": "VMware vCenter Server 8.0",
"platform": null,
"versionRanges": [
{
"fixedVersion": "8.0a",
"introducedVersion": "8.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": null,
"exactValue": null,
"fixed": null,
"fixedInclusive": false,
"introduced": "8.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false,
"style": "greaterThanOrEqual"
},
"vendorExtensions": {
"vmware.product": "VMware vCenter Server 8.0",
"vmware.version.raw": "8.0",
"vmware.fixedVersion.raw": "8.0a"
}
},
"provenance": {
"source": "vmware",
"kind": "range",
"value": "VMware vCenter Server 8.0",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
},
"rangeExpression": "8.0",
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "vmware",
"kind": "affected",
"value": "VMware vCenter Server 8.0",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CVE-2024-1000",
"CVE-2024-1001",
"VMSA-2024-0001"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": "2024-04-01T10:00:00+00:00",
"provenance": [
{
"source": "vmware",
"kind": "document",
"value": "https://vmware.example/api/vmsa/VMSA-2024-0001.json",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
},
{
"source": "vmware",
"kind": "mapping",
"value": "VMSA-2024-0001",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
}
],
"published": "2024-04-01T10:00:00+00:00",
"references": [
{
"kind": "kb",
"provenance": {
"source": "vmware",
"kind": "reference",
"value": "https://kb.vmware.example/90234",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "kb",
"summary": null,
"url": "https://kb.vmware.example/90234"
},
{
"kind": "advisory",
"provenance": {
"source": "vmware",
"kind": "reference",
"value": "https://www.vmware.com/security/advisories/VMSA-2024-0001.html",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "advisory",
"summary": null,
"url": "https://www.vmware.com/security/advisories/VMSA-2024-0001.html"
}
],
"severity": null,
"summary": "Security updates for VMware ESXi 7.0 and vCenter Server 8.0 resolve multiple vulnerabilities.",
"title": "VMware ESXi and vCenter Server updates address vulnerabilities"
},
{
"advisoryKey": "VMSA-2024-0002",
"affectedPackages": [
{
"type": "vendor",
"identifier": "VMware Cloud Foundation 5.x",
"platform": null,
"versionRanges": [
{
"fixedVersion": "5.1.1",
"introducedVersion": "5.1",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": null,
"exactValue": null,
"fixed": "5.1.1",
"fixedInclusive": false,
"introduced": "5.1",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": false,
"style": "range"
},
"vendorExtensions": {
"vmware.product": "VMware Cloud Foundation 5.x",
"vmware.version.raw": "5.1",
"vmware.fixedVersion.raw": "5.1.1"
}
},
"provenance": {
"source": "vmware",
"kind": "range",
"value": "VMware Cloud Foundation 5.x",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
},
"rangeExpression": "5.1",
"rangeKind": "vendor"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "vmware",
"kind": "affected",
"value": "VMware Cloud Foundation 5.x",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CVE-2024-2000",
"VMSA-2024-0002"
],
"canonicalMetricId": null,
"credits": [],
"cvssMetrics": [],
"cwes": [],
"description": null,
"exploitKnown": false,
"language": "en",
"modified": "2024-04-02T09:00:00+00:00",
"provenance": [
{
"source": "vmware",
"kind": "document",
"value": "https://vmware.example/api/vmsa/VMSA-2024-0002.json",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
},
{
"source": "vmware",
"kind": "mapping",
"value": "VMSA-2024-0002",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
}
],
"published": "2024-04-02T09:00:00+00:00",
"references": [
{
"kind": "kb",
"provenance": {
"source": "vmware",
"kind": "reference",
"value": "https://kb.vmware.example/91234",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "kb",
"summary": null,
"url": "https://kb.vmware.example/91234"
},
{
"kind": "advisory",
"provenance": {
"source": "vmware",
"kind": "reference",
"value": "https://www.vmware.com/security/advisories/VMSA-2024-0002.html",
"decisionReason": null,
"recordedAt": "2024-04-05T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "advisory",
"summary": null,
"url": "https://www.vmware.com/security/advisories/VMSA-2024-0002.html"
}
],
"severity": null,
"summary": "An update is available for VMware Cloud Foundation components to address a remote code execution vulnerability.",
"title": "VMware Cloud Foundation remote code execution vulnerability"
}
]

View File

@@ -1,17 +1,20 @@
using System.Collections.Immutable;
using System.Text.Json;
using StellaOps.Aoc;
using StellaOps.Concelier.Core.Aoc;
using StellaOps.Concelier.RawModels;
namespace StellaOps.Concelier.Core.Tests.Aoc;
public sealed class AdvisoryRawWriteGuardTests
{
private static AdvisoryRawDocument CreateDocument(
string tenant = "tenant-a",
bool signaturePresent = false,
bool includeSignaturePayload = true)
using System.Text.Json;
using Microsoft.Extensions.Options;
using StellaOps.Aoc;
using StellaOps.Concelier.Core.Aoc;
using StellaOps.Concelier.RawModels;
namespace StellaOps.Concelier.Core.Tests.Aoc;
public sealed class AdvisoryRawWriteGuardTests
{
private static readonly AocGuardOptions GuardOptions = AocGuardOptions.Default;
private static AdvisoryRawDocument CreateDocument(
string tenant = "tenant-a",
bool signaturePresent = false,
bool includeSignaturePayload = true)
{
using var rawDocument = JsonDocument.Parse("""{"id":"demo"}""");
var signature = signaturePresent
@@ -22,11 +25,11 @@ public sealed class AdvisoryRawWriteGuardTests
Signature: includeSignaturePayload ? "base64signature" : null)
: new RawSignatureMetadata(false);
return new AdvisoryRawDocument(
Tenant: tenant,
Source: new RawSourceMetadata("vendor-x", "connector-y", "1.0.0"),
Upstream: new RawUpstreamMetadata(
UpstreamId: "GHSA-xxxx",
return new AdvisoryRawDocument(
Tenant: tenant,
Source: new RawSourceMetadata("vendor-x", "connector-y", "1.0.0"),
Upstream: new RawUpstreamMetadata(
UpstreamId: "GHSA-xxxx",
DocumentVersion: "1",
RetrievedAt: DateTimeOffset.UtcNow,
ContentHash: "sha256:abc",
@@ -36,47 +39,51 @@ public sealed class AdvisoryRawWriteGuardTests
Format: "OSV",
SpecVersion: "1.0",
Raw: rawDocument.RootElement.Clone()),
Identifiers: new RawIdentifiers(
Aliases: ImmutableArray.Create("GHSA-xxxx"),
PrimaryId: "GHSA-xxxx"),
Linkset: new RawLinkset
{
Aliases = ImmutableArray<string>.Empty,
PackageUrls = ImmutableArray<string>.Empty,
Cpes = ImmutableArray<string>.Empty,
References = ImmutableArray<RawReference>.Empty,
ReconciledFrom = ImmutableArray<string>.Empty,
Notes = ImmutableDictionary<string, string>.Empty
});
}
Identifiers: new RawIdentifiers(
Aliases: ImmutableArray.Create("GHSA-xxxx"),
PrimaryId: "GHSA-xxxx"),
Linkset: new RawLinkset
{
Aliases = ImmutableArray<string>.Empty,
PackageUrls = ImmutableArray<string>.Empty,
Cpes = ImmutableArray<string>.Empty,
References = ImmutableArray<RawReference>.Empty,
ReconciledFrom = ImmutableArray<string>.Empty,
Notes = ImmutableDictionary<string, string>.Empty
},
Links: ImmutableArray<RawLink>.Empty);
}
private static AdvisoryRawWriteGuard CreateGuard()
=> new(new AocWriteGuard(), Options.Create(GuardOptions));
[Fact]
public void EnsureValid_AllowsMinimalDocument()
{
var guard = CreateGuard();
var document = CreateDocument();
guard.EnsureValid(document);
}
[Fact]
public void EnsureValid_AllowsMinimalDocument()
{
var guard = new AdvisoryRawWriteGuard(new AocWriteGuard());
var document = CreateDocument();
guard.EnsureValid(document);
}
[Fact]
public void EnsureValid_ThrowsWhenTenantMissing()
{
var guard = new AdvisoryRawWriteGuard(new AocWriteGuard());
var document = CreateDocument(tenant: string.Empty);
var exception = Assert.Throws<ConcelierAocGuardException>(() => guard.EnsureValid(document));
[Fact]
public void EnsureValid_ThrowsWhenTenantMissing()
{
var guard = CreateGuard();
var document = CreateDocument(tenant: string.Empty);
var exception = Assert.Throws<ConcelierAocGuardException>(() => guard.EnsureValid(document));
Assert.Equal("ERR_AOC_004", exception.PrimaryErrorCode);
Assert.Contains(exception.Violations, violation => violation.ErrorCode == "ERR_AOC_004" && violation.Path == "/tenant");
}
[Fact]
public void EnsureValid_ThrowsWhenSignaturePayloadMissing()
{
var guard = new AdvisoryRawWriteGuard(new AocWriteGuard());
var document = CreateDocument(signaturePresent: true, includeSignaturePayload: false);
var exception = Assert.Throws<ConcelierAocGuardException>(() => guard.EnsureValid(document));
[Fact]
public void EnsureValid_ThrowsWhenSignaturePayloadMissing()
{
var guard = CreateGuard();
var document = CreateDocument(signaturePresent: true, includeSignaturePayload: false);
var exception = Assert.Throws<ConcelierAocGuardException>(() => guard.EnsureValid(document));
Assert.Equal("ERR_AOC_005", exception.PrimaryErrorCode);
Assert.Contains(exception.Violations, violation => violation.ErrorCode == "ERR_AOC_005");
}

View File

@@ -53,11 +53,11 @@ public sealed class AdvisoryEventLogTests
}
[Fact]
public async Task AppendAsync_PersistsConflictsWithCanonicalizedJson()
{
var repository = new FakeRepository();
var timeProvider = new FixedTimeProvider(DateTimeOffset.Parse("2025-10-19T12:00:00Z"));
var log = new AdvisoryEventLog(repository, timeProvider);
public async Task AppendAsync_PersistsConflictsWithCanonicalizedJson()
{
var repository = new FakeRepository();
var timeProvider = new FixedTimeProvider(DateTimeOffset.Parse("2025-10-19T12:00:00Z"));
var log = new AdvisoryEventLog(repository, timeProvider);
using var conflictJson = JsonDocument.Parse("{\"reason\":\"tie\",\"details\":{\"b\":2,\"a\":1}}");
var conflictInput = new AdvisoryConflictInput(
@@ -73,13 +73,52 @@ public sealed class AdvisoryEventLogTests
Assert.Equal("cve-2025-0001", entry.VulnerabilityKey);
Assert.Equal("{\"details\":{\"a\":1,\"b\":2},\"reason\":\"tie\"}", entry.CanonicalJson);
Assert.NotEqual(ImmutableArray<byte>.Empty, entry.ConflictHash);
Assert.Equal(DateTimeOffset.Parse("2025-10-04T00:00:00Z"), entry.AsOf);
}
[Fact]
public async Task ReplayAsync_ReturnsSortedSnapshots()
{
var repository = new FakeRepository();
Assert.Equal(DateTimeOffset.Parse("2025-10-04T00:00:00Z"), entry.AsOf);
}
[Fact]
public async Task AppendAsync_SortsConflictStatementIds()
{
var repository = new FakeRepository();
var timeProvider = new FixedTimeProvider(DateTimeOffset.Parse("2025-10-20T12:00:00Z"));
var log = new AdvisoryEventLog(repository, timeProvider);
var unordered = new[]
{
Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
Guid.Empty,
Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"),
Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
};
using var conflictJson = JsonDocument.Parse("{\"reason\":\"severity-mismatch\"}");
var conflictInput = new AdvisoryConflictInput(
VulnerabilityKey: "CVE-2025-3000",
Details: conflictJson,
AsOf: DateTimeOffset.Parse("2025-10-20T00:00:00Z"),
StatementIds: unordered);
await log.AppendAsync(
new AdvisoryEventAppendRequest(Array.Empty<AdvisoryStatementInput>(), new[] { conflictInput }),
CancellationToken.None);
var entry = Assert.Single(repository.InsertedConflicts);
Assert.Equal(
new[]
{
Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc")
},
entry.StatementIds);
Assert.Equal("{\"reason\":\"severity-mismatch\"}", entry.CanonicalJson);
}
[Fact]
public async Task ReplayAsync_ReturnsSortedSnapshots()
{
var repository = new FakeRepository();
var timeProvider = new FixedTimeProvider(DateTimeOffset.Parse("2025-10-05T00:00:00Z"));
var log = new AdvisoryEventLog(repository, timeProvider);

View File

@@ -122,19 +122,19 @@ public sealed class AdvisoryObservationFactoryTests
var factory = new AdvisoryObservationFactory();
var notes = ImmutableDictionary.CreateRange(new Dictionary<string, string>
{
["range-introduced"] = "1.0.0",
["range-fixed"] = "1.0.5"
});
var rawDocument = BuildRawDocument(
identifiers: new RawIdentifiers(ImmutableArray<string>.Empty, "primary"),
linkset: new RawLinkset
{
Notes = notes,
ReconciledFrom = ImmutableArray.Create("connector-a", "connector-b")
},
supersedes: "tenant-a:vendor-x:previous:sha256:123");
["range-introduced"] = "1.0.0",
["range-fixed"] = "1.0.5"
});
var rawDocument = BuildRawDocument(
identifiers: new RawIdentifiers(ImmutableArray<string>.Empty, "primary"),
linkset: new RawLinkset
{
Notes = notes,
ReconciledFrom = ImmutableArray.Create("connector-a", "connector-b")
},
supersedes: "tenant-a:vendor-x:previous:sha256:123");
var observation = factory.Create(rawDocument);
Assert.Equal("1.0.0", observation.Attributes["linkset.note.range-introduced"]);
@@ -145,6 +145,65 @@ public sealed class AdvisoryObservationFactoryTests
Assert.Equal(new[] { "connector-a", "connector-b" }, observation.RawLinkset.ReconciledFrom);
}
[Fact]
public void Create_PreservesRawReferencesForConflictAudits()
{
var factory = new AdvisoryObservationFactory();
var references = ImmutableArray.Create(
new RawReference(" ADVISORY ", " https://example.test/advisory ", "vendor-feed"),
new RawReference("fix", "https://example.test/fix ", "vendor-feed"));
var notes = ImmutableDictionary.CreateRange(new Dictionary<string, string>
{
["conflict.primary"] = "critical",
["conflict.suppressed"] = "medium"
});
var rawDocument = BuildRawDocument(
identifiers: new RawIdentifiers(
Aliases: ImmutableArray.Create("CVE-2025-2000"),
PrimaryId: "VENDOR-2000"),
linkset: new RawLinkset
{
References = references,
Notes = notes,
ReconciledFrom = ImmutableArray.Create("/content/raw/severity", "/content/raw/status")
});
var observation = factory.Create(rawDocument, SampleTimestamp);
Assert.Collection(
observation.Linkset.References,
first =>
{
Assert.Equal("ADVISORY", first.Type);
Assert.Equal("https://example.test/advisory", first.Url);
},
second =>
{
Assert.Equal("fix", second.Type);
Assert.Equal("https://example.test/fix", second.Url);
});
Assert.Collection(
observation.RawLinkset.References,
first =>
{
Assert.Equal(" ADVISORY ", first.Type);
Assert.Equal(" https://example.test/advisory ", first.Url);
Assert.Equal("vendor-feed", first.Source);
},
second =>
{
Assert.Equal("fix", second.Type);
Assert.Equal("https://example.test/fix ", second.Url);
Assert.Equal("vendor-feed", second.Source);
});
Assert.Equal("critical", observation.Attributes["linkset.note.conflict.primary"]);
Assert.Equal("medium", observation.Attributes["linkset.note.conflict.suppressed"]);
Assert.Equal("/content/raw/severity;/content/raw/status", observation.Attributes["linkset.reconciled_from"]);
}
[Fact]
public void Create_IsDeterministicAcrossRuns()
{

View File

@@ -1,30 +1,34 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Aoc;
using StellaOps.Concelier.Core.Aoc;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.RawModels;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Raw;
public sealed class AdvisoryRawServiceTests
{
[Fact]
public async Task IngestAsync_RemovesClientSupersedesBeforeUpsert()
{
var repository = new RecordingRepository();
var service = CreateService(repository);
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Aoc;
using StellaOps.Concelier.Core.Aoc;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.RawModels;
using StellaOps.Ingestion.Telemetry;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Raw;
public sealed class AdvisoryRawServiceTests
{
private const string GhsaAlias = "GHSA-AAAA-BBBB-CCCC";
[Fact]
public async Task IngestAsync_RemovesClientSupersedesBeforeUpsert()
{
var repository = new RecordingRepository();
var service = CreateService(repository);
var document = CreateDocument() with { Supersedes = " previous-id " };
var storedDocument = document.WithSupersedes("advisory_raw:vendor-x:ghsa-xxxx:sha256-2");
var storedDocument = document.WithSupersedes("advisory_raw:vendor-x:ghsa-aaaa-bbbb-cccc:sha256-2");
var expectedResult = new AdvisoryRawUpsertResult(true, CreateRecord(storedDocument));
repository.NextResult = expectedResult;
@@ -33,12 +37,14 @@ public sealed class AdvisoryRawServiceTests
Assert.NotNull(repository.CapturedDocument);
Assert.Null(repository.CapturedDocument!.Supersedes);
Assert.Equal(expectedResult.Record.Document.Supersedes, result.Record.Document.Supersedes);
Assert.Equal("GHSA-XXXX", repository.CapturedDocument.AdvisoryKey);
Assert.Contains(repository.CapturedDocument.Links, link => link.Scheme == "GHSA" && link.Value == "GHSA-XXXX");
Assert.Contains(repository.CapturedDocument.Links, link => link.Scheme == "PRIMARY" && link.Value == "GHSA-XXXX");
}
[Fact]
Assert.Equal("GHSA-AAAA-BBBB-CCCC", repository.CapturedDocument.AdvisoryKey, ignoreCase: true);
Assert.Contains(repository.CapturedDocument.Links, link =>
string.Equals(link.Value, "GHSA-AAAA-BBBB-CCCC", StringComparison.OrdinalIgnoreCase));
Assert.Contains(repository.CapturedDocument.Links, link =>
link.Scheme == "PRIMARY" && string.Equals(link.Value, "GHSA-AAAA-BBBB-CCCC", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public async Task IngestAsync_PropagatesRepositoryDuplicateResult()
{
var repository = new RecordingRepository();
@@ -54,16 +60,66 @@ public sealed class AdvisoryRawServiceTests
Assert.Same(expectedResult.Record, result.Record);
}
[Fact]
public async Task IngestAsync_EmitsWriteMetric()
{
var repository = new RecordingRepository();
repository.NextResult = new AdvisoryRawUpsertResult(true, CreateRecord(CreateDocument()));
var service = CreateService(repository);
var measurements = await CollectCounterMeasurementsAsync(
"ingestion_write_total",
() => service.IngestAsync(CreateDocument(), CancellationToken.None));
Assert.Contains(
measurements,
tags => string.Equals(GetTagValue(tags, "tenant") as string, "tenant-a", StringComparison.OrdinalIgnoreCase)
&& string.Equals(GetTagValue(tags, "result") as string, IngestionTelemetry.ResultOk, StringComparison.OrdinalIgnoreCase));
}
[Fact]
public async Task IngestAsync_EmitsViolationMetricWhenGuardFails()
{
var repository = new RecordingRepository();
var service = CreateService(repository, new ThrowingWriteGuard());
var violationMeasurements = await CollectCounterMeasurementsAsync(
"aoc_violation_total",
async () =>
{
await Assert.ThrowsAsync<ConcelierAocGuardException>(
() => service.IngestAsync(CreateDocument(), CancellationToken.None));
});
Assert.Contains(
violationMeasurements,
tags => string.Equals(GetTagValue(tags, "tenant") as string, "tenant-a", StringComparison.OrdinalIgnoreCase)
&& string.Equals(GetTagValue(tags, "code") as string, "ERR_AOC_001", StringComparison.OrdinalIgnoreCase));
var writeMeasurements = await CollectCounterMeasurementsAsync(
"ingestion_write_total",
async () =>
{
await Assert.ThrowsAsync<ConcelierAocGuardException>(
() => service.IngestAsync(CreateDocument(), CancellationToken.None));
});
Assert.Contains(
writeMeasurements,
tags => string.Equals(GetTagValue(tags, "tenant") as string, "tenant-a", StringComparison.OrdinalIgnoreCase)
&& string.Equals(GetTagValue(tags, "result") as string, IngestionTelemetry.ResultReject, StringComparison.OrdinalIgnoreCase));
}
[Fact]
public async Task IngestAsync_PreservesAliasOrderAndDuplicates()
{
var repository = new RecordingRepository();
var service = CreateService(repository);
var aliasSeries = ImmutableArray.Create("CVE-2025-0001", "CVE-2025-0001", "GHSA-xxxx", "cve-2025-0001");
var aliasSeries = ImmutableArray.Create("CVE-2025-0001", "CVE-2025-0001", GhsaAlias, "cve-2025-0001");
var document = CreateDocument() with
{
Identifiers = new RawIdentifiers(aliasSeries, "GHSA-xxxx"),
Identifiers = new RawIdentifiers(aliasSeries, GhsaAlias),
};
repository.NextResult = new AdvisoryRawUpsertResult(true, CreateRecord(document));
@@ -74,7 +130,8 @@ public sealed class AdvisoryRawServiceTests
Assert.True(aliasSeries.SequenceEqual(repository.CapturedDocument!.Identifiers.Aliases));
Assert.Equal("CVE-2025-0001", repository.CapturedDocument.AdvisoryKey);
Assert.Contains(repository.CapturedDocument.Links, link => link.Scheme == "CVE" && link.Value == "CVE-2025-0001");
Assert.Contains(repository.CapturedDocument.Links, link => link.Scheme == "GHSA" && link.Value == "GHSA-XXXX");
Assert.Contains(repository.CapturedDocument.Links, link =>
string.Equals(link.Value, "GHSA-AAAA-BBBB-CCCC", StringComparison.OrdinalIgnoreCase));
}
[Fact]
@@ -88,38 +145,42 @@ public sealed class AdvisoryRawServiceTests
var results = await service.FindByAdvisoryKeyAsync(
"Tenant-Example",
"ghsa-xxxx",
"ghsa-aaaa-bbbb-cccc",
new[] { "Vendor-X", " " },
CancellationToken.None);
Assert.Single(results);
Assert.Equal("tenant-example", repository.CapturedTenant);
Assert.Contains("GHSA-XXXX", repository.CapturedAdvisoryKeySearchValues!, StringComparer.Ordinal);
Assert.Contains("ghsa-xxxx", repository.CapturedAdvisoryKeySearchValues!, StringComparer.Ordinal);
Assert.Contains("GHSA-AAAA-BBBB-CCCC", repository.CapturedAdvisoryKeySearchValues!, StringComparer.OrdinalIgnoreCase);
Assert.Contains("ghsa-aaaa-bbbb-cccc", repository.CapturedAdvisoryKeySearchValues!, StringComparer.Ordinal);
Assert.Contains("vendor-x", repository.CapturedAdvisoryKeyVendors!, StringComparer.Ordinal);
}
private static AdvisoryRawService CreateService(RecordingRepository repository)
{
var writeGuard = new AdvisoryRawWriteGuard(new AocWriteGuard());
var linksetMapper = new PassthroughLinksetMapper();
return new AdvisoryRawService(
repository,
writeGuard,
new AocWriteGuard(),
linksetMapper,
TimeProvider.System,
NullLogger<AdvisoryRawService>.Instance);
}
private static AdvisoryRawDocument CreateDocument()
{
using var raw = JsonDocument.Parse("""{"id":"demo"}""");
private static AdvisoryRawService CreateService(
RecordingRepository repository,
IAdvisoryRawWriteGuard? writeGuard = null,
IAocGuard? aocGuard = null)
{
var guard = aocGuard ?? new AocWriteGuard();
var resolvedWriteGuard = writeGuard ?? new NoOpWriteGuard();
var linksetMapper = new PassthroughLinksetMapper();
return new AdvisoryRawService(
repository,
resolvedWriteGuard,
guard,
linksetMapper,
TimeProvider.System,
NullLogger<AdvisoryRawService>.Instance);
}
private static AdvisoryRawDocument CreateDocument()
{
using var raw = JsonDocument.Parse("""{"id":"demo"}""");
return new AdvisoryRawDocument(
Tenant: "Tenant-A",
Source: new RawSourceMetadata("Vendor-X", "connector-y", "1.0.0"),
Upstream: new RawUpstreamMetadata(
UpstreamId: "GHSA-xxxx",
UpstreamId: GhsaAlias,
DocumentVersion: "1",
RetrievedAt: DateTimeOffset.UtcNow,
ContentHash: "sha256:abc",
@@ -134,8 +195,8 @@ public sealed class AdvisoryRawServiceTests
SpecVersion: "1.0",
Raw: raw.RootElement.Clone()),
Identifiers: new RawIdentifiers(
Aliases: ImmutableArray.Create("GHSA-xxxx"),
PrimaryId: "GHSA-xxxx"),
Aliases: ImmutableArray.Create(GhsaAlias),
PrimaryId: GhsaAlias),
Linkset: new RawLinkset
{
Aliases = ImmutableArray<string>.Empty,
@@ -148,7 +209,7 @@ public sealed class AdvisoryRawServiceTests
AdvisoryKey: string.Empty,
Links: ImmutableArray<RawLink>.Empty);
}
private static AdvisoryRawRecord CreateRecord(AdvisoryRawDocument document)
{
var canonical = AdvisoryCanonicalizer.Canonicalize(document.Identifiers, document.Source, document.Upstream);
@@ -159,13 +220,13 @@ public sealed class AdvisoryRawServiceTests
};
return new AdvisoryRawRecord(
Id: "advisory_raw:vendor-x:ghsa-xxxx:sha256-1",
Id: "advisory_raw:vendor-x:ghsa-aaaa-bbbb-cccc:sha256-1",
Document: resolvedDocument,
IngestedAt: DateTimeOffset.UtcNow,
CreatedAt: document.Upstream.RetrievedAt);
}
private sealed class RecordingRepository : IAdvisoryRawRepository
private sealed class RecordingRepository : IAdvisoryRawRepository
{
public AdvisoryRawDocument? CapturedDocument { get; private set; }
@@ -184,15 +245,15 @@ public sealed class AdvisoryRawServiceTests
if (NextResult is null)
{
throw new InvalidOperationException("NextResult must be set before calling UpsertAsync.");
}
CapturedDocument = document;
return Task.FromResult(NextResult);
}
public Task<AdvisoryRawRecord?> FindByIdAsync(string tenant, string id, CancellationToken cancellationToken)
=> throw new NotSupportedException();
}
CapturedDocument = document;
return Task.FromResult(NextResult);
}
public Task<AdvisoryRawRecord?> FindByIdAsync(string tenant, string id, CancellationToken cancellationToken)
=> throw new NotSupportedException();
public Task<AdvisoryRawQueryResult> QueryAsync(AdvisoryRawQueryOptions options, CancellationToken cancellationToken)
=> throw new NotSupportedException();
@@ -213,12 +274,73 @@ public sealed class AdvisoryRawServiceTests
DateTimeOffset since,
DateTimeOffset until,
IReadOnlyCollection<string> sourceVendors,
CancellationToken cancellationToken)
=> throw new NotSupportedException();
}
private sealed class PassthroughLinksetMapper : IAdvisoryLinksetMapper
{
public RawLinkset Map(AdvisoryRawDocument document) => document.Linkset;
}
}
CancellationToken cancellationToken)
=> throw new NotSupportedException();
}
private static async Task<List<KeyValuePair<string, object?>[]>> CollectCounterMeasurementsAsync(
string instrumentName,
Func<Task> action)
{
var measurements = new List<KeyValuePair<string, object?>[]>();
using var listener = new MeterListener();
listener.InstrumentPublished += (instrument, meterListener) =>
{
if (instrument.Meter.Name == IngestionTelemetry.MeterName && instrument.Name == instrumentName)
{
meterListener.EnableMeasurementEvents(instrument);
}
};
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
if (instrument.Name == instrumentName)
{
measurements.Add(tags.ToArray());
}
});
listener.Start();
await action().ConfigureAwait(false);
return measurements;
}
private static object? GetTagValue(IEnumerable<KeyValuePair<string, object?>> tags, string key)
{
foreach (var tag in tags)
{
if (string.Equals(tag.Key, key, StringComparison.Ordinal))
{
return tag.Value;
}
}
return null;
}
private sealed class ThrowingWriteGuard : IAdvisoryRawWriteGuard
{
public void EnsureValid(AdvisoryRawDocument document)
{
var violation = AocViolation.Create(
AocViolationCode.ForbiddenField,
"/content/raw",
"Forbidden derived data detected");
var result = AocGuardResult.FromViolations(new[] { violation });
throw new ConcelierAocGuardException(result);
}
}
private sealed class NoOpWriteGuard : IAdvisoryRawWriteGuard
{
public void EnsureValid(AdvisoryRawDocument document)
{
// Intentionally left blank for tests.
}
}
private sealed class PassthroughLinksetMapper : IAdvisoryLinksetMapper
{
public RawLinkset Map(AdvisoryRawDocument document) => document.Linkset;
}
}

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.RawModels/StellaOps.Concelier.RawModels.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj" />
<ProjectReference Include="../../../Aoc/__Libraries/StellaOps.Aoc/StellaOps.Aoc.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,14 +1,15 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Concelier.Exporter.Json;
using StellaOps.Concelier.Models;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Concelier.Exporter.Json;
using StellaOps.Concelier.Models;
using StellaOps.Cryptography;
namespace StellaOps.Concelier.Exporter.Json.Tests;
@@ -82,26 +83,52 @@ public sealed class JsonExportSnapshotBuilderTests : IDisposable
}
[Fact]
public async Task WriteAsync_NormalizesInputOrdering()
{
var options = new JsonExportOptions { OutputRoot = _root };
var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
var exportedAt = DateTimeOffset.Parse("2024-06-01T00:00:00Z", CultureInfo.InvariantCulture);
public async Task WriteAsync_NormalizesInputOrdering()
{
var options = new JsonExportOptions { OutputRoot = _root };
var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
var exportedAt = DateTimeOffset.Parse("2024-06-01T00:00:00Z", CultureInfo.InvariantCulture);
var advisoryA = CreateAdvisory("CVE-2024-1000", new[] { "CVE-2024-1000" }, "Alpha", "high");
var advisoryB = CreateAdvisory("VENDOR-0001", new[] { "VENDOR-0001" }, "Vendor Advisory", "medium");
var result = await builder.WriteAsync(new[] { advisoryB, advisoryA }, exportedAt, cancellationToken: CancellationToken.None);
var expectedOrder = result.FilePaths.OrderBy(path => path, StringComparer.Ordinal).ToArray();
Assert.Equal(expectedOrder, result.FilePaths.ToArray());
}
[Fact]
public async Task WriteAsync_EnumeratesStreamOnlyOnce()
{
var options = new JsonExportOptions { OutputRoot = _root };
var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
var expectedOrder = result.FilePaths.OrderBy(path => path, StringComparer.Ordinal).ToArray();
Assert.Equal(expectedOrder, result.FilePaths.ToArray());
}
[Fact]
public async Task WriteAsync_DifferentInputOrderProducesSameDigest()
{
var options = new JsonExportOptions { OutputRoot = _root };
var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
var exportedAt = DateTimeOffset.Parse("2024-06-15T00:00:00Z", CultureInfo.InvariantCulture);
var advisoryA = CreateAdvisory("CVE-2024-1100", new[] { "CVE-2024-1100" }, "Alpha", "critical");
var advisoryB = CreateAdvisory("VENDOR-2024-42", new[] { "VENDOR-2024-42" }, "Vendor", "medium");
var first = await builder.WriteAsync(
new[] { advisoryA, advisoryB },
exportedAt,
exportName: "order-a",
cancellationToken: CancellationToken.None);
var second = await builder.WriteAsync(
new[] { advisoryB, advisoryA },
exportedAt,
exportName: "order-b",
cancellationToken: CancellationToken.None);
Assert.Equal(
Convert.ToHexString(ComputeDigest(first)),
Convert.ToHexString(ComputeDigest(second)));
}
[Fact]
public async Task WriteAsync_EnumeratesStreamOnlyOnce()
{
var options = new JsonExportOptions { OutputRoot = _root };
var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
var exportedAt = DateTimeOffset.Parse("2024-08-01T00:00:00Z", CultureInfo.InvariantCulture);
var advisories = new[]
@@ -150,19 +177,20 @@ public sealed class JsonExportSnapshotBuilderTests : IDisposable
});
}
private static byte[] ComputeDigest(JsonExportResult result)
{
using var sha256 = SHA256.Create();
foreach (var relative in result.FilePaths.OrderBy(x => x, StringComparer.Ordinal))
{
var fullPath = ResolvePath(result.ExportDirectory, relative);
var bytes = File.ReadAllBytes(fullPath);
sha256.TransformBlock(bytes, 0, bytes.Length, null, 0);
}
sha256.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
return sha256.Hash ?? Array.Empty<byte>();
}
private static byte[] ComputeDigest(JsonExportResult result)
{
var hash = CryptoHashFactory.CreateDefault();
var buffer = new ArrayBufferWriter<byte>();
foreach (var relative in result.FilePaths.OrderBy(x => x, StringComparer.Ordinal))
{
var fullPath = ResolvePath(result.ExportDirectory, relative);
var bytes = File.ReadAllBytes(fullPath);
buffer.Write(bytes);
}
return hash.ComputeHash(buffer.WrittenSpan, HashAlgorithms.Sha256);
}
private static string ResolvePath(string root, string relative)
{

View File

@@ -13,6 +13,8 @@ using StellaOps.Concelier.Exporter.Json;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Exporting;
using StellaOps.Concelier.Models;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
namespace StellaOps.Concelier.Exporter.Json.Tests;
@@ -26,7 +28,10 @@ public sealed class JsonExporterDependencyInjectionRoutineTests
services.AddSingleton<IAdvisoryStore, StubAdvisoryStore>();
services.AddSingleton<IExportStateStore, StubExportStateStore>();
services.AddSingleton<IAdvisoryEventLog, StubAdvisoryEventLog>();
services.AddOptions();
services.AddOptions<JobSchedulerOptions>();
services.Configure<CryptoHashOptions>(_ => { });
services.AddStellaOpsCrypto();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())

View File

@@ -10,16 +10,17 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using StellaOps.Concelier.Core.Events;
using StellaOps.Concelier.Exporter.Json;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Exporting;
using StellaOps.Cryptography;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using StellaOps.Concelier.Core.Events;
using StellaOps.Concelier.Exporter.Json;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Exporting;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
namespace StellaOps.Concelier.Exporter.Json.Tests;
@@ -70,7 +71,7 @@ public sealed class JsonFeedExporterTests : IDisposable
NullLogger<JsonFeedExporter>.Instance,
timeProvider);
using var provider = new ServiceCollection().BuildServiceProvider();
using var provider = CreateCryptoProvider();
await exporter.ExportAsync(provider, CancellationToken.None);
var record = await stateStore.FindAsync(JsonFeedExporter.ExporterId, CancellationToken.None);
@@ -164,7 +165,7 @@ public sealed class JsonFeedExporterTests : IDisposable
NullLogger<JsonFeedExporter>.Instance,
timeProvider);
using var provider = new ServiceCollection().BuildServiceProvider();
using var provider = CreateCryptoProvider();
await exporter.ExportAsync(provider, CancellationToken.None);
var exportId = exportedAt.ToString(optionsValue.DirectoryNameFormat, CultureInfo.InvariantCulture);
@@ -322,16 +323,7 @@ public sealed class JsonFeedExporterTests : IDisposable
NullLogger<JsonFeedExporter>.Instance,
timeProvider);
var services = new ServiceCollection();
services.AddSingleton<DefaultCryptoProvider>();
services.AddSingleton<ICryptoProvider>(sp => sp.GetRequiredService<DefaultCryptoProvider>());
services.AddSingleton<ICryptoProviderRegistry>(sp =>
{
var provider = sp.GetRequiredService<DefaultCryptoProvider>();
return new CryptoProviderRegistry(new[] { provider });
});
using var provider = services.BuildServiceProvider();
using var provider = CreateCryptoProvider();
await exporter.ExportAsync(provider, CancellationToken.None);
var exportId = exportedAt.ToString(optionsValue.DirectoryNameFormat, CultureInfo.InvariantCulture);
@@ -449,7 +441,7 @@ public sealed class JsonFeedExporterTests : IDisposable
return $"-----BEGIN {label}-----\n{base64}\n-----END {label}-----\n";
}
private static byte[] BuildSigningInput(string protectedHeader, byte[] payload)
private static byte[] BuildSigningInput(string protectedHeader, byte[] payload)
{
var headerBytes = Encoding.ASCII.GetBytes(protectedHeader);
var buffer = new byte[headerBytes.Length + 1 + payload.Length];
@@ -459,9 +451,9 @@ public sealed class JsonFeedExporterTests : IDisposable
return buffer;
}
private static byte[] Base64UrlDecode(string value)
{
var builder = new StringBuilder(value.Length + 3);
private static byte[] Base64UrlDecode(string value)
{
var builder = new StringBuilder(value.Length + 3);
foreach (var ch in value)
{
builder.Append(ch switch
@@ -475,10 +467,19 @@ public sealed class JsonFeedExporterTests : IDisposable
while (builder.Length % 4 != 0)
{
builder.Append('=');
}
return Convert.FromBase64String(builder.ToString());
}
}
return Convert.FromBase64String(builder.ToString());
}
private static ServiceProvider CreateCryptoProvider()
{
var services = new ServiceCollection();
services.AddOptions();
services.Configure<CryptoHashOptions>(_ => { });
services.AddStellaOpsCrypto();
return services.BuildServiceProvider();
}
private sealed class StubAdvisoryStore : IAdvisoryStore
{
@@ -594,4 +595,4 @@ public sealed class JsonFeedExporterTests : IDisposable
public void Advance(TimeSpan delta) => _now = _now.Add(delta);
}
}
}

View File

@@ -10,5 +10,6 @@
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Exporter.Json/StellaOps.Concelier.Exporter.Json.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography.DependencyInjection/StellaOps.Cryptography.DependencyInjection.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -67,6 +67,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
{
_runner = MongoDbRunner.Start(singleNodeReplSet: true);
_factory = new ConcelierApplicationFactory(_runner.ConnectionString);
WarmupFactory(_factory);
return Task.CompletedTask;
}
@@ -670,7 +671,10 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-violation");
var invalidRequest = BuildAdvisoryIngestRequest(contentHash: string.Empty, upstreamId: "GHSA-INVALID-1");
var invalidRequest = BuildAdvisoryIngestRequest(
contentHash: string.Empty,
upstreamId: "GHSA-INVALID-1",
enforceContentHash: false);
var response = await client.PostAsJsonAsync("/ingest/advisory", invalidRequest);
Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode);
@@ -1361,10 +1365,22 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
var snapshot = documents?.ToArray() ?? Array.Empty<AdvisoryObservationDocument>();
if (snapshot.Length == 0)
{
await collection.InsertManyAsync(snapshot);
return;
}
await collection.InsertManyAsync(snapshot);
var rawDocuments = snapshot
.Select(doc => CreateAdvisoryRawDocument(
doc.Tenant,
doc.Source.Vendor,
doc.Id,
doc.Upstream.ContentHash,
doc.Content.Raw.DeepClone().AsBsonDocument))
.ToArray();
await SeedAdvisoryRawDocumentsAsync(rawDocuments);
}
private static AdvisoryObservationDocument[] BuildSampleObservationDocuments()
@@ -1501,6 +1517,11 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
return value ?? string.Empty;
}
if (!string.IsNullOrWhiteSpace(value))
{
return value.Trim();
}
using var sha256 = SHA256.Create();
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(raw.GetRawText()));
return $"sha256:{Convert.ToHexString(bytes).ToLowerInvariant()}";
@@ -1973,10 +1994,18 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
}
}
private static AdvisoryIngestRequest BuildAdvisoryIngestRequest(string? contentHash, string upstreamId)
private static void WarmupFactory(WebApplicationFactory<Program> factory)
{
var normalizedContentHash = contentHash ?? ComputeDeterministicContentHash(upstreamId);
var raw = CreateJsonElement($@"{{""id"":""{upstreamId}"",""modified"":""{DateTime.UtcNow:O}""}}");
using var client = factory.CreateClient();
}
private static AdvisoryIngestRequest BuildAdvisoryIngestRequest(
string? contentHash,
string upstreamId,
bool enforceContentHash = true)
{
var raw = CreateJsonElement($@"{{""id"":""{upstreamId}"",""modified"":""{DefaultIngestTimestamp:O}""}}");
var normalizedContentHash = NormalizeContentHash(contentHash, raw, enforceContentHash);
var references = new[]
{
new AdvisoryLinksetReferenceRequest("advisory", $"https://example.test/advisories/{upstreamId}", null)