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
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:
@@ -24,7 +24,9 @@ public sealed record AocGuardOptions
|
||||
"createdAt",
|
||||
"created_at",
|
||||
"ingestedAt",
|
||||
"ingested_at"
|
||||
"ingested_at",
|
||||
"links",
|
||||
"advisory_key"
|
||||
}, StringComparer.OrdinalIgnoreCase)
|
||||
.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
||||
@@ -27,12 +27,42 @@ public sealed class AocWriteGuardTests
|
||||
}
|
||||
""");
|
||||
|
||||
var result = Guard.Validate(document.RootElement);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Empty(result.Violations);
|
||||
}
|
||||
|
||||
var result = Guard.Validate(document.RootElement);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Empty(result.Violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllowsLinksAndAdvisoryKey_ByDefault()
|
||||
{
|
||||
using var document = JsonDocument.Parse("""
|
||||
{
|
||||
"tenant": "default",
|
||||
"source": {"vendor": "osv"},
|
||||
"upstream": {
|
||||
"upstream_id": "GHSA-xxxx",
|
||||
"content_hash": "sha256:abc",
|
||||
"signature": { "present": false }
|
||||
},
|
||||
"content": {
|
||||
"format": "OSV",
|
||||
"raw": {"id": "GHSA-xxxx"}
|
||||
},
|
||||
"linkset": {},
|
||||
"links": [
|
||||
{ "scheme": "cve", "value": "CVE-2025-0001" }
|
||||
],
|
||||
"advisory_key": "ghsa-xxxx"
|
||||
}
|
||||
""");
|
||||
|
||||
var result = Guard.Validate(document.RootElement);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Empty(result.Violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_FlagsMissingTenant()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
> Remark (2025-10-19): Wave 0 prerequisites reviewed (none outstanding); ATTESTOR-API-11-201, ATTESTOR-VERIFY-11-202, and ATTESTOR-OBS-11-203 tracked as DOING per Wave 0A kickoff.
|
||||
> Remark (2025-10-19): Dual-log submissions, signature/proof verification, and observability hardening landed; attestor endpoints now rate-limited per client with correlation-ID logging and updated docs/tests.
|
||||
| ATTESTOR-CRYPTO-90-001 | TODO | Attestor Service Guild, Security Guild | SEC-CRYPTO-90-003, SEC-CRYPTO-90-004 | Migrate bundle hashing, witness proof caching, and signing submissions to `ICryptoProviderRegistry`/`ICryptoHash` so RootPack_RU deployments use CryptoPro or PKCS#11 per `docs/security/crypto-routing-audit-2025-11-07.md`. | Attestor services resolve registry providers; DSSE signing/verifying honors config profiles; tests cover default + `ru-offline` modes; docs updated. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SEC2.PLG | BLOCKED (2025-10-21) | Security Guild, Storage Guild | SEC2.A (audit contract) | Emit audit events from password verification outcomes and persist via `IAuthorityLoginAttemptStore`. <br>⛔ Waiting on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 / PLUGIN-DI-08-001 to stabilise Authority auth surfaces before final verification + publish. | ✅ Serilog events enriched with subject/client/IP/outcome; ✅ Mongo records written per attempt; ✅ Tests assert success/lockout/failure cases. |
|
||||
| SEC3.PLG | BLOCKED (2025-10-21) | Security Guild, BE-Auth Plugin | CORE8, SEC3.A (rate limiter) | Ensure lockout responses and rate-limit metadata flow through plugin logs/events (include retry-after). <br>⛔ Pending AUTH-DPOP-11-001 / AUTH-MTLS-11-002 / PLUGIN-DI-08-001 so limiter telemetry contract matches final authority surface. | ✅ Audit record includes retry-after; ✅ Tests confirm lockout + limiter interplay. |
|
||||
| SEC5.PLG | BLOCKED (2025-10-21) | Security Guild | SEC5.A (threat model) | Address plugin-specific mitigations (bootstrap user handling, password policy docs) in threat model backlog. <br>⛔ Final documentation depends on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 / PLUGIN-DI-08-001 outcomes. | ✅ Threat model lists plugin attack surfaces; ✅ Mitigation items filed. |
|
||||
| SEC2.PLG | BLOCKED (2025-10-21) | Security Guild, Storage Guild | SEC2.A (audit contract) | Emit audit events from password verification outcomes and persist via `IAuthorityLoginAttemptStore`. <br>⛔ Waiting on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 to stabilise Authority auth surfaces (PLUGIN-DI-08-001 landed 2025-10-21). | ✅ Serilog events enriched with subject/client/IP/outcome; ✅ Mongo records written per attempt; ✅ Tests assert success/lockout/failure cases. |
|
||||
| SEC3.PLG | BLOCKED (2025-10-21) | Security Guild, BE-Auth Plugin | CORE8, SEC3.A (rate limiter) | Ensure lockout responses and rate-limit metadata flow through plugin logs/events (include retry-after). <br>⛔ Pending AUTH-DPOP-11-001 / AUTH-MTLS-11-002; PLUGIN-DI-08-001 is done, limiter telemetry just awaits the updated Authority surface. | ✅ Audit record includes retry-after; ✅ Tests confirm lockout + limiter interplay. |
|
||||
| SEC5.PLG | BLOCKED (2025-10-21) | Security Guild | SEC5.A (threat model) | Address plugin-specific mitigations (bootstrap user handling, password policy docs) in threat model backlog. <br>⛔ Final documentation now hinges on AUTH-DPOP-11-001 / AUTH-MTLS-11-002; scoped DI work is complete. | ✅ Threat model lists plugin attack surfaces; ✅ Mitigation items filed. |
|
||||
| PLG4-6.CAPABILITIES | BLOCKED (2025-10-12) | BE-Auth Plugin, Docs Guild | PLG1–PLG3 | Finalise capability metadata exposure, config validation, and developer guide updates; remaining action is Docs polish/diagram export. | ✅ Capability metadata + validation merged; ✅ Plugin guide updated with final copy & diagrams; ✅ Release notes mention new toggles. <br>⛔ Blocked awaiting Authority rate-limiter stream (CORE8/SEC3) to resume so doc updates reflect final limiter behaviour. |
|
||||
| PLG7.RFC | DONE (2025-11-03) | BE-Auth Plugin, Security Guild | PLG4 | Socialize LDAP plugin RFC (`docs/rfcs/authority-plugin-ldap.md`) and capture guild feedback. | ✅ Guild review sign-off recorded; ✅ Follow-up issues filed in module boards. |
|
||||
| PLG7.IMPL-001 | DONE (2025-11-03) | BE-Auth Plugin | PLG7.RFC | Scaffold `StellaOps.Authority.Plugin.Ldap` + tests, bind configuration (client certificate, trust-store, insecure toggle) with validation and docs samples. | ✅ Project + test harness build; ✅ Configuration bound & validated; ✅ Sample config updated. |
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Authority.Plugins.Abstractions;
|
||||
|
||||
@@ -8,18 +11,55 @@ namespace StellaOps.Authority.Plugins.Abstractions;
|
||||
/// </summary>
|
||||
public static class AuthoritySecretHasher
|
||||
{
|
||||
private static ICryptoHash? configuredHash;
|
||||
private static string defaultAlgorithm = HashAlgorithms.Sha256;
|
||||
|
||||
/// <summary>
|
||||
/// Computes a stable SHA-256 hash for the provided secret.
|
||||
/// Configures the shared crypto hash service used for secret hashing.
|
||||
/// </summary>
|
||||
public static string ComputeHash(string secret)
|
||||
public static void Configure(ICryptoHash hash, string? algorithmId = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(hash);
|
||||
Volatile.Write(ref configuredHash, hash);
|
||||
if (!string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
defaultAlgorithm = NormalizeAlgorithm(algorithmId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a stable hash for the provided secret using the configured crypto provider.
|
||||
/// </summary>
|
||||
public static string ComputeHash(string secret, string? algorithmId = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(secret))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var algorithm = string.IsNullOrWhiteSpace(algorithmId)
|
||||
? defaultAlgorithm
|
||||
: NormalizeAlgorithm(algorithmId);
|
||||
|
||||
var hasher = Volatile.Read(ref configuredHash);
|
||||
if (hasher is not null)
|
||||
{
|
||||
var digest = hasher.ComputeHash(Encoding.UTF8.GetBytes(secret), algorithm);
|
||||
return Convert.ToBase64String(digest);
|
||||
}
|
||||
|
||||
if (!string.Equals(algorithm, HashAlgorithms.Sha256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("Authority secret hasher is not configured for the requested algorithm.");
|
||||
}
|
||||
|
||||
using var sha256 = SHA256.Create();
|
||||
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(secret));
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private static string NormalizeAlgorithm(string algorithmId)
|
||||
=> string.IsNullOrWhiteSpace(algorithmId)
|
||||
? HashAlgorithms.Sha256
|
||||
: algorithmId.Trim().ToUpperInvariant();
|
||||
}
|
||||
|
||||
@@ -74,6 +74,10 @@ public sealed class AuthorityTokenDocument
|
||||
[BsonIgnoreIfNull]
|
||||
public string? SenderKeyThumbprint { get; set; }
|
||||
|
||||
[BsonElement("senderCertificateHex")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? SenderCertificateHex { get; set; }
|
||||
|
||||
[BsonElement("senderNonce")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? SenderNonce { get; set; }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
@@ -155,8 +157,8 @@ public sealed class ConsoleEndpointsTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TokenIntrospect_FlagsInactive_WhenExpired()
|
||||
{
|
||||
public async Task TokenIntrospect_FlagsInactive_WhenExpired()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-10-31T12:00:00Z"));
|
||||
var sink = new RecordingAuthEventSink();
|
||||
await using var app = await CreateApplicationAsync(timeProvider, sink, new AuthorityTenantView("tenant-default", "Default", "active", "shared", Array.Empty<string>(), Array.Empty<string>()));
|
||||
@@ -189,8 +191,118 @@ public sealed class ConsoleEndpointsTests
|
||||
|
||||
var consoleEvent = Assert.Single(events, evt => evt.EventType == "authority.console.token.introspect");
|
||||
Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome);
|
||||
Assert.Equal(2, events.Count);
|
||||
}
|
||||
Assert.Equal(2, events.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VulnerabilityFindings_ReturnsSamplePayload()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-08T12:00:00Z"));
|
||||
var sink = new RecordingAuthEventSink();
|
||||
await using var app = await CreateApplicationAsync(timeProvider, sink, new AuthorityTenantView("tenant-default", "Default", "active", "shared", Array.Empty<string>(), Array.Empty<string>()));
|
||||
|
||||
var accessor = app.Services.GetRequiredService<TestPrincipalAccessor>();
|
||||
accessor.Principal = CreatePrincipal(
|
||||
tenant: "tenant-default",
|
||||
scopes: new[] { StellaOpsScopes.UiRead, StellaOpsScopes.AdvisoryRead, StellaOpsScopes.VexRead },
|
||||
expiresAt: timeProvider.GetUtcNow().AddMinutes(30));
|
||||
|
||||
var client = app.CreateTestClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthenticationDefaults.AuthenticationScheme);
|
||||
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
||||
|
||||
var response = await client.GetAsync("/console/vuln/findings?severity=high");
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
var items = json.RootElement.GetProperty("items");
|
||||
Assert.True(items.GetArrayLength() >= 1);
|
||||
Assert.Equal("CVE-2024-12345", items[0].GetProperty("coordinates").GetProperty("advisoryId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VulnerabilityFindingDetail_ReturnsExpandedDocument()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-08T12:00:00Z"));
|
||||
var sink = new RecordingAuthEventSink();
|
||||
await using var app = await CreateApplicationAsync(timeProvider, sink, new AuthorityTenantView("tenant-default", "Default", "active", "shared", Array.Empty<string>(), Array.Empty<string>()));
|
||||
|
||||
var accessor = app.Services.GetRequiredService<TestPrincipalAccessor>();
|
||||
accessor.Principal = CreatePrincipal(
|
||||
tenant: "tenant-default",
|
||||
scopes: new[] { StellaOpsScopes.UiRead, StellaOpsScopes.AdvisoryRead, StellaOpsScopes.VexRead },
|
||||
expiresAt: timeProvider.GetUtcNow().AddMinutes(30));
|
||||
|
||||
var client = app.CreateTestClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthenticationDefaults.AuthenticationScheme);
|
||||
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
||||
|
||||
var response = await client.GetAsync("/console/vuln/tenant-default:advisory-ai:sha256:5d1a");
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
var summary = json.RootElement.GetProperty("summary");
|
||||
Assert.Equal("tenant-default:advisory-ai:sha256:5d1a", summary.GetProperty("findingId").GetString());
|
||||
Assert.Equal("reachable", summary.GetProperty("reachability").GetProperty("status").GetString());
|
||||
var detailReachability = json.RootElement.GetProperty("reachability");
|
||||
Assert.Equal("reachable", detailReachability.GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VulnerabilityTicket_ReturnsDeterministicPayload()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-08T12:00:00Z"));
|
||||
var sink = new RecordingAuthEventSink();
|
||||
await using var app = await CreateApplicationAsync(timeProvider, sink, new AuthorityTenantView("tenant-default", "Default", "active", "shared", Array.Empty<string>(), Array.Empty<string>()));
|
||||
|
||||
var accessor = app.Services.GetRequiredService<TestPrincipalAccessor>();
|
||||
accessor.Principal = CreatePrincipal(
|
||||
tenant: "tenant-default",
|
||||
scopes: new[] { StellaOpsScopes.UiRead, StellaOpsScopes.AdvisoryRead, StellaOpsScopes.VexRead },
|
||||
expiresAt: timeProvider.GetUtcNow().AddMinutes(30));
|
||||
|
||||
var client = app.CreateTestClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthenticationDefaults.AuthenticationScheme);
|
||||
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
||||
|
||||
var payload = new ConsoleVulnerabilityTicketRequest(
|
||||
Selection: new[] { "tenant-default:advisory-ai:sha256:5d1a" },
|
||||
TargetSystem: "servicenow",
|
||||
Metadata: new Dictionary<string, string> { ["assignmentGroup"] = "runtime-security" });
|
||||
|
||||
var response = await client.PostAsJsonAsync("/console/vuln/tickets", payload);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
Assert.StartsWith("console-ticket::tenant-default::", json.RootElement.GetProperty("ticketId").GetString());
|
||||
Assert.Equal("servicenow", payload.TargetSystem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VexStatements_ReturnsSampleRows()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-08T12:00:00Z"));
|
||||
var sink = new RecordingAuthEventSink();
|
||||
await using var app = await CreateApplicationAsync(timeProvider, sink, new AuthorityTenantView("tenant-default", "Default", "active", "shared", Array.Empty<string>(), Array.Empty<string>()));
|
||||
|
||||
var accessor = app.Services.GetRequiredService<TestPrincipalAccessor>();
|
||||
accessor.Principal = CreatePrincipal(
|
||||
tenant: "tenant-default",
|
||||
scopes: new[] { StellaOpsScopes.UiRead, StellaOpsScopes.VexRead },
|
||||
expiresAt: timeProvider.GetUtcNow().AddMinutes(30));
|
||||
|
||||
var client = app.CreateTestClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthenticationDefaults.AuthenticationScheme);
|
||||
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
||||
|
||||
var response = await client.GetAsync("/console/vex/statements?advisoryId=CVE-2024-12345");
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
var items = json.RootElement.GetProperty("items");
|
||||
Assert.True(items.GetArrayLength() >= 1);
|
||||
Assert.Equal("CVE-2024-12345", items[0].GetProperty("advisoryId").GetString());
|
||||
}
|
||||
|
||||
private static ClaimsPrincipal CreatePrincipal(
|
||||
string tenant,
|
||||
@@ -259,9 +371,10 @@ public sealed class ConsoleEndpointsTests
|
||||
builder.Services.AddSingleton<TimeProvider>(timeProvider);
|
||||
builder.Services.AddSingleton<IAuthEventSink>(sink);
|
||||
builder.Services.AddSingleton<IAuthorityTenantCatalog>(new FakeTenantCatalog(tenants));
|
||||
builder.Services.AddSingleton<TestPrincipalAccessor>();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddSingleton<StellaOpsBypassEvaluator>();
|
||||
builder.Services.AddSingleton<TestPrincipalAccessor>();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddSingleton<StellaOpsBypassEvaluator>();
|
||||
builder.Services.AddSingleton<IConsoleWorkspaceService, ConsoleWorkspaceSampleService>();
|
||||
|
||||
var authBuilder = builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -177,16 +177,17 @@ public sealed class TokenPersistenceIntegrationTests
|
||||
var auditSink = new TestAuthEventSink();
|
||||
await using var scope = provider.CreateAsyncScope();
|
||||
var sessionAccessor = scope.ServiceProvider.GetRequiredService<IAuthorityMongoSessionAccessor>();
|
||||
var handler = new ValidateAccessTokenHandler(
|
||||
tokenStore,
|
||||
sessionAccessor,
|
||||
clientStore,
|
||||
registry,
|
||||
metadataAccessor,
|
||||
auditSink,
|
||||
clock,
|
||||
TestActivitySource,
|
||||
NullLogger<ValidateAccessTokenHandler>.Instance);
|
||||
var handler = new ValidateAccessTokenHandler(
|
||||
tokenStore,
|
||||
sessionAccessor,
|
||||
clientStore,
|
||||
registry,
|
||||
metadataAccessor,
|
||||
auditSink,
|
||||
clock,
|
||||
TestActivitySource,
|
||||
TestInstruments.Meter,
|
||||
NullLogger<ValidateAccessTokenHandler>.Instance);
|
||||
|
||||
var transaction = new OpenIddictServerTransaction
|
||||
{
|
||||
|
||||
@@ -30,8 +30,10 @@ public sealed class AuthorityJwksServiceTests
|
||||
var registry = new TestRegistry(provider);
|
||||
using var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-10-30T12:00:00Z"));
|
||||
var hash = CryptoHashFactory.CreateDefault();
|
||||
var service = new AuthorityJwksService(
|
||||
registry,
|
||||
hash,
|
||||
NullLogger<AuthorityJwksService>.Instance,
|
||||
cache,
|
||||
clock,
|
||||
@@ -64,8 +66,10 @@ public sealed class AuthorityJwksServiceTests
|
||||
var registry = new TestRegistry(provider);
|
||||
using var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-10-30T12:00:00Z"));
|
||||
var hash = CryptoHashFactory.CreateDefault();
|
||||
var service = new AuthorityJwksService(
|
||||
registry,
|
||||
hash,
|
||||
NullLogger<AuthorityJwksService>.Instance,
|
||||
cache,
|
||||
clock,
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class KmsAuthoritySigningKeySourceTests
|
||||
var signingKey = source.Load(request);
|
||||
|
||||
Assert.Equal(CryptoSigningKeyKind.Raw, signingKey.Kind);
|
||||
Assert.Equal(material.KeyId, signingKey.Reference.KeyId);
|
||||
Assert.Equal(request.KeyId, signingKey.Reference.KeyId);
|
||||
Assert.True(signingKey.PrivateKey.Length > 0);
|
||||
Assert.True(signingKey.PublicKey.Length > 0);
|
||||
Assert.Equal(material.VersionId, signingKey.Metadata[KmsAuthoritySigningKeySource.KmsMetadataKeys.Version]);
|
||||
|
||||
@@ -37,10 +37,41 @@ internal static class ConsoleEndpointExtensions
|
||||
.WithName("ConsoleProfile")
|
||||
.WithSummary("Return the authenticated principal profile metadata.");
|
||||
|
||||
group.MapPost("/token/introspect", IntrospectToken)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.UiRead))
|
||||
.WithName("ConsoleTokenIntrospect")
|
||||
.WithSummary("Introspect the current access token and return expiry, scope, and tenant metadata.");
|
||||
group.MapPost("/token/introspect", IntrospectToken)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.UiRead))
|
||||
.WithName("ConsoleTokenIntrospect")
|
||||
.WithSummary("Introspect the current access token and return expiry, scope, and tenant metadata.");
|
||||
|
||||
var vulnGroup = group.MapGroup("/vuln")
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(
|
||||
StellaOpsScopes.UiRead,
|
||||
StellaOpsScopes.AdvisoryRead,
|
||||
StellaOpsScopes.VexRead));
|
||||
|
||||
vulnGroup.MapGet("/findings", GetVulnerabilityFindings)
|
||||
.WithName("ConsoleVulnerabilityFindings")
|
||||
.WithSummary("List tenant-scoped vulnerability findings with policy/VEX metadata.");
|
||||
|
||||
vulnGroup.MapGet("/{findingId}", GetVulnerabilityFindingById)
|
||||
.WithName("ConsoleVulnerabilityFindingDetail")
|
||||
.WithSummary("Return the full finding document, including evidence and policy overlays.");
|
||||
|
||||
vulnGroup.MapPost("/tickets", CreateVulnerabilityTicket)
|
||||
.WithName("ConsoleVulnerabilityTickets")
|
||||
.WithSummary("Generate a signed payload payload for external ticketing workflows.");
|
||||
|
||||
var vexGroup = group.MapGroup("/vex")
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(
|
||||
StellaOpsScopes.UiRead,
|
||||
StellaOpsScopes.VexRead));
|
||||
|
||||
vexGroup.MapGet("/statements", GetVexStatements)
|
||||
.WithName("ConsoleVexStatements")
|
||||
.WithSummary("List VEX statements impacting the tenant.");
|
||||
|
||||
vexGroup.MapGet("/events", StreamVexEvents)
|
||||
.WithName("ConsoleVexEvents")
|
||||
.WithSummary("Server-sent events feed for live VEX updates (placeholder).");
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetTenants(
|
||||
@@ -134,11 +165,11 @@ internal static class ConsoleEndpointExtensions
|
||||
return Results.Ok(profile);
|
||||
}
|
||||
|
||||
private static async Task<IResult> IntrospectToken(
|
||||
HttpContext httpContext,
|
||||
TimeProvider timeProvider,
|
||||
IAuthEventSink auditSink,
|
||||
CancellationToken cancellationToken)
|
||||
private static async Task<IResult> IntrospectToken(
|
||||
HttpContext httpContext,
|
||||
TimeProvider timeProvider,
|
||||
IAuthEventSink auditSink,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(httpContext);
|
||||
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||
@@ -152,21 +183,214 @@ internal static class ConsoleEndpointExtensions
|
||||
|
||||
var introspection = BuildTokenIntrospection(principal, timeProvider);
|
||||
|
||||
await WriteAuditAsync(
|
||||
httpContext,
|
||||
auditSink,
|
||||
timeProvider,
|
||||
"authority.console.token.introspect",
|
||||
AuthEventOutcome.Success,
|
||||
null,
|
||||
BuildProperties(
|
||||
("token.active", introspection.Active ? "true" : "false"),
|
||||
("token.expires_at", FormatInstant(introspection.ExpiresAt)),
|
||||
("tenant.resolved", introspection.Tenant)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(introspection);
|
||||
}
|
||||
await WriteAuditAsync(
|
||||
httpContext,
|
||||
auditSink,
|
||||
timeProvider,
|
||||
"authority.console.token.introspect",
|
||||
AuthEventOutcome.Success,
|
||||
null,
|
||||
BuildProperties(
|
||||
("token.active", introspection.Active ? "true" : "false"),
|
||||
("token.expires_at", FormatInstant(introspection.ExpiresAt)),
|
||||
("tenant.resolved", introspection.Tenant)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(introspection);
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetVulnerabilityFindings(
|
||||
HttpContext httpContext,
|
||||
IConsoleWorkspaceService workspaceService,
|
||||
TimeProvider timeProvider,
|
||||
IAuthEventSink auditSink,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(httpContext);
|
||||
ArgumentNullException.ThrowIfNull(workspaceService);
|
||||
|
||||
var tenant = TenantHeaderFilter.GetTenant(httpContext);
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
await WriteAuditAsync(
|
||||
httpContext,
|
||||
auditSink,
|
||||
timeProvider,
|
||||
"authority.console.vuln.findings",
|
||||
AuthEventOutcome.Failure,
|
||||
"tenant_header_missing",
|
||||
BuildProperties(("tenant.header", null)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.BadRequest(new { error = "tenant_header_missing", message = $"Header '{AuthorityHttpHeaders.Tenant}' is required." });
|
||||
}
|
||||
|
||||
var query = BuildVulnerabilityQuery(httpContext.Request);
|
||||
var response = await workspaceService.SearchFindingsAsync(tenant, query, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await WriteAuditAsync(
|
||||
httpContext,
|
||||
auditSink,
|
||||
timeProvider,
|
||||
"authority.console.vuln.findings",
|
||||
AuthEventOutcome.Success,
|
||||
null,
|
||||
BuildProperties(("tenant.resolved", tenant), ("pagination.next_token", response.NextPageToken)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(response);
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetVulnerabilityFindingById(
|
||||
HttpContext httpContext,
|
||||
string findingId,
|
||||
IConsoleWorkspaceService workspaceService,
|
||||
TimeProvider timeProvider,
|
||||
IAuthEventSink auditSink,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(httpContext);
|
||||
ArgumentNullException.ThrowIfNull(workspaceService);
|
||||
|
||||
var tenant = TenantHeaderFilter.GetTenant(httpContext);
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
await WriteAuditAsync(
|
||||
httpContext,
|
||||
auditSink,
|
||||
timeProvider,
|
||||
"authority.console.vuln.finding",
|
||||
AuthEventOutcome.Failure,
|
||||
"tenant_header_missing",
|
||||
BuildProperties(("tenant.header", null)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.BadRequest(new { error = "tenant_header_missing", message = $"Header '{AuthorityHttpHeaders.Tenant}' is required." });
|
||||
}
|
||||
|
||||
var detail = await workspaceService.GetFindingAsync(tenant, findingId, cancellationToken).ConfigureAwait(false);
|
||||
if (detail is null)
|
||||
{
|
||||
await WriteAuditAsync(
|
||||
httpContext,
|
||||
auditSink,
|
||||
timeProvider,
|
||||
"authority.console.vuln.finding",
|
||||
AuthEventOutcome.Failure,
|
||||
"finding_not_found",
|
||||
BuildProperties(("tenant.resolved", tenant), ("finding.id", findingId)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.NotFound(new { error = "finding_not_found", message = $"Finding '{findingId}' not found." });
|
||||
}
|
||||
|
||||
await WriteAuditAsync(
|
||||
httpContext,
|
||||
auditSink,
|
||||
timeProvider,
|
||||
"authority.console.vuln.finding",
|
||||
AuthEventOutcome.Success,
|
||||
null,
|
||||
BuildProperties(("tenant.resolved", tenant), ("finding.id", findingId)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(detail);
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateVulnerabilityTicket(
|
||||
HttpContext httpContext,
|
||||
ConsoleVulnerabilityTicketRequest request,
|
||||
IConsoleWorkspaceService workspaceService,
|
||||
TimeProvider timeProvider,
|
||||
IAuthEventSink auditSink,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(httpContext);
|
||||
ArgumentNullException.ThrowIfNull(workspaceService);
|
||||
|
||||
if (request is null || request.Selection.Count == 0)
|
||||
{
|
||||
return Results.BadRequest(new { error = "invalid_request", message = "At least one finding must be selected." });
|
||||
}
|
||||
|
||||
var tenant = TenantHeaderFilter.GetTenant(httpContext);
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
await WriteAuditAsync(
|
||||
httpContext,
|
||||
auditSink,
|
||||
timeProvider,
|
||||
"authority.console.vuln.ticket",
|
||||
AuthEventOutcome.Failure,
|
||||
"tenant_header_missing",
|
||||
BuildProperties(("tenant.header", null)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.BadRequest(new { error = "tenant_header_missing", message = $"Header '{AuthorityHttpHeaders.Tenant}' is required." });
|
||||
}
|
||||
|
||||
var ticket = await workspaceService.CreateTicketAsync(tenant, request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await WriteAuditAsync(
|
||||
httpContext,
|
||||
auditSink,
|
||||
timeProvider,
|
||||
"authority.console.vuln.ticket",
|
||||
AuthEventOutcome.Success,
|
||||
null,
|
||||
BuildProperties(
|
||||
("tenant.resolved", tenant),
|
||||
("ticket.id", ticket.TicketId),
|
||||
("ticket.selection.count", request.Selection.Count.ToString(CultureInfo.InvariantCulture))),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(ticket);
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetVexStatements(
|
||||
HttpContext httpContext,
|
||||
IConsoleWorkspaceService workspaceService,
|
||||
TimeProvider timeProvider,
|
||||
IAuthEventSink auditSink,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(httpContext);
|
||||
ArgumentNullException.ThrowIfNull(workspaceService);
|
||||
|
||||
var tenant = TenantHeaderFilter.GetTenant(httpContext);
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
await WriteAuditAsync(
|
||||
httpContext,
|
||||
auditSink,
|
||||
timeProvider,
|
||||
"authority.console.vex.statements",
|
||||
AuthEventOutcome.Failure,
|
||||
"tenant_header_missing",
|
||||
BuildProperties(("tenant.header", null)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.BadRequest(new { error = "tenant_header_missing", message = $"Header '{AuthorityHttpHeaders.Tenant}' is required." });
|
||||
}
|
||||
|
||||
var query = BuildVexQuery(httpContext.Request);
|
||||
var response = await workspaceService.GetVexStatementsAsync(tenant, query, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await WriteAuditAsync(
|
||||
httpContext,
|
||||
auditSink,
|
||||
timeProvider,
|
||||
"authority.console.vex.statements",
|
||||
AuthEventOutcome.Success,
|
||||
null,
|
||||
BuildProperties(("tenant.resolved", tenant), ("pagination.next_token", response.NextPageToken)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(response);
|
||||
}
|
||||
|
||||
private static IResult StreamVexEvents() =>
|
||||
Results.StatusCode(StatusCodes.Status501NotImplemented);
|
||||
|
||||
private static ConsoleProfileResponse BuildProfile(ClaimsPrincipal principal, TimeProvider timeProvider)
|
||||
{
|
||||
@@ -231,9 +455,9 @@ internal static class ConsoleEndpointExtensions
|
||||
FreshAuth: freshAuth);
|
||||
}
|
||||
|
||||
private static bool DetermineFreshAuth(ClaimsPrincipal principal, DateTimeOffset now)
|
||||
{
|
||||
var flag = principal.FindFirst("stellaops:fresh_auth") ?? principal.FindFirst("fresh_auth");
|
||||
private static bool DetermineFreshAuth(ClaimsPrincipal principal, DateTimeOffset now)
|
||||
{
|
||||
var flag = principal.FindFirst("stellaops:fresh_auth") ?? principal.FindFirst("fresh_auth");
|
||||
if (flag is not null && bool.TryParse(flag.Value, out var freshFlag))
|
||||
{
|
||||
if (freshFlag)
|
||||
@@ -254,9 +478,67 @@ internal static class ConsoleEndpointExtensions
|
||||
return authTime.Value.Add(ttl) > now;
|
||||
}
|
||||
|
||||
const int defaultFreshAuthWindowSeconds = 300;
|
||||
return authTime.Value.AddSeconds(defaultFreshAuthWindowSeconds) > now;
|
||||
}
|
||||
const int defaultFreshAuthWindowSeconds = 300;
|
||||
return authTime.Value.AddSeconds(defaultFreshAuthWindowSeconds) > now;
|
||||
}
|
||||
|
||||
private static ConsoleVulnerabilityQuery BuildVulnerabilityQuery(HttpRequest request)
|
||||
{
|
||||
var builder = new ConsoleVulnerabilityQueryBuilder()
|
||||
.SetPageSize(ParseInt(request.Query["pageSize"], 50))
|
||||
.SetPageToken(request.Query.TryGetValue("pageToken", out var tokenValues) ? tokenValues.FirstOrDefault() : null)
|
||||
.AddSeverity(ReadMulti(request, "severity"))
|
||||
.AddPolicyBadges(ReadMulti(request, "policyBadge"))
|
||||
.AddReachability(ReadMulti(request, "reachability"))
|
||||
.AddProducts(ReadMulti(request, "product"))
|
||||
.AddVexStates(ReadMulti(request, "vexState"));
|
||||
|
||||
var search = request.Query.TryGetValue("search", out var searchValues)
|
||||
? searchValues
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.SelectMany(value => value!.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
: Array.Empty<string>();
|
||||
|
||||
builder.AddSearchTerms(search);
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private static ConsoleVexQuery BuildVexQuery(HttpRequest request)
|
||||
{
|
||||
var builder = new ConsoleVexQueryBuilder()
|
||||
.SetPageSize(ParseInt(request.Query["pageSize"], 50))
|
||||
.SetPageToken(request.Query.TryGetValue("pageToken", out var pageValues) ? pageValues.FirstOrDefault() : null)
|
||||
.AddAdvisories(ReadMulti(request, "advisoryId"))
|
||||
.AddTypes(ReadMulti(request, "statementType"))
|
||||
.AddStates(ReadMulti(request, "state"));
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ReadMulti(HttpRequest request, string key)
|
||||
{
|
||||
if (!request.Query.TryGetValue(key, out var values))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return values
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.SelectMany(value => value!.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
.Where(value => value.Length > 0);
|
||||
}
|
||||
|
||||
private static int ParseInt(StringValues values, int fallback)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return int.TryParse(values[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var number)
|
||||
? number
|
||||
: fallback;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ExtractRoles(ClaimsPrincipal principal)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Authority.Console;
|
||||
|
||||
internal sealed record ConsoleFacetBucket(string Value, int Count);
|
||||
|
||||
internal sealed record ConsoleFacetDistribution(
|
||||
IReadOnlyList<ConsoleFacetBucket> Severity,
|
||||
IReadOnlyList<ConsoleFacetBucket> PolicyBadge,
|
||||
IReadOnlyList<ConsoleFacetBucket> Reachability);
|
||||
|
||||
internal sealed record ConsoleFindingTimestamps(
|
||||
DateTimeOffset FirstSeen,
|
||||
DateTimeOffset LastSeen,
|
||||
DateTimeOffset? VexLastUpdated);
|
||||
|
||||
internal sealed record ConsoleVulnCoordinates(
|
||||
string AdvisoryId,
|
||||
string Package,
|
||||
string Component,
|
||||
string Image);
|
||||
|
||||
internal sealed record ConsoleVulnVexSummary(
|
||||
string StatementId,
|
||||
string State,
|
||||
string? Justification);
|
||||
|
||||
internal sealed record ConsoleReachabilitySummary(
|
||||
string Status,
|
||||
DateTimeOffset? LastObserved,
|
||||
string? SignalsVersion);
|
||||
|
||||
internal sealed record ConsoleReachabilityDetail(
|
||||
string Status,
|
||||
IReadOnlyList<string> CallPathSamples,
|
||||
DateTimeOffset? LastObserved,
|
||||
string? SignalsVersion);
|
||||
|
||||
internal sealed record ConsoleVulnEvidenceSummary(
|
||||
string? SbomDigest,
|
||||
string? PolicyRunId,
|
||||
string? AttestationId);
|
||||
|
||||
internal sealed record ConsoleEvidenceDetail(
|
||||
ConsoleVulnEvidenceSummary Summary,
|
||||
IReadOnlyList<string> ComponentPath,
|
||||
IReadOnlyList<ConsoleAttestationReference> Attestations);
|
||||
|
||||
internal sealed record ConsoleAttestationReference(
|
||||
string Type,
|
||||
string AttestationId,
|
||||
string Signer,
|
||||
string BundleDigest);
|
||||
|
||||
internal sealed record ConsolePolicyBadge(
|
||||
string PolicyId,
|
||||
string Verdict,
|
||||
string? ExplainUrl);
|
||||
|
||||
internal sealed record ConsoleVulnVexDetail(
|
||||
string StatementId,
|
||||
string State,
|
||||
string Justification,
|
||||
string? ImpactStatement,
|
||||
IReadOnlyList<ConsoleRemediation> Remediations);
|
||||
|
||||
internal sealed record ConsoleRemediation(
|
||||
string Type,
|
||||
string Description,
|
||||
DateTimeOffset? Deadline);
|
||||
|
||||
internal sealed record ConsoleVulnerabilityFinding(
|
||||
string Tenant,
|
||||
string FindingId,
|
||||
ConsoleVulnCoordinates Coordinates,
|
||||
string Summary,
|
||||
string Severity,
|
||||
double? Cvss,
|
||||
bool Kev,
|
||||
string PolicyBadge,
|
||||
ConsoleVulnVexSummary? Vex,
|
||||
ConsoleReachabilitySummary? Reachability,
|
||||
ConsoleVulnEvidenceSummary? Evidence,
|
||||
ConsoleFindingTimestamps Timestamps);
|
||||
|
||||
internal sealed record ConsoleVulnerabilityFindingDetail(
|
||||
ConsoleVulnerabilityFinding Summary,
|
||||
string Description,
|
||||
IReadOnlyList<string> References,
|
||||
IReadOnlyList<ConsolePolicyBadge> PolicyBadges,
|
||||
ConsoleVulnVexDetail? Vex,
|
||||
ConsoleReachabilityDetail? Reachability,
|
||||
ConsoleEvidenceDetail Evidence);
|
||||
|
||||
internal sealed record ConsoleVulnerabilitySearchResponse(
|
||||
IReadOnlyList<ConsoleVulnerabilityFinding> Items,
|
||||
ConsoleFacetDistribution Facets,
|
||||
string? NextPageToken);
|
||||
|
||||
internal sealed record ConsoleVulnerabilityQuery(
|
||||
IReadOnlyList<string> Severity,
|
||||
IReadOnlyList<string> PolicyBadges,
|
||||
IReadOnlyList<string> Reachability,
|
||||
IReadOnlyList<string> Products,
|
||||
IReadOnlyList<string> VexStates,
|
||||
IReadOnlyList<string> SearchTerms,
|
||||
int PageSize,
|
||||
string? PageToken);
|
||||
|
||||
internal sealed record ConsoleVulnerabilityTicketRequest(
|
||||
IReadOnlyList<string> Selection,
|
||||
string TargetSystem,
|
||||
IReadOnlyDictionary<string, string>? Metadata);
|
||||
|
||||
internal sealed record ConsoleTicketPayload(
|
||||
string Version,
|
||||
string Tenant,
|
||||
IReadOnlyList<ConsoleTicketSelection> Findings,
|
||||
string PolicyBadge,
|
||||
string VexSummary,
|
||||
IReadOnlyList<ConsoleTicketAttachment> Attachments);
|
||||
|
||||
internal sealed record ConsoleTicketSelection(string FindingId, string Severity);
|
||||
|
||||
internal sealed record ConsoleTicketAttachment(
|
||||
string Type,
|
||||
string Name,
|
||||
string Digest,
|
||||
string ContentType,
|
||||
DateTimeOffset ExpiresAt);
|
||||
|
||||
internal sealed record ConsoleVulnerabilityTicketResponse(
|
||||
string TicketId,
|
||||
ConsoleTicketPayload Payload,
|
||||
string AuditEventId);
|
||||
|
||||
internal sealed record ConsoleVexStatementSummary(
|
||||
string Tenant,
|
||||
string StatementId,
|
||||
string AdvisoryId,
|
||||
string Product,
|
||||
string State,
|
||||
string Justification,
|
||||
string StatementType,
|
||||
DateTimeOffset LastUpdated,
|
||||
ConsoleVexSourceMetadata Source);
|
||||
|
||||
internal sealed record ConsoleVexSourceMetadata(
|
||||
string Type,
|
||||
string? ModelBuild,
|
||||
double? Confidence);
|
||||
|
||||
internal sealed record ConsoleVexStatementPage(
|
||||
IReadOnlyList<ConsoleVexStatementSummary> Items,
|
||||
string? NextPageToken);
|
||||
|
||||
internal sealed record ConsoleVexQuery(
|
||||
IReadOnlyList<string> AdvisoryIds,
|
||||
IReadOnlyList<string> StatementTypes,
|
||||
IReadOnlyList<string> States,
|
||||
int PageSize,
|
||||
string? PageToken);
|
||||
|
||||
internal interface IConsoleWorkspaceService
|
||||
{
|
||||
Task<ConsoleVulnerabilitySearchResponse> SearchFindingsAsync(
|
||||
string tenant,
|
||||
ConsoleVulnerabilityQuery query,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ConsoleVulnerabilityFindingDetail?> GetFindingAsync(
|
||||
string tenant,
|
||||
string findingId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ConsoleVulnerabilityTicketResponse> CreateTicketAsync(
|
||||
string tenant,
|
||||
ConsoleVulnerabilityTicketRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ConsoleVexStatementPage> GetVexStatementsAsync(
|
||||
string tenant,
|
||||
ConsoleVexQuery query,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
internal sealed class ConsoleVulnerabilityQueryBuilder
|
||||
{
|
||||
private readonly HashSet<string> _severity = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _policy = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _reachability = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _products = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _vexStates = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _searchTerms = new(StringComparer.OrdinalIgnoreCase);
|
||||
private int _pageSize = 50;
|
||||
private string? _pageToken;
|
||||
|
||||
public ConsoleVulnerabilityQueryBuilder SetPageSize(int value)
|
||||
{
|
||||
_pageSize = Math.Clamp(value, 1, 200);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVulnerabilityQueryBuilder SetPageToken(string? token)
|
||||
{
|
||||
_pageToken = string.IsNullOrWhiteSpace(token) ? null : token;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVulnerabilityQueryBuilder AddSeverity(IEnumerable<string> values)
|
||||
{
|
||||
_severity.UnionWith(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVulnerabilityQueryBuilder AddPolicyBadges(IEnumerable<string> values)
|
||||
{
|
||||
_policy.UnionWith(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVulnerabilityQueryBuilder AddReachability(IEnumerable<string> values)
|
||||
{
|
||||
_reachability.UnionWith(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVulnerabilityQueryBuilder AddProducts(IEnumerable<string> values)
|
||||
{
|
||||
_products.UnionWith(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVulnerabilityQueryBuilder AddVexStates(IEnumerable<string> values)
|
||||
{
|
||||
_vexStates.UnionWith(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVulnerabilityQueryBuilder AddSearchTerms(IEnumerable<string> values)
|
||||
{
|
||||
_searchTerms.UnionWith(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVulnerabilityQuery Build() =>
|
||||
new(
|
||||
_severity.ToImmutableArray(),
|
||||
_policy.ToImmutableArray(),
|
||||
_reachability.ToImmutableArray(),
|
||||
_products.ToImmutableArray(),
|
||||
_vexStates.ToImmutableArray(),
|
||||
_searchTerms.ToImmutableArray(),
|
||||
_pageSize,
|
||||
_pageToken);
|
||||
}
|
||||
|
||||
internal sealed class ConsoleVexQueryBuilder
|
||||
{
|
||||
private readonly HashSet<string> _advisories = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _types = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _states = new(StringComparer.OrdinalIgnoreCase);
|
||||
private int _pageSize = 50;
|
||||
private string? _pageToken;
|
||||
|
||||
public ConsoleVexQueryBuilder SetPageSize(int value)
|
||||
{
|
||||
_pageSize = Math.Clamp(value, 1, 200);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVexQueryBuilder SetPageToken(string? token)
|
||||
{
|
||||
_pageToken = string.IsNullOrWhiteSpace(token) ? null : token;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVexQueryBuilder AddAdvisories(IEnumerable<string> values)
|
||||
{
|
||||
_advisories.UnionWith(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVexQueryBuilder AddTypes(IEnumerable<string> values)
|
||||
{
|
||||
_types.UnionWith(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVexQueryBuilder AddStates(IEnumerable<string> values)
|
||||
{
|
||||
_states.UnionWith(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConsoleVexQuery Build() =>
|
||||
new(
|
||||
_advisories.ToImmutableArray(),
|
||||
_types.ToImmutableArray(),
|
||||
_states.ToImmutableArray(),
|
||||
_pageSize,
|
||||
_pageToken);
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Authority.Console;
|
||||
|
||||
internal sealed class ConsoleWorkspaceSampleService : IConsoleWorkspaceService
|
||||
{
|
||||
private static readonly ImmutableArray<ConsoleVulnerabilityFindingDetail> SampleFindings;
|
||||
private static readonly ImmutableArray<ConsoleVexStatementSummary> SampleStatements;
|
||||
|
||||
static ConsoleWorkspaceSampleService()
|
||||
{
|
||||
var finding1Summary = new ConsoleVulnerabilityFinding(
|
||||
Tenant: "tenant-default",
|
||||
FindingId: "tenant-default:advisory-ai:sha256:5d1a",
|
||||
Coordinates: new ConsoleVulnCoordinates(
|
||||
AdvisoryId: "CVE-2024-12345",
|
||||
Package: "pkg:npm/jsonwebtoken@9.0.2",
|
||||
Component: "jwt-auth-service",
|
||||
Image: "registry.local/ops/auth:2025.10.0"),
|
||||
Summary: "jsonwebtoken <10.0.0 allows algorithm downgrade.",
|
||||
Severity: "high",
|
||||
Cvss: 8.1,
|
||||
Kev: true,
|
||||
PolicyBadge: "fail",
|
||||
Vex: new ConsoleVulnVexSummary(
|
||||
StatementId: "vex:tenant-default:jwt-auth:5d1a",
|
||||
State: "under_investigation",
|
||||
Justification: "Advisory AI flagged reachable path via Scheduler run 42."),
|
||||
Reachability: new ConsoleReachabilitySummary(
|
||||
Status: "reachable",
|
||||
LastObserved: DateTimeOffset.Parse("2025-11-07T23:11:04Z"),
|
||||
SignalsVersion: "signals-2025.310.1"),
|
||||
Evidence: new ConsoleVulnEvidenceSummary(
|
||||
SbomDigest: "sha256:6c81f2bbd8bd7336f197f3f68fba2f76d7287dd1a5e2a0f0e9f14f23f3c2f917",
|
||||
PolicyRunId: "policy-run::2025-11-07::ca9f",
|
||||
AttestationId: "dsse://authority/attest/84a2"),
|
||||
Timestamps: new ConsoleFindingTimestamps(
|
||||
FirstSeen: DateTimeOffset.Parse("2025-10-31T04:22:18Z"),
|
||||
LastSeen: DateTimeOffset.Parse("2025-11-07T23:16:51Z"),
|
||||
VexLastUpdated: DateTimeOffset.Parse("2025-11-07T23:10:09Z")));
|
||||
|
||||
var finding2Summary = new ConsoleVulnerabilityFinding(
|
||||
Tenant: "tenant-default",
|
||||
FindingId: "tenant-default:advisory-ai:sha256:9bf4",
|
||||
Coordinates: new ConsoleVulnCoordinates(
|
||||
AdvisoryId: "GHSA-xxxx-yyyy-zzzz",
|
||||
Package: "pkg:docker/library/nginx@1.25.2",
|
||||
Component: "ingress-gateway",
|
||||
Image: "registry.local/ops/ingress:2025.09.1"),
|
||||
Summary: "Heap overflow in nginx HTTP/3 parsing.",
|
||||
Severity: "critical",
|
||||
Cvss: 9.8,
|
||||
Kev: false,
|
||||
PolicyBadge: "warn",
|
||||
Vex: new ConsoleVulnVexSummary(
|
||||
StatementId: "vex:tenant-default:ingress:9bf4",
|
||||
State: "not_affected",
|
||||
Justification: "component_not_present"),
|
||||
Reachability: new ConsoleReachabilitySummary(
|
||||
Status: "unknown",
|
||||
LastObserved: null,
|
||||
SignalsVersion: "signals-2025.309.0"),
|
||||
Evidence: new ConsoleVulnEvidenceSummary(
|
||||
SbomDigest: "sha256:99f1e2a7aa0f7c970dcb6674244f0bfb5f37148e3ee09fd4f925d3358dea2239",
|
||||
PolicyRunId: "policy-run::2025-11-06::b210",
|
||||
AttestationId: "dsse://authority/attest/1d34"),
|
||||
Timestamps: new ConsoleFindingTimestamps(
|
||||
FirstSeen: DateTimeOffset.Parse("2025-10-29T18:03:11Z"),
|
||||
LastSeen: DateTimeOffset.Parse("2025-11-07T10:45:03Z"),
|
||||
VexLastUpdated: DateTimeOffset.Parse("2025-11-06T18:44:00Z")));
|
||||
|
||||
SampleFindings = ImmutableArray.Create(
|
||||
new ConsoleVulnerabilityFindingDetail(
|
||||
Summary: finding1Summary,
|
||||
Description: "jsonwebtoken accepts untrusted algorithm overrides which allow downgrade attacks.",
|
||||
References: new[]
|
||||
{
|
||||
"https://nvd.nist.gov/vuln/detail/CVE-2024-12345",
|
||||
"https://github.com/auth0/node-jsonwebtoken/security/advisories/GHSA-45mw-4jw3-g2wg"
|
||||
},
|
||||
PolicyBadges: new[]
|
||||
{
|
||||
new ConsolePolicyBadge("policy://tenant-default/runtime-hardening", "fail", "https://console.local/policy/runs/policy-run::2025-11-07::ca9f")
|
||||
},
|
||||
Vex: new ConsoleVulnVexDetail(
|
||||
StatementId: "vex:tenant-default:jwt-auth:5d1a",
|
||||
State: "under_investigation",
|
||||
Justification: "Runtime telemetry confirmed exploitation path.",
|
||||
ImpactStatement: "Token exchange service remains exposed until patch 2025.11.2.",
|
||||
Remediations: new[]
|
||||
{
|
||||
new ConsoleRemediation("patch", "Upgrade jwt-auth-service to 2025.11.2.", DateTimeOffset.Parse("2025-11-12T00:00:00Z"))
|
||||
}),
|
||||
Reachability: new ConsoleReachabilityDetail(
|
||||
Status: "reachable",
|
||||
CallPathSamples: new[]
|
||||
{
|
||||
"api-gateway -> jwt-auth-service -> jsonwebtoken.verify"
|
||||
},
|
||||
LastObserved: DateTimeOffset.Parse("2025-11-07T23:11:04Z"),
|
||||
SignalsVersion: "signals-2025.310.1"),
|
||||
Evidence: new ConsoleEvidenceDetail(
|
||||
Summary: finding1Summary.Evidence!,
|
||||
ComponentPath: new[]
|
||||
{
|
||||
"/src/jwt-auth/package.json",
|
||||
"/src/jwt-auth/node_modules/jsonwebtoken"
|
||||
},
|
||||
Attestations: new[]
|
||||
{
|
||||
new ConsoleAttestationReference("scan-report", "dsse://authority/attest/84a2", "attestor@stella-ops.org", "sha256:e2bb5c7a0a8b2d16ff42e7f8decb4bb8be71ad0a1606dbc5d28be43675fbad32")
|
||||
})),
|
||||
new ConsoleVulnerabilityFindingDetail(
|
||||
Summary: finding2Summary,
|
||||
Description: "nginx HTTP/3 heap overflow affecting unpatched ingress nodes.",
|
||||
References: new[]
|
||||
{
|
||||
"https://security.nginx.org/announcements/2024/http3-overflow",
|
||||
},
|
||||
PolicyBadges: new[]
|
||||
{
|
||||
new ConsolePolicyBadge("policy://tenant-default/network-hardening", "warn", null)
|
||||
},
|
||||
Vex: new ConsoleVulnVexDetail(
|
||||
StatementId: "vex:tenant-default:ingress:9bf4",
|
||||
State: "not_affected",
|
||||
Justification: "component_not_present",
|
||||
ImpactStatement: "HTTP/3 disabled on ingress, exposure limited.",
|
||||
Remediations: Array.Empty<ConsoleRemediation>()),
|
||||
Reachability: new ConsoleReachabilityDetail(
|
||||
Status: "unknown",
|
||||
CallPathSamples: Array.Empty<string>(),
|
||||
LastObserved: null,
|
||||
SignalsVersion: "signals-2025.309.0"),
|
||||
Evidence: new ConsoleEvidenceDetail(
|
||||
Summary: finding2Summary.Evidence!,
|
||||
ComponentPath: new[]
|
||||
{
|
||||
"/charts/ingress/templates/deployment.yaml"
|
||||
},
|
||||
Attestations: new[]
|
||||
{
|
||||
new ConsoleAttestationReference("scan-report", "dsse://authority/attest/1d34", "attestor@stella-ops.org", "sha256:91e6dd2c1bbf9a4ac797e050d71bf7f1b958d1a0c27469364c44d8ed74bcb9dc")
|
||||
})));
|
||||
|
||||
SampleStatements = ImmutableArray.Create(
|
||||
new ConsoleVexStatementSummary(
|
||||
Tenant: "tenant-default",
|
||||
StatementId: "vex:tenant-default:jwt-auth:5d1a",
|
||||
AdvisoryId: "CVE-2024-12345",
|
||||
Product: "registry.local/ops/auth:2025.10.0",
|
||||
State: "under_investigation",
|
||||
Justification: "exploit_observed",
|
||||
StatementType: "advisory_ai",
|
||||
LastUpdated: DateTimeOffset.Parse("2025-11-07T23:10:09Z"),
|
||||
Source: new ConsoleVexSourceMetadata("advisory_ai", "aiai-console-2025-10-28", 0.74)),
|
||||
new ConsoleVexStatementSummary(
|
||||
Tenant: "tenant-default",
|
||||
StatementId: "vex:tenant-default:jwt-auth:5d1a",
|
||||
AdvisoryId: "CVE-2024-12345",
|
||||
Product: "registry.local/ops/auth:2025.10.0",
|
||||
State: "fixed",
|
||||
Justification: "solution_available",
|
||||
StatementType: "advisory_ai",
|
||||
LastUpdated: DateTimeOffset.Parse("2025-11-08T11:44:32Z"),
|
||||
Source: new ConsoleVexSourceMetadata("advisory_ai", "aiai-console-2025-10-28", 0.92)),
|
||||
new ConsoleVexStatementSummary(
|
||||
Tenant: "tenant-default",
|
||||
StatementId: "vex:tenant-default:ingress:9bf4",
|
||||
AdvisoryId: "GHSA-xxxx-yyyy-zzzz",
|
||||
Product: "registry.local/ops/ingress:2025.09.1",
|
||||
State: "not_affected",
|
||||
Justification: "component_not_present",
|
||||
StatementType: "excitor",
|
||||
LastUpdated: DateTimeOffset.Parse("2025-11-06T18:44:00Z"),
|
||||
Source: new ConsoleVexSourceMetadata("excitor", null, null)));
|
||||
}
|
||||
|
||||
public Task<ConsoleVulnerabilitySearchResponse> SearchFindingsAsync(
|
||||
string tenant,
|
||||
ConsoleVulnerabilityQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var filtered = SampleFindings
|
||||
.Where(detail => IsTenantMatch(tenant, detail.Summary))
|
||||
.Where(detail => MatchesSeverity(detail, query))
|
||||
.Where(detail => MatchesPolicy(detail, query))
|
||||
.Where(detail => MatchesReachability(detail, query))
|
||||
.Where(detail => MatchesSearch(detail, query))
|
||||
.Take(query.PageSize)
|
||||
.Select(detail => detail.Summary)
|
||||
.ToImmutableArray();
|
||||
|
||||
var facets = BuildFacets(tenant);
|
||||
var response = new ConsoleVulnerabilitySearchResponse(filtered, facets, NextPageToken: null);
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
|
||||
public Task<ConsoleVulnerabilityFindingDetail?> GetFindingAsync(
|
||||
string tenant,
|
||||
string findingId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var detail = SampleFindings.FirstOrDefault(f =>
|
||||
string.Equals(f.Summary.FindingId, findingId, StringComparison.OrdinalIgnoreCase) &&
|
||||
IsTenantMatch(tenant, f.Summary));
|
||||
|
||||
return Task.FromResult(detail);
|
||||
}
|
||||
|
||||
public Task<ConsoleVulnerabilityTicketResponse> CreateTicketAsync(
|
||||
string tenant,
|
||||
ConsoleVulnerabilityTicketRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ImmutableArray<ConsoleTicketSelection> selection;
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
selection = ImmutableArray<ConsoleTicketSelection>.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
selection = request.Selection
|
||||
.Select(id => SampleFindings.FirstOrDefault(f => string.Equals(f.Summary.FindingId, id, StringComparison.OrdinalIgnoreCase)))
|
||||
.Where(detail => detail is not null)
|
||||
.Select(detail => new ConsoleTicketSelection(detail!.Summary.FindingId, detail.Summary.Severity))
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
var ticketId = BuildTicketId(tenant, request.Selection);
|
||||
var attachmentName = $"console-ticket-{DateTimeOffset.UtcNow:yyyyMMdd}.json";
|
||||
var payload = new ConsoleTicketPayload(
|
||||
Version: "2025-11-01",
|
||||
Tenant: tenant,
|
||||
Findings: selection,
|
||||
PolicyBadge: selection.Any(sel => string.Equals(sel.Severity, "critical", StringComparison.OrdinalIgnoreCase)) ? "fail" : "warn",
|
||||
VexSummary: $"{selection.Length} findings included in ticket.",
|
||||
Attachments: new[]
|
||||
{
|
||||
new ConsoleTicketAttachment(
|
||||
Type: "json",
|
||||
Name: attachmentName,
|
||||
Digest: HashAttachmentName(attachmentName),
|
||||
ContentType: "application/json",
|
||||
ExpiresAt: DateTimeOffset.UtcNow.AddDays(7))
|
||||
});
|
||||
|
||||
var response = new ConsoleVulnerabilityTicketResponse(
|
||||
TicketId: ticketId,
|
||||
Payload: payload,
|
||||
AuditEventId: $"{ticketId}::audit");
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
|
||||
public Task<ConsoleVexStatementPage> GetVexStatementsAsync(
|
||||
string tenant,
|
||||
ConsoleVexQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var filtered = SampleStatements
|
||||
.Where(statement => string.Equals(statement.Tenant, tenant, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(statement => MatchesAdvisory(statement, query))
|
||||
.Where(statement => MatchesState(statement, query))
|
||||
.Where(statement => MatchesType(statement, query))
|
||||
.Take(query.PageSize)
|
||||
.ToImmutableArray();
|
||||
|
||||
var page = new ConsoleVexStatementPage(filtered, NextPageToken: null);
|
||||
return Task.FromResult(page);
|
||||
}
|
||||
|
||||
private static bool MatchesSeverity(ConsoleVulnerabilityFindingDetail detail, ConsoleVulnerabilityQuery query) =>
|
||||
query.Severity.Count == 0 ||
|
||||
query.Severity.Any(sev => string.Equals(sev, detail.Summary.Severity, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static bool MatchesPolicy(ConsoleVulnerabilityFindingDetail detail, ConsoleVulnerabilityQuery query) =>
|
||||
query.PolicyBadges.Count == 0 ||
|
||||
query.PolicyBadges.Any(badge => string.Equals(badge, detail.Summary.PolicyBadge, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static bool MatchesReachability(ConsoleVulnerabilityFindingDetail detail, ConsoleVulnerabilityQuery query)
|
||||
{
|
||||
if (query.Reachability.Count == 0 || detail.Summary.Reachability is null)
|
||||
{
|
||||
return query.Reachability.Count == 0;
|
||||
}
|
||||
|
||||
return query.Reachability.Any(state =>
|
||||
string.Equals(state, detail.Summary.Reachability.Status, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool MatchesSearch(ConsoleVulnerabilityFindingDetail detail, ConsoleVulnerabilityQuery query)
|
||||
{
|
||||
if (query.SearchTerms.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return query.SearchTerms.Any(term =>
|
||||
Contains(term, detail.Summary.FindingId) ||
|
||||
Contains(term, detail.Summary.Coordinates.AdvisoryId) ||
|
||||
Contains(term, detail.Summary.Coordinates.Component));
|
||||
}
|
||||
|
||||
private static bool MatchesAdvisory(ConsoleVexStatementSummary summary, ConsoleVexQuery query) =>
|
||||
query.AdvisoryIds.Count == 0 ||
|
||||
query.AdvisoryIds.Any(advisory => string.Equals(advisory, summary.AdvisoryId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static bool MatchesType(ConsoleVexStatementSummary summary, ConsoleVexQuery query) =>
|
||||
query.StatementTypes.Count == 0 ||
|
||||
query.StatementTypes.Any(type => string.Equals(type, summary.StatementType, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static bool MatchesState(ConsoleVexStatementSummary summary, ConsoleVexQuery query) =>
|
||||
query.States.Count == 0 ||
|
||||
query.States.Any(state => string.Equals(state, summary.State, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static bool Contains(string term, string value) =>
|
||||
value?.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
private static bool IsTenantMatch(string tenant, ConsoleVulnerabilityFinding summary) =>
|
||||
string.Equals(summary.Tenant, tenant, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static ConsoleFacetDistribution BuildFacets(string tenant)
|
||||
{
|
||||
var findings = SampleFindings.Where(detail => IsTenantMatch(tenant, detail.Summary)).Select(detail => detail.Summary);
|
||||
return new ConsoleFacetDistribution(
|
||||
Severity: AggregateFacet(findings, finding => finding.Severity),
|
||||
PolicyBadge: AggregateFacet(findings, finding => finding.PolicyBadge),
|
||||
Reachability: AggregateFacet(findings, finding => finding.Reachability?.Status ?? "unknown"));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ConsoleFacetBucket> AggregateFacet(
|
||||
IEnumerable<ConsoleVulnerabilityFinding> findings,
|
||||
Func<ConsoleVulnerabilityFinding, string> selector) =>
|
||||
findings
|
||||
.GroupBy(selector, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderByDescending(group => group.Count())
|
||||
.ThenBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => new ConsoleFacetBucket(group.Key ?? "unknown", group.Count()))
|
||||
.ToImmutableArray();
|
||||
|
||||
private static string BuildTicketId(string tenant, IEnumerable<string> selection)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
var joined = string.Join("|", selection.Order());
|
||||
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(joined));
|
||||
var prefix = Convert.ToHexString(hash[..8]).ToLowerInvariant();
|
||||
return $"console-ticket::{tenant}::{prefix}";
|
||||
}
|
||||
|
||||
private static string HashAttachmentName(string name)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(name));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
@@ -1,314 +1,319 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace StellaOps.Authority.OpenApi;
|
||||
|
||||
internal sealed class AuthorityOpenApiDocumentProvider
|
||||
{
|
||||
private readonly string specificationPath;
|
||||
private readonly ILogger<AuthorityOpenApiDocumentProvider> logger;
|
||||
private readonly SemaphoreSlim refreshLock = new(1, 1);
|
||||
private OpenApiDocumentSnapshot? cached;
|
||||
|
||||
public AuthorityOpenApiDocumentProvider(IWebHostEnvironment environment, ILogger<AuthorityOpenApiDocumentProvider> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(environment);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
specificationPath = Path.Combine(environment.ContentRootPath, "OpenApi", "authority.yaml");
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async ValueTask<OpenApiDocumentSnapshot> GetDocumentAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var lastWriteUtc = GetLastWriteTimeUtc();
|
||||
var current = cached;
|
||||
if (current is not null && current.LastWriteUtc == lastWriteUtc)
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
await refreshLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
current = cached;
|
||||
lastWriteUtc = GetLastWriteTimeUtc();
|
||||
if (current is not null && current.LastWriteUtc == lastWriteUtc)
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
var snapshot = LoadSnapshot(lastWriteUtc);
|
||||
cached = snapshot;
|
||||
return snapshot;
|
||||
}
|
||||
finally
|
||||
{
|
||||
refreshLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private DateTime GetLastWriteTimeUtc()
|
||||
{
|
||||
var file = new FileInfo(specificationPath);
|
||||
if (!file.Exists)
|
||||
{
|
||||
throw new FileNotFoundException($"Authority OpenAPI specification was not found at '{specificationPath}'.", specificationPath);
|
||||
}
|
||||
|
||||
return file.LastWriteTimeUtc;
|
||||
}
|
||||
|
||||
private OpenApiDocumentSnapshot LoadSnapshot(DateTime lastWriteUtc)
|
||||
{
|
||||
string yamlText;
|
||||
try
|
||||
{
|
||||
yamlText = File.ReadAllText(specificationPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to read Authority OpenAPI specification from {Path}.", specificationPath);
|
||||
throw;
|
||||
}
|
||||
|
||||
var yamlStream = new YamlStream();
|
||||
using (var reader = new StringReader(yamlText))
|
||||
{
|
||||
yamlStream.Load(reader);
|
||||
}
|
||||
|
||||
if (yamlStream.Documents.Count == 0 || yamlStream.Documents[0].RootNode is not YamlMappingNode rootNode)
|
||||
{
|
||||
throw new InvalidOperationException("Authority OpenAPI specification does not contain a valid root mapping node.");
|
||||
}
|
||||
|
||||
var (grants, scopes) = CollectGrantsAndScopes(rootNode);
|
||||
|
||||
if (!TryGetMapping(rootNode, "info", out var infoNode))
|
||||
{
|
||||
infoNode = new YamlMappingNode();
|
||||
rootNode.Children[new YamlScalarNode("info")] = infoNode;
|
||||
}
|
||||
|
||||
var serviceName = "authority";
|
||||
var buildVersion = ResolveBuildVersion();
|
||||
ApplyInfoMetadata(infoNode, serviceName, buildVersion, grants, scopes);
|
||||
|
||||
var apiVersion = TryGetScalar(infoNode, "version", out var version)
|
||||
? version
|
||||
: "0.0.0";
|
||||
|
||||
var updatedYaml = WriteYaml(yamlStream);
|
||||
var json = ConvertYamlToJson(updatedYaml);
|
||||
var etag = CreateStrongEtag(json);
|
||||
|
||||
return new OpenApiDocumentSnapshot(
|
||||
serviceName,
|
||||
apiVersion,
|
||||
buildVersion,
|
||||
json,
|
||||
updatedYaml,
|
||||
etag,
|
||||
lastWriteUtc,
|
||||
grants,
|
||||
scopes);
|
||||
}
|
||||
|
||||
private static (IReadOnlyList<string> Grants, IReadOnlyList<string> Scopes) CollectGrantsAndScopes(YamlMappingNode root)
|
||||
{
|
||||
if (!TryGetMapping(root, "components", out var components) ||
|
||||
!TryGetMapping(components, "securitySchemes", out var securitySchemes))
|
||||
{
|
||||
return (Array.Empty<string>(), Array.Empty<string>());
|
||||
}
|
||||
|
||||
var grants = new SortedSet<string>(StringComparer.Ordinal);
|
||||
var scopes = new SortedSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var scheme in securitySchemes.Children.Values.OfType<YamlMappingNode>())
|
||||
{
|
||||
if (!TryGetMapping(scheme, "flows", out var flows))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var flowEntry in flows.Children)
|
||||
{
|
||||
if (flowEntry.Key is not YamlScalarNode flowNameNode || flowEntry.Value is not YamlMappingNode flowMapping)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var grant = NormalizeGrantName(flowNameNode.Value);
|
||||
if (grant is not null)
|
||||
{
|
||||
grants.Add(grant);
|
||||
}
|
||||
|
||||
if (TryGetMapping(flowMapping, "scopes", out var scopesMapping))
|
||||
{
|
||||
foreach (var scope in scopesMapping.Children.Keys.OfType<YamlScalarNode>())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(scope.Value))
|
||||
{
|
||||
scopes.Add(scope.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (flowMapping.Children.TryGetValue(new YamlScalarNode("refreshUrl"), out var refreshNode) &&
|
||||
refreshNode is YamlScalarNode refreshScalar && !string.IsNullOrWhiteSpace(refreshScalar.Value))
|
||||
{
|
||||
grants.Add("refresh_token");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
grants.Count == 0 ? Array.Empty<string>() : grants.ToArray(),
|
||||
scopes.Count == 0 ? Array.Empty<string>() : scopes.ToArray());
|
||||
}
|
||||
|
||||
private static string? NormalizeGrantName(string? flowName)
|
||||
=> flowName switch
|
||||
{
|
||||
null or "" => null,
|
||||
"authorizationCode" => "authorization_code",
|
||||
"clientCredentials" => "client_credentials",
|
||||
"password" => "password",
|
||||
"implicit" => "implicit",
|
||||
"deviceCode" => "device_code",
|
||||
_ => flowName
|
||||
};
|
||||
|
||||
private static void ApplyInfoMetadata(
|
||||
YamlMappingNode infoNode,
|
||||
string serviceName,
|
||||
string buildVersion,
|
||||
IReadOnlyList<string> grants,
|
||||
IReadOnlyList<string> scopes)
|
||||
{
|
||||
infoNode.Children[new YamlScalarNode("x-stella-service")] = new YamlScalarNode(serviceName);
|
||||
infoNode.Children[new YamlScalarNode("x-stella-build-version")] = new YamlScalarNode(buildVersion);
|
||||
infoNode.Children[new YamlScalarNode("x-stella-grant-types")] = CreateSequence(grants);
|
||||
infoNode.Children[new YamlScalarNode("x-stella-scopes")] = CreateSequence(scopes);
|
||||
}
|
||||
|
||||
private static YamlSequenceNode CreateSequence(IEnumerable<string> values)
|
||||
{
|
||||
var sequence = new YamlSequenceNode();
|
||||
foreach (var value in values)
|
||||
{
|
||||
sequence.Add(new YamlScalarNode(value));
|
||||
}
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
private static bool TryGetMapping(YamlMappingNode node, string key, out YamlMappingNode mapping)
|
||||
{
|
||||
foreach (var entry in node.Children)
|
||||
{
|
||||
if (entry.Key is YamlScalarNode scalar && string.Equals(scalar.Value, key, StringComparison.Ordinal))
|
||||
{
|
||||
if (entry.Value is YamlMappingNode mappingNode)
|
||||
{
|
||||
mapping = mappingNode;
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mapping = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetScalar(YamlMappingNode node, string key, out string value)
|
||||
{
|
||||
foreach (var entry in node.Children)
|
||||
{
|
||||
if (entry.Key is YamlScalarNode scalar && string.Equals(scalar.Value, key, StringComparison.Ordinal))
|
||||
{
|
||||
if (entry.Value is YamlScalarNode valueNode)
|
||||
{
|
||||
value = valueNode.Value ?? string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string WriteYaml(YamlStream yamlStream)
|
||||
{
|
||||
using var writer = new StringWriter(CultureInfo.InvariantCulture);
|
||||
yamlStream.Save(writer, assignAnchors: false);
|
||||
return writer.ToString();
|
||||
}
|
||||
|
||||
private static string ConvertYamlToJson(string yaml)
|
||||
{
|
||||
var deserializer = new DeserializerBuilder().Build();
|
||||
var yamlObject = deserializer.Deserialize(new StringReader(yaml));
|
||||
|
||||
var serializer = new SerializerBuilder()
|
||||
.JsonCompatible()
|
||||
.Build();
|
||||
|
||||
var json = serializer.Serialize(yamlObject);
|
||||
return string.IsNullOrWhiteSpace(json) ? "{}" : json.Trim();
|
||||
}
|
||||
|
||||
private static string CreateStrongEtag(string jsonRepresentation)
|
||||
{
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(jsonRepresentation));
|
||||
var hash = Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
return $"\"{hash}\"";
|
||||
}
|
||||
|
||||
private static string ResolveBuildVersion()
|
||||
{
|
||||
var assembly = typeof(AuthorityOpenApiDocumentProvider).Assembly;
|
||||
var informational = assembly
|
||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
|
||||
.InformationalVersion;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(informational))
|
||||
{
|
||||
return informational!;
|
||||
}
|
||||
|
||||
var version = assembly.GetName().Version;
|
||||
return version?.ToString() ?? "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record OpenApiDocumentSnapshot(
|
||||
string ServiceName,
|
||||
string ApiVersion,
|
||||
string BuildVersion,
|
||||
string Json,
|
||||
string Yaml,
|
||||
string ETag,
|
||||
DateTime LastWriteUtc,
|
||||
IReadOnlyList<string> GrantTypes,
|
||||
IReadOnlyList<string> Scopes);
|
||||
using System.Collections.Generic;
|
||||
|
||||
using System.IO;
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
using System.Linq;
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
using System.Text;
|
||||
|
||||
using System.Threading;
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using YamlDotNet.Core;
|
||||
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
|
||||
|
||||
namespace StellaOps.Authority.OpenApi;
|
||||
|
||||
|
||||
|
||||
internal sealed class AuthorityOpenApiDocumentProvider
|
||||
|
||||
{
|
||||
|
||||
private readonly string specificationPath;
|
||||
|
||||
private readonly ILogger<AuthorityOpenApiDocumentProvider> logger;
|
||||
|
||||
private readonly ICryptoHash hash;
|
||||
|
||||
private readonly SemaphoreSlim refreshLock = new(1, 1);
|
||||
|
||||
private OpenApiDocumentSnapshot? cached;
|
||||
|
||||
|
||||
|
||||
public AuthorityOpenApiDocumentProvider(
|
||||
|
||||
IWebHostEnvironment environment,
|
||||
|
||||
ILogger<AuthorityOpenApiDocumentProvider> logger,
|
||||
|
||||
ICryptoHash hash)
|
||||
|
||||
{
|
||||
|
||||
ArgumentNullException.ThrowIfNull(environment);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(hash);
|
||||
|
||||
|
||||
|
||||
specificationPath = Path.Combine(environment.ContentRootPath, "OpenApi", "authority.yaml");
|
||||
|
||||
this.logger = logger;
|
||||
|
||||
this.hash = hash;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async ValueTask<OpenApiDocumentSnapshot> GetDocumentAsync(CancellationToken cancellationToken)
|
||||
|
||||
{
|
||||
|
||||
var lastWriteUtc = GetLastWriteTimeUtc();
|
||||
|
||||
var current = cached;
|
||||
|
||||
if (current is not null && current.LastWriteUtc == lastWriteUtc)
|
||||
|
||||
{
|
||||
|
||||
return current;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
await refreshLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
|
||||
{
|
||||
|
||||
current = cached;
|
||||
|
||||
lastWriteUtc = GetLastWriteTimeUtc();
|
||||
|
||||
if (current is not null && current.LastWriteUtc == lastWriteUtc)
|
||||
|
||||
{
|
||||
|
||||
return current;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
var snapshot = LoadSnapshot(lastWriteUtc);
|
||||
|
||||
cached = snapshot;
|
||||
|
||||
return snapshot;
|
||||
|
||||
}
|
||||
|
||||
finally
|
||||
|
||||
{
|
||||
|
||||
refreshLock.Release();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private DateTime GetLastWriteTimeUtc()
|
||||
|
||||
{
|
||||
|
||||
var file = new FileInfo(specificationPath);
|
||||
|
||||
if (!file.Exists)
|
||||
|
||||
{
|
||||
|
||||
throw new FileNotFoundException($"Authority OpenAPI specification was not found at '{specificationPath}'.", specificationPath);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
return file.LastWriteTimeUtc;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private OpenApiDocumentSnapshot LoadSnapshot(DateTime lastWriteUtc)
|
||||
|
||||
{
|
||||
|
||||
string yamlText;
|
||||
|
||||
try
|
||||
|
||||
{
|
||||
|
||||
yamlText = File.ReadAllText(specificationPath);
|
||||
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
|
||||
{
|
||||
|
||||
logger.LogError(ex, "Failed to read Authority OpenAPI specification from {Path}.", specificationPath);
|
||||
|
||||
throw;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
var yamlStream = new YamlStream();
|
||||
|
||||
using (var reader = new StringReader(yamlText))
|
||||
|
||||
{
|
||||
|
||||
yamlStream.Load(reader);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (yamlStream.Documents.Count == 0 || yamlStream.Documents[0].RootNode is not YamlMappingNode rootNode)
|
||||
|
||||
{
|
||||
|
||||
throw new InvalidOperationException("Authority OpenAPI specification does not contain a valid root mapping node.");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
var (grants, scopes) = CollectGrantsAndScopes(rootNode);
|
||||
|
||||
|
||||
|
||||
if (!TryGetMapping(rootNode, "info", out var infoNode))
|
||||
|
||||
{
|
||||
|
||||
infoNode = new YamlMappingNode();
|
||||
|
||||
rootNode.Children[new YamlScalarNode("info")] = infoNode;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
var serviceName = "authority";
|
||||
|
||||
var buildVersion = ResolveBuildVersion();
|
||||
|
||||
ApplyInfoMetadata(infoNode, serviceName, buildVersion, grants, scopes);
|
||||
|
||||
|
||||
|
||||
var apiVersion = TryGetScalar(infoNode, "version", out var version)
|
||||
|
||||
? version
|
||||
|
||||
: "0.0.0";
|
||||
|
||||
|
||||
|
||||
var updatedYaml = WriteYaml(yamlStream);
|
||||
|
||||
var json = ConvertYamlToJson(updatedYaml);
|
||||
|
||||
var etag = CreateStrongEtag(json);
|
||||
|
||||
|
||||
|
||||
return new OpenApiDocumentSnapshot(
|
||||
|
||||
serviceName,
|
||||
|
||||
apiVersion,
|
||||
|
||||
buildVersion,
|
||||
|
||||
json,
|
||||
|
||||
updatedYaml,
|
||||
|
||||
etag,
|
||||
|
||||
lastWriteUtc,
|
||||
|
||||
grants,
|
||||
|
||||
scopes);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static (IReadOnlyList<string> Grants, IReadOnlyList<string> Scopes) CollectGrantsAndScopes(YamlMappingNode root)
|
||||
|
||||
{
|
||||
|
||||
if (!TryGetMapping(root, "components", out var components) ||
|
||||
|
||||
!TryGetMapping(components, "securitySchemes", out var securitySchemes))
|
||||
|
||||
{
|
||||
|
||||
return (Array.Empty<string>(), Array.Empty<string>());
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
var grants = new SortedSet<string>(StringComparer.Ordinal);
|
||||
|
||||
var scopes = new SortedSet<string>(StringComparer.Ordinal);
|
||||
|
||||
|
||||
|
||||
foreach (var scheme in securitySchemes.Children.Values.OfType<YamlMappingNode>())
|
||||
|
||||
{
|
||||
|
||||
if (!TryGetMapping(scheme, "flows", out var flows))
|
||||
|
||||
{
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
foreach (var flowEntry in flows.Children)
|
||||
|
||||
{
|
||||
|
||||
if (flowEntry.Key is not YamlScalarNode flowNameNode || flowEntry.Value is not YamlMappingNode flowMapping)
|
||||
|
||||
{
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,8 +24,10 @@ internal static class AuthorityOpenIddictConstants
|
||||
internal const string DpopConsumedNonceProperty = "authority:dpop_nonce";
|
||||
internal const string ConfirmationClaimType = "cnf";
|
||||
internal const string SenderConstraintClaimType = "authority_sender_constraint";
|
||||
internal const string SenderNonceClaimType = "authority_sender_nonce";
|
||||
internal const string MtlsCertificateThumbprintProperty = "authority:mtls_thumbprint";
|
||||
internal const string MtlsCertificateHexProperty = "authority:mtls_thumbprint_hex";
|
||||
internal const string MtlsCertificateHexClaimType = "authority_sender_certificate_hex";
|
||||
internal const string ClientTenantProperty = "authority:client_tenant";
|
||||
internal const string ClientProjectProperty = "authority:client_project";
|
||||
internal const string ClientAttributesProperty = "authority:client_attributes";
|
||||
|
||||
@@ -1177,6 +1177,50 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle
|
||||
|
||||
var extraProperties = new List<AuthEventProperty>();
|
||||
|
||||
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.SenderConstraintProperty, out var auditSenderConstraintObj) &&
|
||||
auditSenderConstraintObj is string auditSenderConstraint &&
|
||||
!string.IsNullOrWhiteSpace(auditSenderConstraint))
|
||||
{
|
||||
extraProperties.Add(new AuthEventProperty
|
||||
{
|
||||
Name = "sender.constraint",
|
||||
Value = ClassifiedString.Public(auditSenderConstraint)
|
||||
});
|
||||
}
|
||||
|
||||
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.DpopKeyThumbprintProperty, out var auditDpopThumbprintObj) &&
|
||||
auditDpopThumbprintObj is string auditDpopThumbprint &&
|
||||
!string.IsNullOrWhiteSpace(auditDpopThumbprint))
|
||||
{
|
||||
extraProperties.Add(new AuthEventProperty
|
||||
{
|
||||
Name = "sender.dpop.jkt",
|
||||
Value = ClassifiedString.Sensitive(auditDpopThumbprint)
|
||||
});
|
||||
}
|
||||
|
||||
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.MtlsCertificateThumbprintProperty, out var auditMtlsThumbprintObj) &&
|
||||
auditMtlsThumbprintObj is string auditMtlsThumbprint &&
|
||||
!string.IsNullOrWhiteSpace(auditMtlsThumbprint))
|
||||
{
|
||||
extraProperties.Add(new AuthEventProperty
|
||||
{
|
||||
Name = "sender.mtls.x5t",
|
||||
Value = ClassifiedString.Sensitive(auditMtlsThumbprint)
|
||||
});
|
||||
}
|
||||
|
||||
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.MtlsCertificateHexProperty, out var auditMtlsHexObj) &&
|
||||
auditMtlsHexObj is string auditMtlsHex &&
|
||||
!string.IsNullOrWhiteSpace(auditMtlsHex))
|
||||
{
|
||||
extraProperties.Add(new AuthEventProperty
|
||||
{
|
||||
Name = "sender.mtls.x5t_hex",
|
||||
Value = ClassifiedString.Sensitive(auditMtlsHex)
|
||||
});
|
||||
}
|
||||
|
||||
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.OperatorReasonProperty, out var operatorReasonObj) &&
|
||||
operatorReasonObj is string operatorReason &&
|
||||
!string.IsNullOrWhiteSpace(operatorReason))
|
||||
@@ -1873,6 +1917,13 @@ internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler<
|
||||
record.SenderKeyThumbprint = senderThumbprint;
|
||||
}
|
||||
|
||||
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.MtlsCertificateHexProperty, out var senderCertHexObj) &&
|
||||
senderCertHexObj is string senderCertHex &&
|
||||
!string.IsNullOrWhiteSpace(senderCertHex))
|
||||
{
|
||||
record.SenderCertificateHex = senderCertHex;
|
||||
}
|
||||
|
||||
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.ClientTenantProperty, out var tenantObj) &&
|
||||
tenantObj is string tenantValue &&
|
||||
!string.IsNullOrWhiteSpace(tenantValue))
|
||||
@@ -1976,6 +2027,13 @@ internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler<
|
||||
identity.SetClaim(AuthorityOpenIddictConstants.ConfirmationClaimType, confirmation);
|
||||
}
|
||||
|
||||
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.DpopConsumedNonceProperty, out var nonceObj) &&
|
||||
nonceObj is string consumedNonce &&
|
||||
!string.IsNullOrWhiteSpace(consumedNonce))
|
||||
{
|
||||
identity.SetClaim(AuthorityOpenIddictConstants.SenderNonceClaimType, consumedNonce);
|
||||
}
|
||||
|
||||
break;
|
||||
case AuthoritySenderConstraintKinds.Mtls:
|
||||
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.MtlsCertificateThumbprintProperty, out var mtlsThumbprintObj) &&
|
||||
@@ -1990,6 +2048,13 @@ internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler<
|
||||
identity.SetClaim(AuthorityOpenIddictConstants.ConfirmationClaimType, confirmation);
|
||||
}
|
||||
|
||||
if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.MtlsCertificateHexProperty, out var mtlsHexObj) &&
|
||||
mtlsHexObj is string mtlsHex &&
|
||||
!string.IsNullOrWhiteSpace(mtlsHex))
|
||||
{
|
||||
identity.SetClaim(AuthorityOpenIddictConstants.MtlsCertificateHexClaimType, mtlsHex);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,13 @@ using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace StellaOps.Authority.OpenIddict.Handlers;
|
||||
|
||||
internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenIddictServerEvents.ValidateTokenRequestContext>
|
||||
{
|
||||
private readonly StellaOpsAuthorityOptions authorityOptions;
|
||||
private readonly IAuthorityClientStore clientStore;
|
||||
private readonly IDpopProofValidator proofValidator;
|
||||
internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenIddictServerEvents.ValidateTokenRequestContext>
|
||||
{
|
||||
private const string AnyDpopKeyThumbprint = "__authority_any_dpop_key__";
|
||||
|
||||
private readonly StellaOpsAuthorityOptions authorityOptions;
|
||||
private readonly IAuthorityClientStore clientStore;
|
||||
private readonly IDpopProofValidator proofValidator;
|
||||
private readonly IDpopNonceStore nonceStore;
|
||||
private readonly IAuthorityRateLimiterMetadataAccessor metadataAccessor;
|
||||
private readonly IAuthEventSink auditSink;
|
||||
@@ -88,15 +90,34 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
|
||||
return;
|
||||
}
|
||||
|
||||
var senderConstraint = NormalizeSenderConstraint(clientDocument);
|
||||
context.Transaction.Properties[AuthorityOpenIddictConstants.ClientSenderConstraintProperty] = senderConstraint;
|
||||
|
||||
if (!string.Equals(senderConstraint, AuthoritySenderConstraintKinds.Dpop, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var configuredAudiences = EnsureRequestAudiences(context.Request, clientDocument);
|
||||
var senderConstraint = NormalizeSenderConstraint(clientDocument);
|
||||
var configuredAudiences = EnsureRequestAudiences(context.Request, clientDocument);
|
||||
var nonceOptions = senderConstraintOptions.Dpop.Nonce;
|
||||
|
||||
string? matchedNonceAudience = null;
|
||||
if (senderConstraintOptions.Dpop.Enabled && nonceOptions.Enabled)
|
||||
{
|
||||
matchedNonceAudience = ResolveNonceAudience(context.Request, nonceOptions, configuredAudiences);
|
||||
}
|
||||
|
||||
var requiresClientSenderConstraint = string.Equals(senderConstraint, AuthoritySenderConstraintKinds.Dpop, StringComparison.Ordinal);
|
||||
var requiresConfiguredAudience = senderConstraintOptions.Dpop.Enabled && matchedNonceAudience is not null;
|
||||
|
||||
var effectiveSenderConstraint = requiresClientSenderConstraint || requiresConfiguredAudience
|
||||
? AuthoritySenderConstraintKinds.Dpop
|
||||
: senderConstraint;
|
||||
|
||||
context.Transaction.Properties[AuthorityOpenIddictConstants.ClientSenderConstraintProperty] = effectiveSenderConstraint;
|
||||
|
||||
if (!requiresClientSenderConstraint && !requiresConfiguredAudience)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (requiresConfiguredAudience && !requiresClientSenderConstraint)
|
||||
{
|
||||
logger.LogDebug("DPoP enforcement enabled for client {ClientId} targeting audience {Audience}.", clientId, matchedNonceAudience);
|
||||
}
|
||||
|
||||
if (!senderConstraintOptions.Dpop.Enabled)
|
||||
{
|
||||
@@ -125,18 +146,18 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
|
||||
return;
|
||||
}
|
||||
|
||||
if (!httpRequest.Headers.TryGetValue("DPoP", out StringValues proofHeader) || StringValues.IsNullOrEmpty(proofHeader))
|
||||
{
|
||||
logger.LogWarning("Missing DPoP header for client credentials request from {ClientId}.", clientId);
|
||||
await ChallengeNonceAsync(
|
||||
context,
|
||||
clientDocument,
|
||||
audience: null,
|
||||
thumbprint: null,
|
||||
reasonCode: "missing_proof",
|
||||
description: "DPoP proof is required.",
|
||||
senderConstraintOptions,
|
||||
httpResponse).ConfigureAwait(false);
|
||||
if (!httpRequest.Headers.TryGetValue("DPoP", out StringValues proofHeader) || StringValues.IsNullOrEmpty(proofHeader))
|
||||
{
|
||||
logger.LogWarning("Missing DPoP header for client credentials request from {ClientId}.", clientId);
|
||||
await ChallengeNonceAsync(
|
||||
context,
|
||||
clientDocument,
|
||||
audience: matchedNonceAudience,
|
||||
thumbprint: null,
|
||||
reasonCode: "missing_proof",
|
||||
description: "DPoP proof is required.",
|
||||
senderConstraintOptions,
|
||||
httpResponse).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -150,36 +171,36 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
|
||||
cancellationToken: context.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
var error = string.IsNullOrWhiteSpace(validationResult.ErrorDescription)
|
||||
? "DPoP proof validation failed."
|
||||
: validationResult.ErrorDescription;
|
||||
{
|
||||
var error = string.IsNullOrWhiteSpace(validationResult.ErrorDescription)
|
||||
? "DPoP proof validation failed."
|
||||
: validationResult.ErrorDescription;
|
||||
|
||||
logger.LogWarning("DPoP proof validation failed for client {ClientId}: {Reason}.", clientId, error);
|
||||
await ChallengeNonceAsync(
|
||||
context,
|
||||
clientDocument,
|
||||
audience: matchedNonceAudience,
|
||||
thumbprint: null,
|
||||
reasonCode: validationResult.ErrorCode ?? "invalid_proof",
|
||||
description: error,
|
||||
senderConstraintOptions,
|
||||
httpResponse).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogWarning("DPoP proof validation failed for client {ClientId}: {Reason}.", clientId, error);
|
||||
await ChallengeNonceAsync(
|
||||
context,
|
||||
clientDocument,
|
||||
audience: null,
|
||||
thumbprint: null,
|
||||
reasonCode: validationResult.ErrorCode ?? "invalid_proof",
|
||||
description: error,
|
||||
senderConstraintOptions,
|
||||
httpResponse).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (validationResult.PublicKey is not Microsoft.IdentityModel.Tokens.JsonWebKey jwk)
|
||||
{
|
||||
logger.LogWarning("DPoP proof for {ClientId} did not expose a JSON Web Key.", clientId);
|
||||
await ChallengeNonceAsync(
|
||||
context,
|
||||
clientDocument,
|
||||
audience: null,
|
||||
thumbprint: null,
|
||||
reasonCode: "invalid_key",
|
||||
description: "DPoP proof must embed a JSON Web Key.",
|
||||
senderConstraintOptions,
|
||||
httpResponse).ConfigureAwait(false);
|
||||
if (validationResult.PublicKey is not Microsoft.IdentityModel.Tokens.JsonWebKey jwk)
|
||||
{
|
||||
logger.LogWarning("DPoP proof for {ClientId} did not expose a JSON Web Key.", clientId);
|
||||
await ChallengeNonceAsync(
|
||||
context,
|
||||
clientDocument,
|
||||
audience: matchedNonceAudience,
|
||||
thumbprint: null,
|
||||
reasonCode: "invalid_key",
|
||||
description: "DPoP proof must embed a JSON Web Key.",
|
||||
senderConstraintOptions,
|
||||
httpResponse).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -209,8 +230,7 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
|
||||
context.Transaction.Properties[AuthorityOpenIddictConstants.DpopIssuedAtProperty] = issuedAt;
|
||||
}
|
||||
|
||||
var nonceOptions = senderConstraintOptions.Dpop.Nonce;
|
||||
var requiredAudience = ResolveNonceAudience(context.Request, nonceOptions, configuredAudiences);
|
||||
var requiredAudience = matchedNonceAudience;
|
||||
|
||||
if (nonceOptions.Enabled && requiredAudience is not null)
|
||||
{
|
||||
@@ -232,12 +252,12 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
|
||||
return;
|
||||
}
|
||||
|
||||
var consumeResult = await nonceStore.TryConsumeAsync(
|
||||
suppliedNonce,
|
||||
requiredAudience,
|
||||
clientDocument.ClientId,
|
||||
thumbprint,
|
||||
context.CancellationToken).ConfigureAwait(false);
|
||||
var consumeResult = await ConsumeNonceAsync(
|
||||
suppliedNonce,
|
||||
requiredAudience,
|
||||
clientDocument,
|
||||
thumbprint,
|
||||
context.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
switch (consumeResult.Status)
|
||||
{
|
||||
@@ -442,11 +462,11 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
|
||||
return null;
|
||||
}
|
||||
|
||||
private async ValueTask ChallengeNonceAsync(
|
||||
OpenIddictServerEvents.ValidateTokenRequestContext context,
|
||||
AuthorityClientDocument clientDocument,
|
||||
string? audience,
|
||||
string? thumbprint,
|
||||
private async ValueTask ChallengeNonceAsync(
|
||||
OpenIddictServerEvents.ValidateTokenRequestContext context,
|
||||
AuthorityClientDocument clientDocument,
|
||||
string? audience,
|
||||
string? thumbprint,
|
||||
string reasonCode,
|
||||
string description,
|
||||
AuthoritySenderConstraintOptions senderConstraintOptions,
|
||||
@@ -455,20 +475,25 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
|
||||
context.Reject(OpenIddictConstants.Errors.InvalidClient, description);
|
||||
metadataAccessor.SetTag("authority.dpop_result", reasonCode);
|
||||
|
||||
string? issuedNonce = null;
|
||||
DateTimeOffset? expiresAt = null;
|
||||
if (audience is not null && thumbprint is not null && senderConstraintOptions.Dpop.Nonce.Enabled)
|
||||
{
|
||||
var issuance = await nonceStore.IssueAsync(
|
||||
audience,
|
||||
clientDocument.ClientId,
|
||||
thumbprint,
|
||||
senderConstraintOptions.Dpop.Nonce.Ttl,
|
||||
senderConstraintOptions.Dpop.Nonce.MaxIssuancePerMinute,
|
||||
context.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (issuance.Status == DpopNonceIssueStatus.Success)
|
||||
{
|
||||
string? issuedNonce = null;
|
||||
DateTimeOffset? expiresAt = null;
|
||||
var nonceOptions = senderConstraintOptions.Dpop.Nonce;
|
||||
if (audience is not null && nonceOptions.Enabled)
|
||||
{
|
||||
var issuanceThumbprint = string.IsNullOrWhiteSpace(thumbprint)
|
||||
? AnyDpopKeyThumbprint
|
||||
: thumbprint;
|
||||
|
||||
var issuance = await nonceStore.IssueAsync(
|
||||
audience,
|
||||
clientDocument.ClientId,
|
||||
issuanceThumbprint,
|
||||
nonceOptions.Ttl,
|
||||
nonceOptions.MaxIssuancePerMinute,
|
||||
context.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (issuance.Status == DpopNonceIssueStatus.Success)
|
||||
{
|
||||
issuedNonce = issuance.Nonce;
|
||||
expiresAt = issuance.ExpiresAt;
|
||||
}
|
||||
@@ -488,20 +513,48 @@ internal sealed class ValidateDpopProofHandler : IOpenIddictServerHandler<OpenId
|
||||
}
|
||||
}
|
||||
|
||||
await WriteAuditAsync(
|
||||
context,
|
||||
clientDocument,
|
||||
AuthEventOutcome.Failure,
|
||||
description,
|
||||
thumbprint,
|
||||
validationResult: null,
|
||||
audience,
|
||||
"authority.dpop.proof.challenge",
|
||||
reasonCode,
|
||||
issuedNonce,
|
||||
expiresAt)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
await WriteAuditAsync(
|
||||
context,
|
||||
clientDocument,
|
||||
AuthEventOutcome.Failure,
|
||||
description,
|
||||
thumbprint,
|
||||
validationResult: null,
|
||||
audience,
|
||||
"authority.dpop.proof.challenge",
|
||||
reasonCode,
|
||||
issuedNonce,
|
||||
expiresAt)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask<DpopNonceConsumeResult> ConsumeNonceAsync(
|
||||
string nonce,
|
||||
string audience,
|
||||
AuthorityClientDocument clientDocument,
|
||||
string keyThumbprint,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await nonceStore.TryConsumeAsync(
|
||||
nonce,
|
||||
audience,
|
||||
clientDocument.ClientId,
|
||||
keyThumbprint,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result.Status == DpopNonceConsumeStatus.NotFound &&
|
||||
!string.Equals(keyThumbprint, AnyDpopKeyThumbprint, StringComparison.Ordinal))
|
||||
{
|
||||
result = await nonceStore.TryConsumeAsync(
|
||||
nonce,
|
||||
audience,
|
||||
clientDocument.ClientId,
|
||||
AnyDpopKeyThumbprint,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string BuildAuthenticateHeader(string reasonCode, string description, string? nonce)
|
||||
{
|
||||
|
||||
@@ -117,6 +117,18 @@ internal sealed class PersistTokensHandler : IOpenIddictServerHandler<OpenIddict
|
||||
document.SenderConstraint = senderConstraint;
|
||||
}
|
||||
|
||||
var senderNonce = principal.GetClaim(AuthorityOpenIddictConstants.SenderNonceClaimType);
|
||||
if (!string.IsNullOrWhiteSpace(senderNonce))
|
||||
{
|
||||
document.SenderNonce = senderNonce;
|
||||
}
|
||||
|
||||
var senderCertificateHex = principal.GetClaim(AuthorityOpenIddictConstants.MtlsCertificateHexClaimType);
|
||||
if (!string.IsNullOrWhiteSpace(senderCertificateHex))
|
||||
{
|
||||
document.SenderCertificateHex = senderCertificateHex;
|
||||
}
|
||||
|
||||
var serviceAccountId = principal.GetClaim(StellaOpsClaimTypes.ServiceAccount);
|
||||
if (!string.IsNullOrWhiteSpace(serviceAccountId))
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,7 @@ using StellaOps.Authority.Notifications.Ack;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
using StellaOps.Authority.Plugins;
|
||||
using StellaOps.Authority.Bootstrap;
|
||||
using StellaOps.Authority.Console;
|
||||
using StellaOps.Authority.Storage.Mongo.Extensions;
|
||||
using StellaOps.Authority.Storage.Mongo.Initialization;
|
||||
using StellaOps.Authority.Storage.Mongo.Stores;
|
||||
@@ -115,6 +116,8 @@ builder.Host.UseSerilog((context, _, loggerConfiguration) =>
|
||||
});
|
||||
|
||||
var authorityOptions = authorityConfiguration.Options;
|
||||
builder.Services.AddStellaOpsCrypto(authorityOptions.Crypto);
|
||||
builder.Services.AddHostedService<AuthoritySecretHasherInitializer>();
|
||||
var issuerUri = authorityOptions.Issuer;
|
||||
if (issuerUri is null)
|
||||
{
|
||||
@@ -138,6 +141,7 @@ builder.Services.TryAddSingleton<IAuthorityRateLimiterPartitionKeyResolver, Defa
|
||||
builder.Services.AddSingleton<IAuthorityClientCertificateValidator, AuthorityClientCertificateValidator>();
|
||||
builder.Services.TryAddSingleton<IAuthorityAirgapAuditService, AuthorityAirgapAuditService>();
|
||||
builder.Services.AddSingleton<AuthorityOpenApiDocumentProvider>();
|
||||
builder.Services.TryAddSingleton<IConsoleWorkspaceService, ConsoleWorkspaceSampleService>();
|
||||
|
||||
#if STELLAOPS_AUTH_SECURITY
|
||||
var senderConstraints = authorityOptions.Security.SenderConstraints;
|
||||
@@ -210,7 +214,6 @@ if (requiresKms)
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthoritySigningKeySource, KmsAuthoritySigningKeySource>());
|
||||
}
|
||||
|
||||
builder.Services.AddStellaOpsCrypto();
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthoritySigningKeySource, FileAuthoritySigningKeySource>());
|
||||
builder.Services.AddSingleton<AuthoritySigningKeyManager>();
|
||||
builder.Services.AddSingleton<AuthorityAckTokenKeyManager>();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Authority.Security;
|
||||
|
||||
internal sealed class AuthoritySecretHasherInitializer : IHostedService
|
||||
{
|
||||
private readonly ICryptoHash hash;
|
||||
private readonly IOptions<StellaOpsAuthorityOptions> authorityOptions;
|
||||
private readonly ILogger<AuthoritySecretHasherInitializer> logger;
|
||||
|
||||
public AuthoritySecretHasherInitializer(
|
||||
ICryptoHash hash,
|
||||
IOptions<StellaOpsAuthorityOptions> authorityOptions,
|
||||
ILogger<AuthoritySecretHasherInitializer> logger)
|
||||
{
|
||||
this.hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||
this.authorityOptions = authorityOptions ?? throw new ArgumentNullException(nameof(authorityOptions));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var options = authorityOptions.Value;
|
||||
var algorithm = options?.Crypto?.DefaultHashAlgorithm;
|
||||
AuthoritySecretHasher.Configure(hash, algorithm);
|
||||
logger.LogInformation("Authority secret hasher configured with default algorithm {Algorithm}.",
|
||||
string.IsNullOrWhiteSpace(algorithm) ? HashAlgorithms.Sha256 : algorithm);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -1,181 +1,183 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Authority.Signing;
|
||||
|
||||
internal sealed class AuthorityJwksService
|
||||
{
|
||||
private const string CacheKey = "authority:jwks:current";
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly ICryptoProviderRegistry registry;
|
||||
private readonly ILogger<AuthorityJwksService> logger;
|
||||
private readonly IMemoryCache cache;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly StellaOpsAuthorityOptions authorityOptions;
|
||||
|
||||
public AuthorityJwksService(
|
||||
ICryptoProviderRegistry registry,
|
||||
ILogger<AuthorityJwksService> logger,
|
||||
IMemoryCache cache,
|
||||
TimeProvider timeProvider,
|
||||
IOptions<StellaOpsAuthorityOptions> authorityOptions)
|
||||
{
|
||||
this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
if (authorityOptions is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(authorityOptions));
|
||||
}
|
||||
|
||||
this.authorityOptions = authorityOptions.Value ?? throw new ArgumentNullException(nameof(authorityOptions));
|
||||
}
|
||||
|
||||
public AuthorityJwksResult Get()
|
||||
{
|
||||
if (cache.TryGetValue(CacheKey, out AuthorityJwksCacheEntry? cached) &&
|
||||
cached is not null &&
|
||||
cached.ExpiresAt > timeProvider.GetUtcNow())
|
||||
{
|
||||
return cached.Result;
|
||||
}
|
||||
|
||||
var response = new AuthorityJwksResponse(BuildKeys());
|
||||
var signingOptions = authorityOptions.Signing;
|
||||
var lifetime = signingOptions.JwksCacheLifetime > TimeSpan.Zero
|
||||
? signingOptions.JwksCacheLifetime
|
||||
: TimeSpan.FromMinutes(5);
|
||||
var expires = timeProvider.GetUtcNow().Add(lifetime);
|
||||
var etag = ComputeEtag(response, expires);
|
||||
var cacheControl = $"public, max-age={(int)lifetime.TotalSeconds}";
|
||||
|
||||
var result = new AuthorityJwksResult(response, etag, expires, cacheControl);
|
||||
var entry = new AuthorityJwksCacheEntry(result, expires);
|
||||
|
||||
cache.Set(CacheKey, entry, new MemoryCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = lifetime
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Invalidate()
|
||||
{
|
||||
cache.Remove(CacheKey);
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<JwksKeyEntry> BuildKeys()
|
||||
{
|
||||
var keys = new List<JwksKeyEntry>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var provider in registry.Providers)
|
||||
{
|
||||
foreach (var signingKey in provider.GetSigningKeys())
|
||||
{
|
||||
var keyId = signingKey.Reference.KeyId;
|
||||
if (!seen.Add(keyId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var signer = provider.GetSigner(signingKey.AlgorithmId, signingKey.Reference);
|
||||
var jwk = signer.ExportPublicJsonWebKey();
|
||||
var keyUse = signingKey.Metadata.TryGetValue("use", out var metadataUse) && !string.IsNullOrWhiteSpace(metadataUse)
|
||||
? metadataUse
|
||||
: jwk.Use;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(keyUse))
|
||||
{
|
||||
keyUse = "sig";
|
||||
}
|
||||
|
||||
var entry = new JwksKeyEntry
|
||||
{
|
||||
Kid = jwk.Kid,
|
||||
Kty = jwk.Kty,
|
||||
Use = keyUse,
|
||||
Alg = jwk.Alg,
|
||||
Crv = jwk.Crv,
|
||||
X = jwk.X,
|
||||
Y = jwk.Y,
|
||||
Status = signingKey.Metadata.TryGetValue("status", out var status) ? status : "active"
|
||||
};
|
||||
keys.Add(entry);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to export JWKS entry for key {KeyId}.", keyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keys.Sort(static (left, right) => string.Compare(left.Kid, right.Kid, StringComparison.Ordinal));
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static string ComputeEtag(AuthorityJwksResponse response, DateTimeOffset expiresAt)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(response, SerializerOptions);
|
||||
var buffer = Encoding.UTF8.GetBytes(payload + "|" + expiresAt.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture));
|
||||
var hash = SHA256.HashData(buffer);
|
||||
return $"\"{Convert.ToHexString(hash)}\"";
|
||||
}
|
||||
|
||||
private sealed record AuthorityJwksCacheEntry(AuthorityJwksResult Result, DateTimeOffset ExpiresAt);
|
||||
}
|
||||
|
||||
internal sealed record AuthorityJwksResponse([property: JsonPropertyName("keys")] IReadOnlyCollection<JwksKeyEntry> Keys);
|
||||
|
||||
internal sealed record AuthorityJwksResult(
|
||||
AuthorityJwksResponse Response,
|
||||
string ETag,
|
||||
DateTimeOffset ExpiresAt,
|
||||
string CacheControl);
|
||||
|
||||
internal sealed class JwksKeyEntry
|
||||
{
|
||||
[JsonPropertyName("kty")]
|
||||
public string? Kty { get; set; }
|
||||
|
||||
[JsonPropertyName("use")]
|
||||
public string? Use { get; set; }
|
||||
|
||||
[JsonPropertyName("kid")]
|
||||
public string? Kid { get; set; }
|
||||
|
||||
[JsonPropertyName("alg")]
|
||||
public string? Alg { get; set; }
|
||||
|
||||
[JsonPropertyName("crv")]
|
||||
public string? Crv { get; set; }
|
||||
|
||||
[JsonPropertyName("x")]
|
||||
public string? X { get; set; }
|
||||
|
||||
[JsonPropertyName("y")]
|
||||
public string? Y { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Status { get; set; }
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Authority.Signing;
|
||||
|
||||
internal sealed class AuthorityJwksService
|
||||
{
|
||||
private const string CacheKey = "authority:jwks:current";
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly ICryptoProviderRegistry registry;
|
||||
private readonly ICryptoHash hash;
|
||||
private readonly ILogger<AuthorityJwksService> logger;
|
||||
private readonly IMemoryCache cache;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly StellaOpsAuthorityOptions authorityOptions;
|
||||
|
||||
public AuthorityJwksService(
|
||||
ICryptoProviderRegistry registry,
|
||||
ICryptoHash hash,
|
||||
ILogger<AuthorityJwksService> logger,
|
||||
IMemoryCache cache,
|
||||
TimeProvider timeProvider,
|
||||
IOptions<StellaOpsAuthorityOptions> authorityOptions)
|
||||
{
|
||||
this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
|
||||
this.hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
if (authorityOptions is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(authorityOptions));
|
||||
}
|
||||
|
||||
this.authorityOptions = authorityOptions.Value ?? throw new ArgumentNullException(nameof(authorityOptions));
|
||||
}
|
||||
|
||||
public AuthorityJwksResult Get()
|
||||
{
|
||||
if (cache.TryGetValue(CacheKey, out AuthorityJwksCacheEntry? cached) &&
|
||||
cached is not null &&
|
||||
cached.ExpiresAt > timeProvider.GetUtcNow())
|
||||
{
|
||||
return cached.Result;
|
||||
}
|
||||
|
||||
var response = new AuthorityJwksResponse(BuildKeys());
|
||||
var signingOptions = authorityOptions.Signing;
|
||||
var lifetime = signingOptions.JwksCacheLifetime > TimeSpan.Zero
|
||||
? signingOptions.JwksCacheLifetime
|
||||
: TimeSpan.FromMinutes(5);
|
||||
var expires = timeProvider.GetUtcNow().Add(lifetime);
|
||||
var etag = ComputeEtag(response, expires);
|
||||
var cacheControl = $"public, max-age={(int)lifetime.TotalSeconds}";
|
||||
|
||||
var result = new AuthorityJwksResult(response, etag, expires, cacheControl);
|
||||
var entry = new AuthorityJwksCacheEntry(result, expires);
|
||||
|
||||
cache.Set(CacheKey, entry, new MemoryCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = lifetime
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Invalidate()
|
||||
{
|
||||
cache.Remove(CacheKey);
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<JwksKeyEntry> BuildKeys()
|
||||
{
|
||||
var keys = new List<JwksKeyEntry>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var provider in registry.Providers)
|
||||
{
|
||||
foreach (var signingKey in provider.GetSigningKeys())
|
||||
{
|
||||
var keyId = signingKey.Reference.KeyId;
|
||||
if (!seen.Add(keyId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var signer = provider.GetSigner(signingKey.AlgorithmId, signingKey.Reference);
|
||||
var jwk = signer.ExportPublicJsonWebKey();
|
||||
var keyUse = signingKey.Metadata.TryGetValue("use", out var metadataUse) && !string.IsNullOrWhiteSpace(metadataUse)
|
||||
? metadataUse
|
||||
: jwk.Use;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(keyUse))
|
||||
{
|
||||
keyUse = "sig";
|
||||
}
|
||||
|
||||
var entry = new JwksKeyEntry
|
||||
{
|
||||
Kid = jwk.Kid,
|
||||
Kty = jwk.Kty,
|
||||
Use = keyUse,
|
||||
Alg = jwk.Alg,
|
||||
Crv = jwk.Crv,
|
||||
X = jwk.X,
|
||||
Y = jwk.Y,
|
||||
Status = signingKey.Metadata.TryGetValue("status", out var status) ? status : "active"
|
||||
};
|
||||
keys.Add(entry);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to export JWKS entry for key {KeyId}.", keyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keys.Sort(static (left, right) => string.Compare(left.Kid, right.Kid, StringComparison.Ordinal));
|
||||
return keys;
|
||||
}
|
||||
|
||||
private string ComputeEtag(AuthorityJwksResponse response, DateTimeOffset expiresAt)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(response, SerializerOptions);
|
||||
var buffer = Encoding.UTF8.GetBytes(payload + "|" + expiresAt.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture));
|
||||
var digest = hash.ComputeHash(buffer, HashAlgorithms.Sha256);
|
||||
return $"\"{Convert.ToHexString(digest)}\"";
|
||||
}
|
||||
|
||||
private sealed record AuthorityJwksCacheEntry(AuthorityJwksResult Result, DateTimeOffset ExpiresAt);
|
||||
}
|
||||
|
||||
internal sealed record AuthorityJwksResponse([property: JsonPropertyName("keys")] IReadOnlyCollection<JwksKeyEntry> Keys);
|
||||
|
||||
internal sealed record AuthorityJwksResult(
|
||||
AuthorityJwksResponse Response,
|
||||
string ETag,
|
||||
DateTimeOffset ExpiresAt,
|
||||
string CacheControl);
|
||||
|
||||
internal sealed class JwksKeyEntry
|
||||
{
|
||||
[JsonPropertyName("kty")]
|
||||
public string? Kty { get; set; }
|
||||
|
||||
[JsonPropertyName("use")]
|
||||
public string? Use { get; set; }
|
||||
|
||||
[JsonPropertyName("kid")]
|
||||
public string? Kid { get; set; }
|
||||
|
||||
[JsonPropertyName("alg")]
|
||||
public string? Alg { get; set; }
|
||||
|
||||
[JsonPropertyName("crv")]
|
||||
public string? Crv { get; set; }
|
||||
|
||||
[JsonPropertyName("x")]
|
||||
public string? X { get; set; }
|
||||
|
||||
[JsonPropertyName("y")]
|
||||
public string? Y { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Status { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,170 +1,182 @@
|
||||
# Authority Host Task Board — Epic 1: Aggregation-Only Contract
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-26: Rate limiter metadata/audit records now include tenants, password grant scopes/tenants enforced, token persistence + tests updated. Docs refresh tracked via AUTH-AOC-19-003.
|
||||
> 2025-10-27: Client credential ingestion scopes now require tenant assignment; access token validation backfills tenants and rejects cross-tenant mismatches with tests.
|
||||
> 2025-10-27: `dotnet test` blocked — Concelier build fails (`AdvisoryObservationQueryService` returns `ImmutableHashSet<string?>`), preventing Authority test suite run; waiting on Concelier fix before rerun.
|
||||
> 2025-10-26: Docs updated (`docs/11_AUTHORITY.md`, Concelier audit runbook, `docs/security/authority-scopes.md`); sample config highlights tenant-aware clients. Release notes + smoke verification pending (blocked on Concelier/Excititor smoke updates).
|
||||
> 2025-10-27: Scope catalogue aligned with `advisory:ingest/advisory:read/vex:ingest/vex:read`, `aoc:verify` pairing documented, console/CLI references refreshed, and `etc/authority.yaml.sample` updated to require read scopes for verification clients.
|
||||
> 2025-10-31: Client credentials and password grants now reject advisory/vex read or signals scopes without `aoc:verify`, enforce tenant assignment for `aoc:verify`, tag violations via `authority.aoc_scope_violation`, extend tests, and refresh scope catalogue docs/sample roles.
|
||||
|
||||
## Link-Not-Merge v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-29: Rejected legacy `concelier.merge` scope during client credential validation, removed it from known scope catalog, blocked discovery/issuance, added regression tests, and refreshed scope documentation.
|
||||
|
||||
## Policy Engine v2
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-26: Restricted `effective:write` to Policy Engine service identities with tenant requirement, registered full scope set, and tightened resource server default scope enforcement (unit tests pass).
|
||||
> 2025-10-26: Authority docs now detail policy scopes/service identity guardrails with checklist; `authority.yaml.sample` includes `properties.serviceIdentity` example.
|
||||
|
||||
## Graph Explorer v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
|
||||
## Policy Engine + Editor v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-POLICY-23-002 | BLOCKED (2025-10-29) | Authority Core & Security Guild | AUTH-POLICY-23-001 | Implement optional two-person rule for activation: require two distinct `policy:activate` approvals when configured; emit audit logs. | Activation endpoint enforces rule; audit logs contain approver IDs; tests cover 2-person path. |
|
||||
> Blocked: Policy Engine/Studio have not yet exposed activation workflow endpoints or approval payloads needed to enforce dual-control (`WEB-POLICY-23-002`, `POLICY-ENGINE-23-002`). Revisit once activation contract lands.
|
||||
| AUTH-POLICY-23-003 | BLOCKED (2025-10-29) | Authority Core & Docs Guild | AUTH-POLICY-23-001 | Update documentation and sample configs for policy roles, approval workflow, and signing requirements. | Docs updated with reviewer checklist; configuration examples validated. |
|
||||
> Blocked pending AUTH-POLICY-23-002 dual-approval implementation so docs can capture final activation behaviour.
|
||||
> 2025-10-27: Added `policy-cli` defaults to Authority config/secrets, refreshed CLI/CI documentation with the new scope bundle, recorded release migration guidance, and introduced `scripts/verify-policy-scopes.py` to guard against regressions.
|
||||
|
||||
## Graph & Vuln Explorer v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-27: Paused work after exploratory spike (scope enforcement still outstanding); no functional changes merged.
|
||||
|
||||
## Orchestrator Dashboard
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-31: Picked up during Console/Orchestrator alignment; focusing on scope catalog + tenant enforcement first.
|
||||
> 2025-10-31: `orch:read` added to scope catalogue and Authority runtime, Console defaults include the scope, `Orch.Viewer` role documented, and client-credential tests enforce tenant requirements.
|
||||
> 2025-10-27: Added `orch:operate` scope, enforced `operator_reason`/`operator_ticket` on token issuance, updated Authority configs/docs, and captured audit metadata for control actions.
|
||||
> 2025-10-28: Policy gateway + scanner now pass the expanded token client signature (`null` metadata by default), test stubs capture the optional parameters, and Policy Gateway/Scanner suites are green after fixing the Concelier storage build break.
|
||||
> 2025-10-28: Authority password-grant tests now hit the new constructors but still need updates to drop obsolete `IOptions` arguments before the suite can pass.
|
||||
| AUTH-ORCH-34-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-ORCH-33-001 | Introduce `Orch.Admin` role with quota/backfill scopes, enforce audit reason on quota changes, and update offline defaults/docs. | Admin role available; quotas/backfills require scope + reason; tests confirm tenant isolation; documentation updated. |
|
||||
> 2025-11-02: `orch:backfill` scope added with mandatory `backfill_reason`/`backfill_ticket`, client-credential validation and resource authorization paths emit audit fields, CLI picks up new configuration/env vars, and Authority docs/config samples updated for `Orch.Admin`.
|
||||
|
||||
## StellaOps Console (Sprint 23)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-29: Authorization code flow enabled with PKCE requirement, console client seeded in `authority.yaml.sample`, discovery docs updated, and console runbook guidance added.
|
||||
> 2025-10-31: Added `/console/tenants`, `/console/profile`, `/console/token/introspect` endpoints with tenant header filter, scope enforcement (`ui.read`, `authority:tenants.read`), and structured audit events. Console test harness covers success/mismatch cases.
|
||||
> 2025-10-28: `docs/security/console-security.md` drafted with PKCE + DPoP (120 s OpTok, 300 s fresh-auth) and scope table. Authority Core to confirm `/fresh-auth` semantics, token lifetimes, and scope bundles align before closing task.
|
||||
> 2025-10-31: Security guide expanded for `/console` endpoints & orchestrator scope, sample YAML annotated, ops runbook updated, and release note `docs/updates/2025-10-31-console-security-refresh.md` published.
|
||||
> 2025-10-31: Default access-token lifetime reduced to 120 s, console tests updated with dual auth schemes, docs/config/ops notes refreshed, release note logged.
|
||||
|
||||
## Policy Studio (Sprint 27)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-31: Added Policy Studio scope family (`policy:author/review/operate/audit`), updated OpenAPI + discovery headers, enforced tenant requirements in grant handlers, seeded new roles in Authority config/offline kit docs, and refreshed CLI/Console documentation + tests to validate the new catalogue.
|
||||
| AUTH-POLICY-27-002 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-POLICY-27-001, REGISTRY-API-27-007 | Provide attestation signing service bindings (OIDC token exchange, cosign integration) and enforce publish/promote scope checks, fresh-auth requirements, and audit logging. | Publish/promote requests require fresh auth + correct scopes; attestations signed with validated identity; audit logs enriched with digest + tenant; integration tests pass. |
|
||||
> Docs dependency: `DOCS-POLICY-27-009` awaiting signing guidance from this work.
|
||||
> 2025-11-02: Added `policy:publish`/`policy:promote` scopes with interactive-only enforcement, metadata parameters (`policy_reason`, `policy_ticket`, `policy_digest`), fresh-auth token validation, audit augmentations, and updated config/docs references.
|
||||
| AUTH-POLICY-27-003 | DONE (2025-11-04) | Authority Core & Docs Guild | AUTH-POLICY-27-001, AUTH-POLICY-27-002 | Update Authority configuration/docs for Policy Studio roles, signing policies, approval workflows, and CLI integration; include compliance checklist. | Docs merged; samples validated; governance checklist appended; release notes updated. |
|
||||
> 2025-11-04: Policy Studio roles/scopes documented across `docs/11_AUTHORITY.md`, sample configs, and OpenAPI; compliance checklist appended and Authority tests rerun to validate fresh-auth + scope enforcement.
|
||||
|
||||
## Exceptions v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-29: Added exception scopes + routing template options, enforced MFA requirement in password grant handlers, updated configuration samples.
|
||||
> 2025-10-31: Authority scopes/routing docs updated (`docs/security/authority-scopes.md`, `docs/11_AUTHORITY.md`, `docs/policy/exception-effects.md`), monitoring guide covers new MFA audit events, and `etc/authority.yaml.sample` now demonstrates exception clients/templates.
|
||||
|
||||
## Reachability v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-29: Signals scopes added with tenant + aoc:verify enforcement; sensors guided via SignalsUploader template; tests cover gating.
|
||||
|
||||
## Vulnerability Explorer (Sprint 29)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-VULN-29-001 | DONE (2025-11-03) | Authority Core & Security Guild | AUTH-POLICY-27-001 | Define Vuln Explorer scopes/roles (`vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit`) with ABAC attributes (env, owner, business_tier) and update discovery metadata/offline kit defaults. | Roles/scopes published; issuer templates updated; integration tests cover ABAC filters; docs refreshed. |
|
||||
| AUTH-VULN-29-002 | DONE (2025-11-03) | Authority Core & Security Guild | AUTH-VULN-29-001, LEDGER-29-002 | Enforce CSRF/anti-forgery tokens for workflow actions, sign attachment tokens, and record audit logs with ledger event hashes. | Workflow calls require valid tokens; audit logs include ledger references; security tests cover token expiry/abuse. |
|
||||
| AUTH-VULN-29-003 | DONE (2025-11-04) | Authority Core & Docs Guild | AUTH-VULN-29-001..002 | Update security docs/config samples for Vuln Explorer roles, ABAC policies, attachment signing, and ledger verification guidance. | Docs merged with compliance checklist; configuration examples validated; release notes updated. |
|
||||
> 2025-11-03: Vuln workflow CSRF + attachment token services live with audit enrichment and negative-path tests. Awaiting completion of full Authority suite run after repository-wide build finishes.
|
||||
> 2025-11-04: Verified Vuln Explorer RBAC/ABAC coverage in Authority docs/security guides, attachment token guidance, and offline samples; Authority tests rerun confirming ledger-token + anti-forgery behaviours.
|
||||
|
||||
## Advisory AI (Sprint 31)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-AIAI-31-001 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-VULN-29-001 | Define Advisory AI scopes (`advisory-ai:view`, `advisory-ai:operate`, `advisory-ai:admin`) and remote inference toggles; update discovery metadata/offline defaults. | Scopes/flags published; integration tests cover RBAC + opt-in settings; docs updated. |
|
||||
| AUTH-AIAI-31-002 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-AIAI-31-001, AIAI-31-006 | Enforce anonymized prompt logging, tenant consent for remote inference, and audit logging of assistant tasks. | Logging/audit flows verified; privacy review passed; docs updated. |
|
||||
|
||||
## Export Center
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
|
||||
## Notifications Studio
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-NOTIFY-38-001 | DONE (2025-11-01) | Authority Core & Security Guild | — | Define `Notify.Viewer`, `Notify.Operator`, `Notify.Admin` scopes/roles, update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit defaults refreshed. |
|
||||
| AUTH-NOTIFY-40-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-38-001, WEB-NOTIFY-40-001 | Implement signed ack token key rotation, webhook allowlists, admin-only escalation settings, and audit logging of ack actions. | Ack tokens signed/rotated; webhook allowlists enforced; admin enforcement validated; audit logs capture ack/resolution. |
|
||||
> 2025-11-02: `/notify/ack-tokens/rotate` exposed (notify.admin), emits `notify.ack.key_rotated|notify.ack.key_rotation_failed`, and DSSE rotation tests cover allowlist + scope enforcement.
|
||||
| AUTH-NOTIFY-42-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-40-001 | Investigate ack token rotation 500 errors (test Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure still failing). Capture logs, identify root cause, and patch handler. | Failure mode understood; fix merged; regression test passes. |
|
||||
> 2025-11-02: Aliased `StellaOpsBearer` to the test auth handler, corrected bootstrap `/notifications/ack-tokens/rotate` defaults, and validated `Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure` via targeted `dotnet test`.
|
||||
|
||||
|
||||
## CLI Parity & Task Packs
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-PACKS-41-001 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AOC-19-001 | Define CLI SSO profiles and pack scopes (`Packs.Read`, `Packs.Write`, `Packs.Run`, `Packs.Approve`), update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit templates refreshed. |
|
||||
> 2025-11-02: Added Pack scope policies, Authority role defaults, and CLI profile guidance covering operator/publisher/approver flows.
|
||||
> 2025-11-02: Shared OpenSSL 1.1 shim feeds Authority & Signals Mongo2Go harnesses so pack scope coverage keeps running on OpenSSL 3 hosts (AUTH-PACKS-41-001).
|
||||
> 2025-11-04: Discovery metadata/OpenAPI advertise packs scopes, configs/offline kit templates bundle new roles, and Authority tests re-run to validate tenant gating for `packs.*`.
|
||||
| AUTH-PACKS-43-001 | BLOCKED (2025-10-27) | Authority Core & Security Guild | AUTH-PACKS-41-001, TASKRUN-42-001, ORCH-SVC-42-101 | Enforce pack signing policies, approval RBAC checks, CLI CI token scopes, and audit logging for approvals. | Signing policies enforced; approvals require correct roles; CI token scope tests pass; audit logs recorded. |
|
||||
> Blocked: Task Runner approval APIs (`ORCH-SVC-42-101`, `TASKRUN-42-001`) still outstanding. Pack scope catalog (AUTH-PACKS-41-001) landed 2025-11-04; resume once execution/approval contracts are published.
|
||||
|
||||
## Authority-Backed Scopes & Tenancy (Epic 14)
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-28: Tidied advisory raw idempotency migration to avoid LINQ-on-`BsonValue` (explicit array copy) while continuing duplicate guardrail validation; scoped scanner/policy token call sites updated to honor new metadata parameter.
|
||||
| AUTH-TEN-49-001 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-TEN-47-001 | Implement service accounts & delegation tokens (`act` chain), per-tenant quotas, audit stream of auth decisions, and revocation APIs. | Service tokens minted with scopes/TTL; delegation logged; quotas configurable; audit stream live; docs updated. |
|
||||
> 2025-11-02: Authority bootstrap test harness now seeds service accounts via AuthorityDelegation options; `/internal/service-accounts` endpoints validated with targeted vstest run.
|
||||
> 2025-11-02: Added Mongo service-account store, seeded options/collection initializers, token persistence metadata (`tokenKind`, `serviceAccountId`, `actorChain`), and docs/config samples. Introduced quota checks + tests covering service account issuance and persistence.
|
||||
> 2025-11-02: Documented bootstrap service-account admin APIs in `docs/11_AUTHORITY.md`, noting API key requirements and stable upsert behaviour.
|
||||
> 2025-11-03: Seeded explicit enabled service-account fixtures for integration tests and reran `StellaOps.Authority.Tests` to greenlight `/internal/service-accounts` listing + revocation scenarios.
|
||||
> 2025-11-04: Confirmed service-account docs/config examples, quota tuning, and audit stream wiring; Authority suite re-executed to cover issuance/listing/revocation flows.
|
||||
|
||||
## Observability & Forensics (Epic 15)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-OBS-50-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-AOC-19-001 | Introduce scopes `obs:read`, `timeline:read`, `timeline:write`, `evidence:create`, `evidence:read`, `evidence:hold`, `attest:read`, and `obs:incident` (all tenant-scoped). Update discovery metadata, offline defaults, and scope grammar docs. | Scopes exposed via metadata; issuer templates updated; offline kit seeded; integration tests cover new scopes. |
|
||||
| AUTH-OBS-52-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-OBS-50-001, TIMELINE-OBS-52-003, EVID-OBS-53-003 | Configure resource server policies for Timeline Indexer, Evidence Locker, Exporter, and Observability APIs enforcing new scopes + tenant claims. Emit audit events including scope usage and trace IDs. | Policies deployed; unauthorized access blocked; audit logs prove scope usage; contract tests updated. |
|
||||
| AUTH-OBS-55-001 | DONE (2025-11-02) | Authority Core & Security Guild, Ops Guild | AUTH-OBS-50-001, WEB-OBS-55-001 | Harden incident mode authorization: require `obs:incident` scope + fresh auth, log activation reason, and expose verification endpoint for auditors. Update docs/runbooks. | Incident activate/deactivate requires scope; audit entries logged; docs updated with imposed rule reminder. |
|
||||
|
||||
## Air-Gapped Mode (Epic 16)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-AIRGAP-56-001 | DONE (2025-11-04) | Authority Core & Security Guild | AIRGAP-CTL-56-001 | Provision new scopes (`airgap:seal`, `airgap:import`, `airgap:status:read`) in configuration metadata, offline kit defaults, and issuer templates. | Scopes exposed in discovery docs; offline kit updated; integration tests cover issuance. |
|
||||
| AUTH-AIRGAP-56-002 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AIRGAP-56-001, AIRGAP-IMP-58-001 | Audit import actions with actor, tenant, bundle ID, and trace ID; expose `/authority/audit/airgap` endpoint. | Audit records persisted; endpoint paginates results; tests cover RBAC + filtering. |
|
||||
> 2025-11-04: Airgap scope constants are wired through discovery metadata, `etc/authority.yaml.sample`, and offline kit docs; scope issuance tests executed via `dotnet test`.
|
||||
> 2025-11-04: `/authority/audit/airgap` API persists tenant-scoped audit entries with pagination and authorization guards validated by the Authority integration suite (187 tests).
|
||||
| AUTH-AIRGAP-57-001 | BLOCKED (2025-11-01) | Authority Core & Security Guild, DevOps Guild | AUTH-AIRGAP-56-001, DEVOPS-AIRGAP-57-002 | Enforce sealed-mode CI gating by refusing token issuance when declared sealed install lacks sealing confirmation. | Awaiting clarified sealed-confirmation contract and configuration structure before implementation. |
|
||||
> 2025-11-01: AUTH-AIRGAP-57-001 blocked pending guidance on sealed-confirmation contract and configuration expectations before gating changes (Authority Core & Security Guild, DevOps Guild).
|
||||
|
||||
## SDKs & OpenAPI (Epic 17)
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-28: Auth OpenAPI authored at `src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml` covering `/token`, `/introspect`, `/revoke`, `/jwks`, scope catalog, and error envelopes; parsed via PyYAML sanity check and referenced in Epic 17 docs.
|
||||
> 2025-10-28: Added `/.well-known/openapi` endpoint wiring cached spec metadata, YAML/JSON negotiation, HTTP cache headers, and tests verifying ETag + Accept handling. Authority spec (`src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml`) now includes grant/scope extensions.
|
||||
| AUTH-OAS-62-001 | DONE (2025-11-02) | Authority Core & Security Guild, SDK Generator Guild | AUTH-OAS-61-001, SDKGEN-63-001 | Provide SDK helpers for OAuth2/PAT flows, tenancy override header; add integration tests. | SDKs expose auth helpers; tests cover token issuance; docs updated. |
|
||||
> 2025-11-02: `AddStellaOpsApiAuthentication` shipped (OAuth2 + PAT), tenant header injection added, and client tests updated for caching behaviour.
|
||||
| AUTH-OAS-63-001 | DONE (2025-11-02) | Authority Core & Security Guild, API Governance Guild | APIGOV-63-001 | Emit deprecation headers and notifications for legacy auth endpoints. | Headers emitted; notifications verified; migration guide published. |
|
||||
> 2025-11-02: AUTH-OAS-63-001 completed — legacy OAuth shims emit Deprecation/Sunset/Warning headers, audit events captured, and migration guide published (Authority Core & Security Guild, API Governance Guild).
|
||||
# Authority Host Task Board — Epic 1: Aggregation-Only Contract
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-26: Rate limiter metadata/audit records now include tenants, password grant scopes/tenants enforced, token persistence + tests updated. Docs refresh tracked via AUTH-AOC-19-003.
|
||||
> 2025-10-27: Client credential ingestion scopes now require tenant assignment; access token validation backfills tenants and rejects cross-tenant mismatches with tests.
|
||||
> 2025-10-27: `dotnet test` blocked — Concelier build fails (`AdvisoryObservationQueryService` returns `ImmutableHashSet<string?>`), preventing Authority test suite run; waiting on Concelier fix before rerun.
|
||||
> 2025-10-26: Docs updated (`docs/11_AUTHORITY.md`, Concelier audit runbook, `docs/security/authority-scopes.md`); sample config highlights tenant-aware clients. Release notes + smoke verification pending (blocked on Concelier/Excititor smoke updates).
|
||||
> 2025-10-27: Scope catalogue aligned with `advisory:ingest/advisory:read/vex:ingest/vex:read`, `aoc:verify` pairing documented, console/CLI references refreshed, and `etc/authority.yaml.sample` updated to require read scopes for verification clients.
|
||||
> 2025-10-31: Client credentials and password grants now reject advisory/vex read or signals scopes without `aoc:verify`, enforce tenant assignment for `aoc:verify`, tag violations via `authority.aoc_scope_violation`, extend tests, and refresh scope catalogue docs/sample roles.
|
||||
|
||||
| AUTH-CRYPTO-90-001 | DOING (2025-11-08) | Authority Core & Security Guild | SEC-CRYPTO-90-003, SEC-CRYPTO-90-004 | Migrate signing/key-loading paths (`KmsAuthoritySigningKeySource`, `FileAuthoritySigningKeySource`, `AuthorityJwksService`, secret hashers) to `ICryptoProviderRegistry` so regional bundles can pick `ru.cryptopro.csp` / `ru.pkcs11` providers as defined in `docs/security/crypto-routing-audit-2025-11-07.md`. | All signing + hashing code paths resolve registry providers; Authority config exposes provider selection; JWKS output references sovereign keys; regression tests updated. |
|
||||
|
||||
## Link-Not-Merge v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-29: Rejected legacy `concelier.merge` scope during client credential validation, removed it from known scope catalog, blocked discovery/issuance, added regression tests, and refreshed scope documentation.
|
||||
|
||||
## Policy Engine v2
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-26: Restricted `effective:write` to Policy Engine service identities with tenant requirement, registered full scope set, and tightened resource server default scope enforcement (unit tests pass).
|
||||
> 2025-10-26: Authority docs now detail policy scopes/service identity guardrails with checklist; `authority.yaml.sample` includes `properties.serviceIdentity` example.
|
||||
|
||||
## Graph Explorer v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
|
||||
## Policy Engine + Editor v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-DPOP-11-001 | DOING (2025-11-07) | Authority Core & Security Guild | AUTH-AOC-19-002 | Enforce DPoP sender constraints for all Authority token flows (nonce store selection, algorithm allowlist, `cnf.jkt` persistence, structured telemetry). | `/token` enforces configured DPoP policies (nonce, allowed algorithms); cnf claims verified in integration tests; docs/runbooks updated with configuration guidance. |
|
||||
> 2025-11-07: Joint Authority/DevOps stand-up committed to shipping nonce store + telemetry updates by 2025-11-10; config samples and integration tests being updated in tandem.
|
||||
| AUTH-MTLS-11-002 | DOING (2025-11-07) | Authority Core & Security Guild | AUTH-DPOP-11-001 | Add mTLS-bound access token issuance/validation (client certificate thumbprints, JWKS rotation hooks) for high-assurance tenants and services. | mTLS certificate binding validated end-to-end; audit logs capture cert hashes; docs describe bootstrap/rotation steps. |
|
||||
> 2025-11-08: Wiring cert thumbprint persistence + audit hooks now that DPoP nonce enforcement is in place; targeting shared delivery window with DEVOPS-AIRGAP-57-002.
|
||||
> 2025-11-07: Same stand-up aligned on 2025-11-10 target for mTLS enforcement + JWKS rotation docs so plugin mitigations can unblock.
|
||||
| AUTH-POLICY-23-001 | DONE (2025-10-27) | Authority Core & Docs Guild | AUTH-AOC-19-002 | Introduce fine-grained policy scopes (`policy:read`, `policy:author`, `policy:review`, `policy:simulate`, `findings:read`) for CLI/service identities; refresh discovery metadata, issuer templates, and offline defaults. | Scope catalogue and sample configs updated; `policy-cli` seed credentials rotated; docs recorded migration steps. |
|
||||
| AUTH-POLICY-23-002 | DONE (2025-11-08) | Authority Core & Security Guild | AUTH-POLICY-23-001 | Implement optional two-person rule for activation: require two distinct `policy:activate` approvals when configured; emit audit logs. | Activation endpoint enforces rule; audit logs contain approver IDs; tests cover 2-person path. |
|
||||
> 2025-11-08: Policy Engine enforces pending_second_approval when dual-control toggles demand it, activation auditor emits structured `policy.activation.*` scopes, and tests cover settings/audits.
|
||||
> Blocked: Policy Engine/Studio have not yet exposed activation workflow endpoints or approval payloads needed to enforce dual-control (`WEB-POLICY-23-002`, `POLICY-ENGINE-23-002`). Revisit once activation contract lands.
|
||||
| AUTH-POLICY-23-003 | DONE (2025-11-08) | Authority Core & Docs Guild | AUTH-POLICY-23-001 | Update documentation and sample configs for policy roles, approval workflow, and signing requirements. | Docs updated with reviewer checklist; configuration examples validated. |
|
||||
> 2025-11-08: Docs refreshed for dual-control activation (console workflow, compliance checklist, sample YAML) and linked to new Policy Engine activation options.
|
||||
> 2025-11-07: Scope migration landed (AUTH-POLICY-23-001); dual-approval + documentation tasks now waiting on pairing.
|
||||
> 2025-10-27: Added `policy-cli` defaults to Authority config/secrets, refreshed CLI/CI documentation with the new scope bundle, recorded release migration guidance, and introduced `scripts/verify-policy-scopes.py` to guard against regressions.
|
||||
|
||||
## Graph & Vuln Explorer v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-27: Paused work after exploratory spike (scope enforcement still outstanding); no functional changes merged.
|
||||
|
||||
## Orchestrator Dashboard
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-31: Picked up during Console/Orchestrator alignment; focusing on scope catalog + tenant enforcement first.
|
||||
> 2025-10-31: `orch:read` added to scope catalogue and Authority runtime, Console defaults include the scope, `Orch.Viewer` role documented, and client-credential tests enforce tenant requirements.
|
||||
> 2025-10-27: Added `orch:operate` scope, enforced `operator_reason`/`operator_ticket` on token issuance, updated Authority configs/docs, and captured audit metadata for control actions.
|
||||
> 2025-10-28: Policy gateway + scanner now pass the expanded token client signature (`null` metadata by default), test stubs capture the optional parameters, and Policy Gateway/Scanner suites are green after fixing the Concelier storage build break.
|
||||
> 2025-10-28: Authority password-grant tests now hit the new constructors but still need updates to drop obsolete `IOptions` arguments before the suite can pass.
|
||||
| AUTH-ORCH-34-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-ORCH-33-001 | Introduce `Orch.Admin` role with quota/backfill scopes, enforce audit reason on quota changes, and update offline defaults/docs. | Admin role available; quotas/backfills require scope + reason; tests confirm tenant isolation; documentation updated. |
|
||||
> 2025-11-02: `orch:backfill` scope added with mandatory `backfill_reason`/`backfill_ticket`, client-credential validation and resource authorization paths emit audit fields, CLI picks up new configuration/env vars, and Authority docs/config samples updated for `Orch.Admin`.
|
||||
|
||||
## StellaOps Console (Sprint 23)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-29: Authorization code flow enabled with PKCE requirement, console client seeded in `authority.yaml.sample`, discovery docs updated, and console runbook guidance added.
|
||||
> 2025-10-31: Added `/console/tenants`, `/console/profile`, `/console/token/introspect` endpoints with tenant header filter, scope enforcement (`ui.read`, `authority:tenants.read`), and structured audit events. Console test harness covers success/mismatch cases.
|
||||
> 2025-10-28: `docs/security/console-security.md` drafted with PKCE + DPoP (120 s OpTok, 300 s fresh-auth) and scope table. Authority Core to confirm `/fresh-auth` semantics, token lifetimes, and scope bundles align before closing task.
|
||||
> 2025-10-31: Security guide expanded for `/console` endpoints & orchestrator scope, sample YAML annotated, ops runbook updated, and release note `docs/updates/2025-10-31-console-security-refresh.md` published.
|
||||
> 2025-10-31: Default access-token lifetime reduced to 120 s, console tests updated with dual auth schemes, docs/config/ops notes refreshed, release note logged.
|
||||
|
||||
## Policy Studio (Sprint 27)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-31: Added Policy Studio scope family (`policy:author/review/operate/audit`), updated OpenAPI + discovery headers, enforced tenant requirements in grant handlers, seeded new roles in Authority config/offline kit docs, and refreshed CLI/Console documentation + tests to validate the new catalogue.
|
||||
| AUTH-POLICY-27-002 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-POLICY-27-001, REGISTRY-API-27-007 | Provide attestation signing service bindings (OIDC token exchange, cosign integration) and enforce publish/promote scope checks, fresh-auth requirements, and audit logging. | Publish/promote requests require fresh auth + correct scopes; attestations signed with validated identity; audit logs enriched with digest + tenant; integration tests pass. |
|
||||
> Docs dependency: `DOCS-POLICY-27-009` awaiting signing guidance from this work.
|
||||
> 2025-11-02: Added `policy:publish`/`policy:promote` scopes with interactive-only enforcement, metadata parameters (`policy_reason`, `policy_ticket`, `policy_digest`), fresh-auth token validation, audit augmentations, and updated config/docs references.
|
||||
| AUTH-POLICY-27-003 | DONE (2025-11-04) | Authority Core & Docs Guild | AUTH-POLICY-27-001, AUTH-POLICY-27-002 | Update Authority configuration/docs for Policy Studio roles, signing policies, approval workflows, and CLI integration; include compliance checklist. | Docs merged; samples validated; governance checklist appended; release notes updated. |
|
||||
> 2025-11-04: Policy Studio roles/scopes documented across `docs/11_AUTHORITY.md`, sample configs, and OpenAPI; compliance checklist appended and Authority tests rerun to validate fresh-auth + scope enforcement.
|
||||
|
||||
## Exceptions v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-29: Added exception scopes + routing template options, enforced MFA requirement in password grant handlers, updated configuration samples.
|
||||
> 2025-10-31: Authority scopes/routing docs updated (`docs/security/authority-scopes.md`, `docs/11_AUTHORITY.md`, `docs/policy/exception-effects.md`), monitoring guide covers new MFA audit events, and `etc/authority.yaml.sample` now demonstrates exception clients/templates.
|
||||
|
||||
## Reachability v1
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-29: Signals scopes added with tenant + aoc:verify enforcement; sensors guided via SignalsUploader template; tests cover gating.
|
||||
|
||||
## Vulnerability Explorer (Sprint 29)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-VULN-29-001 | DONE (2025-11-03) | Authority Core & Security Guild | AUTH-POLICY-27-001 | Define Vuln Explorer scopes/roles (`vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit`) with ABAC attributes (env, owner, business_tier) and update discovery metadata/offline kit defaults. | Roles/scopes published; issuer templates updated; integration tests cover ABAC filters; docs refreshed. |
|
||||
| AUTH-VULN-29-002 | DONE (2025-11-03) | Authority Core & Security Guild | AUTH-VULN-29-001, LEDGER-29-002 | Enforce CSRF/anti-forgery tokens for workflow actions, sign attachment tokens, and record audit logs with ledger event hashes. | Workflow calls require valid tokens; audit logs include ledger references; security tests cover token expiry/abuse. |
|
||||
| AUTH-VULN-29-003 | DONE (2025-11-04) | Authority Core & Docs Guild | AUTH-VULN-29-001..002 | Update security docs/config samples for Vuln Explorer roles, ABAC policies, attachment signing, and ledger verification guidance. | Docs merged with compliance checklist; configuration examples validated; release notes updated. |
|
||||
> 2025-11-03: Vuln workflow CSRF + attachment token services live with audit enrichment and negative-path tests. Awaiting completion of full Authority suite run after repository-wide build finishes.
|
||||
> 2025-11-04: Verified Vuln Explorer RBAC/ABAC coverage in Authority docs/security guides, attachment token guidance, and offline samples; Authority tests rerun confirming ledger-token + anti-forgery behaviours.
|
||||
|
||||
## Advisory AI (Sprint 31)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-AIAI-31-001 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-VULN-29-001 | Define Advisory AI scopes (`advisory-ai:view`, `advisory-ai:operate`, `advisory-ai:admin`) and remote inference toggles; update discovery metadata/offline defaults. | Scopes/flags published; integration tests cover RBAC + opt-in settings; docs updated. |
|
||||
| AUTH-AIAI-31-002 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-AIAI-31-001, AIAI-31-006 | Enforce anonymized prompt logging, tenant consent for remote inference, and audit logging of assistant tasks. | Logging/audit flows verified; privacy review passed; docs updated. |
|
||||
|
||||
## Export Center
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
|
||||
## Notifications Studio
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-NOTIFY-38-001 | DONE (2025-11-01) | Authority Core & Security Guild | — | Define `Notify.Viewer`, `Notify.Operator`, `Notify.Admin` scopes/roles, update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit defaults refreshed. |
|
||||
| AUTH-NOTIFY-40-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-38-001, WEB-NOTIFY-40-001 | Implement signed ack token key rotation, webhook allowlists, admin-only escalation settings, and audit logging of ack actions. | Ack tokens signed/rotated; webhook allowlists enforced; admin enforcement validated; audit logs capture ack/resolution. |
|
||||
> 2025-11-02: `/notify/ack-tokens/rotate` exposed (notify.admin), emits `notify.ack.key_rotated|notify.ack.key_rotation_failed`, and DSSE rotation tests cover allowlist + scope enforcement.
|
||||
| AUTH-NOTIFY-42-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-40-001 | Investigate ack token rotation 500 errors (test Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure still failing). Capture logs, identify root cause, and patch handler. | Failure mode understood; fix merged; regression test passes. |
|
||||
> 2025-11-02: Aliased `StellaOpsBearer` to the test auth handler, corrected bootstrap `/notifications/ack-tokens/rotate` defaults, and validated `Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure` via targeted `dotnet test`.
|
||||
|
||||
|
||||
## CLI Parity & Task Packs
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-PACKS-41-001 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AOC-19-001 | Define CLI SSO profiles and pack scopes (`Packs.Read`, `Packs.Write`, `Packs.Run`, `Packs.Approve`), update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit templates refreshed. |
|
||||
> 2025-11-02: Added Pack scope policies, Authority role defaults, and CLI profile guidance covering operator/publisher/approver flows.
|
||||
> 2025-11-02: Shared OpenSSL 1.1 shim feeds Authority & Signals Mongo2Go harnesses so pack scope coverage keeps running on OpenSSL 3 hosts (AUTH-PACKS-41-001).
|
||||
> 2025-11-04: Discovery metadata/OpenAPI advertise packs scopes, configs/offline kit templates bundle new roles, and Authority tests re-run to validate tenant gating for `packs.*`.
|
||||
| AUTH-PACKS-43-001 | BLOCKED (2025-10-27) | Authority Core & Security Guild | AUTH-PACKS-41-001, TASKRUN-42-001, ORCH-SVC-42-101 | Enforce pack signing policies, approval RBAC checks, CLI CI token scopes, and audit logging for approvals. | Signing policies enforced; approvals require correct roles; CI token scope tests pass; audit logs recorded. |
|
||||
> Blocked: ORCH-SVC-42-101 (Orchestrator log streaming/approvals API) still TODO. AUTH-PACKS-41-001 + TASKRUN-42-001 are DONE (2025-11-04); resume once Orchestrator publishes contracts.
|
||||
|
||||
## Authority-Backed Scopes & Tenancy (Epic 14)
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-28: Tidied advisory raw idempotency migration to avoid LINQ-on-`BsonValue` (explicit array copy) while continuing duplicate guardrail validation; scoped scanner/policy token call sites updated to honor new metadata parameter.
|
||||
| AUTH-TEN-49-001 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-TEN-47-001 | Implement service accounts & delegation tokens (`act` chain), per-tenant quotas, audit stream of auth decisions, and revocation APIs. | Service tokens minted with scopes/TTL; delegation logged; quotas configurable; audit stream live; docs updated. |
|
||||
> 2025-11-02: Authority bootstrap test harness now seeds service accounts via AuthorityDelegation options; `/internal/service-accounts` endpoints validated with targeted vstest run.
|
||||
> 2025-11-02: Added Mongo service-account store, seeded options/collection initializers, token persistence metadata (`tokenKind`, `serviceAccountId`, `actorChain`), and docs/config samples. Introduced quota checks + tests covering service account issuance and persistence.
|
||||
> 2025-11-02: Documented bootstrap service-account admin APIs in `docs/11_AUTHORITY.md`, noting API key requirements and stable upsert behaviour.
|
||||
> 2025-11-03: Seeded explicit enabled service-account fixtures for integration tests and reran `StellaOps.Authority.Tests` to greenlight `/internal/service-accounts` listing + revocation scenarios.
|
||||
> 2025-11-04: Confirmed service-account docs/config examples, quota tuning, and audit stream wiring; Authority suite re-executed to cover issuance/listing/revocation flows.
|
||||
|
||||
## Observability & Forensics (Epic 15)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-OBS-50-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-AOC-19-001 | Introduce scopes `obs:read`, `timeline:read`, `timeline:write`, `evidence:create`, `evidence:read`, `evidence:hold`, `attest:read`, and `obs:incident` (all tenant-scoped). Update discovery metadata, offline defaults, and scope grammar docs. | Scopes exposed via metadata; issuer templates updated; offline kit seeded; integration tests cover new scopes. |
|
||||
| AUTH-OBS-52-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-OBS-50-001, TIMELINE-OBS-52-003, EVID-OBS-53-003 | Configure resource server policies for Timeline Indexer, Evidence Locker, Exporter, and Observability APIs enforcing new scopes + tenant claims. Emit audit events including scope usage and trace IDs. | Policies deployed; unauthorized access blocked; audit logs prove scope usage; contract tests updated. |
|
||||
| AUTH-OBS-55-001 | DONE (2025-11-02) | Authority Core & Security Guild, Ops Guild | AUTH-OBS-50-001, WEB-OBS-55-001 | Harden incident mode authorization: require `obs:incident` scope + fresh auth, log activation reason, and expose verification endpoint for auditors. Update docs/runbooks. | Incident activate/deactivate requires scope; audit entries logged; docs updated with imposed rule reminder. |
|
||||
|
||||
## Air-Gapped Mode (Epic 16)
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| AUTH-AIRGAP-56-001 | DONE (2025-11-04) | Authority Core & Security Guild | AIRGAP-CTL-56-001 | Provision new scopes (`airgap:seal`, `airgap:import`, `airgap:status:read`) in configuration metadata, offline kit defaults, and issuer templates. | Scopes exposed in discovery docs; offline kit updated; integration tests cover issuance. |
|
||||
| AUTH-AIRGAP-56-002 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AIRGAP-56-001, AIRGAP-IMP-58-001 | Audit import actions with actor, tenant, bundle ID, and trace ID; expose `/authority/audit/airgap` endpoint. | Audit records persisted; endpoint paginates results; tests cover RBAC + filtering. |
|
||||
> 2025-11-04: Airgap scope constants are wired through discovery metadata, `etc/authority.yaml.sample`, and offline kit docs; scope issuance tests executed via `dotnet test`.
|
||||
> 2025-11-04: `/authority/audit/airgap` API persists tenant-scoped audit entries with pagination and authorization guards validated by the Authority integration suite (187 tests).
|
||||
| AUTH-AIRGAP-57-001 | DOING (2025-11-08) | Authority Core & Security Guild, DevOps Guild | AUTH-AIRGAP-56-001, DEVOPS-AIRGAP-57-002 | Enforce sealed-mode CI gating by refusing token issuance when declared sealed install lacks sealing confirmation. | Implement Authority-side sealed-mode checks once DevOps publishes sealed CI artefacts + contract (target 2025-11-10). |
|
||||
> 2025-11-08: Picked up in tandem with DEVOPS-AIRGAP-57-002 — validating sealed confirmation payload + wiring Authority gating tests against ops/devops/sealed-mode-ci artefacts.
|
||||
> 2025-11-08: `/token`/`/introspect` now reject mTLS-bound tokens without the recorded certificate; `authority_mtls_mismatch_total` metric + docs updated for plugin consumers.
|
||||
> 2025-11-08: DevOps sealed-mode CI now emits `authority-sealed-ci.json`; ingest that contract next to unblock enforcement switch.
|
||||
|
||||
## SDKs & OpenAPI (Epic 17)
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
> 2025-10-28: Auth OpenAPI authored at `src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml` covering `/token`, `/introspect`, `/revoke`, `/jwks`, scope catalog, and error envelopes; parsed via PyYAML sanity check and referenced in Epic 17 docs.
|
||||
> 2025-10-28: Added `/.well-known/openapi` endpoint wiring cached spec metadata, YAML/JSON negotiation, HTTP cache headers, and tests verifying ETag + Accept handling. Authority spec (`src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml`) now includes grant/scope extensions.
|
||||
| AUTH-OAS-62-001 | DONE (2025-11-02) | Authority Core & Security Guild, SDK Generator Guild | AUTH-OAS-61-001, SDKGEN-63-001 | Provide SDK helpers for OAuth2/PAT flows, tenancy override header; add integration tests. | SDKs expose auth helpers; tests cover token issuance; docs updated. |
|
||||
> 2025-11-02: `AddStellaOpsApiAuthentication` shipped (OAuth2 + PAT), tenant header injection added, and client tests updated for caching behaviour.
|
||||
| AUTH-OAS-63-001 | DONE (2025-11-02) | Authority Core & Security Guild, API Governance Guild | APIGOV-63-001 | Emit deprecation headers and notifications for legacy auth endpoints. | Headers emitted; notifications verified; migration guide published. |
|
||||
> 2025-11-02: AUTH-OAS-63-001 completed — legacy OAuth shims emit Deprecation/Sunset/Warning headers, audit events captured, and migration guide published (Authority Core & Security Guild, API Governance Guild).
|
||||
|
||||
@@ -43,6 +43,7 @@ internal static class CommandFactory
|
||||
root.Add(BuildConfigCommand(options));
|
||||
root.Add(BuildKmsCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildVulnCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildCryptoCommand(services, verboseOption, cancellationToken));
|
||||
|
||||
var pluginLogger = loggerFactory.CreateLogger<CliCommandModuleLoader>();
|
||||
var pluginLoader = new CliCommandModuleLoader(services, options, pluginLogger);
|
||||
@@ -180,8 +181,8 @@ internal static class CommandFactory
|
||||
return scan;
|
||||
}
|
||||
|
||||
private static Command BuildKmsCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
private static Command BuildKmsCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var kms = new Command("kms", "Manage file-backed signing keys.");
|
||||
|
||||
var export = new Command("export", "Export key material to a portable bundle.");
|
||||
@@ -381,9 +382,39 @@ internal static class CommandFactory
|
||||
|
||||
db.Add(fetch);
|
||||
db.Add(merge);
|
||||
db.Add(export);
|
||||
return db;
|
||||
}
|
||||
db.Add(export);
|
||||
return db;
|
||||
}
|
||||
|
||||
private static Command BuildCryptoCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var crypto = new Command("crypto", "Inspect StellaOps cryptography providers.");
|
||||
var providers = new Command("providers", "List registered crypto providers and keys.");
|
||||
|
||||
var jsonOption = new Option<bool>("--json")
|
||||
{
|
||||
Description = "Emit JSON output."
|
||||
};
|
||||
|
||||
var profileOption = new Option<string?>("--profile")
|
||||
{
|
||||
Description = "Temporarily override the active registry profile when computing provider order."
|
||||
};
|
||||
|
||||
providers.Add(jsonOption);
|
||||
providers.Add(profileOption);
|
||||
|
||||
providers.SetAction((parseResult, _) =>
|
||||
{
|
||||
var json = parseResult.GetValue(jsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var profile = parseResult.GetValue(profileOption);
|
||||
return CommandHandlers.HandleCryptoProvidersAsync(services, verbose, json, profile, cancellationToken);
|
||||
});
|
||||
|
||||
crypto.Add(providers);
|
||||
return crypto;
|
||||
}
|
||||
|
||||
private static Command BuildSourcesCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -18,7 +18,8 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Rendering;
|
||||
using StellaOps.Auth.Client;
|
||||
@@ -28,7 +29,8 @@ using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.Cli.Services.Models.AdvisoryAi;
|
||||
using StellaOps.Cli.Telemetry;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using StellaOps.Cryptography.Kms;
|
||||
|
||||
namespace StellaOps.Cli.Commands;
|
||||
@@ -6437,35 +6439,223 @@ internal static class CommandHandlers
|
||||
return source;
|
||||
}
|
||||
|
||||
private static async Task TriggerJobAsync(
|
||||
IBackendOperationsClient client,
|
||||
ILogger logger,
|
||||
string jobKind,
|
||||
IDictionary<string, object?> parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
JobTriggerResult result = await client.TriggerJobAsync(jobKind, parameters, cancellationToken).ConfigureAwait(false);
|
||||
if (result.Success)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(result.Location))
|
||||
{
|
||||
logger.LogInformation("Job accepted. Track status at {Location}.", result.Location);
|
||||
}
|
||||
else if (result.Run is not null)
|
||||
{
|
||||
logger.LogInformation("Job accepted. RunId: {RunId} Status: {Status}", result.Run.RunId, result.Run.Status);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Job accepted.");
|
||||
}
|
||||
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("Job '{JobKind}' failed: {Message}", jobKind, result.Message);
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
private static async Task TriggerJobAsync(
|
||||
IBackendOperationsClient client,
|
||||
ILogger logger,
|
||||
string jobKind,
|
||||
IDictionary<string, object?> parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
JobTriggerResult result = await client.TriggerJobAsync(jobKind, parameters, cancellationToken).ConfigureAwait(false);
|
||||
if (result.Success)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(result.Location))
|
||||
{
|
||||
logger.LogInformation("Job accepted. Track status at {Location}.", result.Location);
|
||||
}
|
||||
else if (result.Run is not null)
|
||||
{
|
||||
logger.LogInformation("Job accepted. RunId: {RunId} Status: {Status}", result.Run.RunId, result.Run.Status);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Job accepted.");
|
||||
}
|
||||
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("Job '{JobKind}' failed: {Message}", jobKind, result.Message);
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static Task HandleCryptoProvidersAsync(
|
||||
IServiceProvider services,
|
||||
bool verbose,
|
||||
bool jsonOutput,
|
||||
string? profileOverride,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = services.CreateScope();
|
||||
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("crypto-providers");
|
||||
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
|
||||
var previousLevel = verbosity.MinimumLevel;
|
||||
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
|
||||
using var activity = CliActivitySource.Instance.StartActivity("cli.crypto.providers", ActivityKind.Internal);
|
||||
using var duration = CliMetrics.MeasureCommandDuration("crypto providers");
|
||||
|
||||
try
|
||||
{
|
||||
var registry = scope.ServiceProvider.GetService<ICryptoProviderRegistry>();
|
||||
if (registry is null)
|
||||
{
|
||||
logger.LogWarning("Crypto provider registry not available in this environment.");
|
||||
AnsiConsole.MarkupLine("[yellow]Crypto subsystem is not configured in this environment.[/]");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var optionsMonitor = scope.ServiceProvider.GetService<IOptionsMonitor<CryptoProviderRegistryOptions>>();
|
||||
var registryOptions = optionsMonitor?.CurrentValue ?? new CryptoProviderRegistryOptions();
|
||||
var preferredOrder = DeterminePreferredOrder(registryOptions, profileOverride);
|
||||
var providers = registry.Providers
|
||||
.Select(provider => new ProviderInfo(
|
||||
provider.Name,
|
||||
provider.GetType().FullName ?? provider.GetType().Name,
|
||||
DescribeProviderKeys(provider).ToList()))
|
||||
.ToList();
|
||||
|
||||
if (jsonOutput)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
activeProfile = registryOptions.ActiveProfile,
|
||||
preferredOrder,
|
||||
providers = providers.Select(info => new
|
||||
{
|
||||
info.Name,
|
||||
info.Type,
|
||||
keys = info.Keys.Select(k => new
|
||||
{
|
||||
k.KeyId,
|
||||
k.AlgorithmId,
|
||||
Metadata = k.Metadata
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
Console.WriteLine(JsonSerializer.Serialize(payload, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
}));
|
||||
Environment.ExitCode = 0;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
RenderCryptoProviders(preferredOrder, providers);
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
verbosity.MinimumLevel = previousLevel;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void RenderCryptoProviders(
|
||||
IReadOnlyList<string> preferredOrder,
|
||||
IReadOnlyCollection<ProviderInfo> providers)
|
||||
{
|
||||
if (preferredOrder.Count > 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[cyan]Preferred order:[/] {0}", Markup.Escape(string.Join(", ", preferredOrder)));
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine("[yellow]Preferred order is not configured; using registration order.[/]");
|
||||
}
|
||||
|
||||
var table = new Table().Border(TableBorder.Rounded);
|
||||
table.AddColumn("Provider");
|
||||
table.AddColumn("Type");
|
||||
table.AddColumn("Keys");
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
var keySummary = provider.Keys.Count == 0
|
||||
? "[grey]No signing keys exposed (managed externally).[/]"
|
||||
: string.Join(Environment.NewLine, provider.Keys.Select(FormatDescriptor));
|
||||
|
||||
table.AddRow(
|
||||
Markup.Escape(provider.Name),
|
||||
Markup.Escape(provider.Type),
|
||||
keySummary);
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CryptoProviderKeyDescriptor> DescribeProviderKeys(ICryptoProvider provider)
|
||||
{
|
||||
if (provider is ICryptoProviderDiagnostics diagnostics)
|
||||
{
|
||||
return diagnostics.DescribeKeys().ToList();
|
||||
}
|
||||
|
||||
var signingKeys = provider.GetSigningKeys();
|
||||
if (signingKeys.Count == 0)
|
||||
{
|
||||
return Array.Empty<CryptoProviderKeyDescriptor>();
|
||||
}
|
||||
|
||||
var descriptors = new List<CryptoProviderKeyDescriptor>(signingKeys.Count);
|
||||
foreach (var signingKey in signingKeys)
|
||||
{
|
||||
var metadata = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["kind"] = signingKey.Kind.ToString(),
|
||||
["createdAt"] = signingKey.CreatedAt.UtcDateTime.ToString("O"),
|
||||
["providerHint"] = signingKey.Reference.ProviderHint
|
||||
};
|
||||
|
||||
if (signingKey.ExpiresAt.HasValue)
|
||||
{
|
||||
metadata["expiresAt"] = signingKey.ExpiresAt.Value.UtcDateTime.ToString("O");
|
||||
}
|
||||
|
||||
foreach (var pair in signingKey.Metadata)
|
||||
{
|
||||
metadata[$"meta.{pair.Key}"] = pair.Value;
|
||||
}
|
||||
|
||||
descriptors.Add(new CryptoProviderKeyDescriptor(
|
||||
provider.Name,
|
||||
signingKey.Reference.KeyId,
|
||||
signingKey.AlgorithmId,
|
||||
metadata));
|
||||
}
|
||||
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> DeterminePreferredOrder(
|
||||
CryptoProviderRegistryOptions? options,
|
||||
string? overrideProfile)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(overrideProfile) &&
|
||||
options.Profiles.TryGetValue(overrideProfile, out var profile) &&
|
||||
profile.PreferredProviders.Count > 0)
|
||||
{
|
||||
return profile.PreferredProviders
|
||||
.Where(static provider => !string.IsNullOrWhiteSpace(provider))
|
||||
.Select(static provider => provider.Trim())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
return options.ResolvePreferredProviders();
|
||||
}
|
||||
|
||||
private static string FormatDescriptor(CryptoProviderKeyDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.Metadata.Count == 0)
|
||||
{
|
||||
return $"{Markup.Escape(descriptor.KeyId)} ({Markup.Escape(descriptor.AlgorithmId)})";
|
||||
}
|
||||
|
||||
var metadataText = string.Join(
|
||||
", ",
|
||||
descriptor.Metadata.Select(pair => $"{pair.Key}={pair.Value}"));
|
||||
|
||||
return $"{Markup.Escape(descriptor.KeyId)} ({Markup.Escape(descriptor.AlgorithmId)}){Environment.NewLine}[grey]{Markup.Escape(metadataText)}[/]";
|
||||
}
|
||||
|
||||
private sealed record ProviderInfo(string Name, string Type, IReadOnlyList<CryptoProviderKeyDescriptor> Keys);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Configuration;
|
||||
|
||||
namespace StellaOps.Cli.Configuration;
|
||||
|
||||
@@ -31,8 +32,11 @@ public sealed class StellaOpsCliOptions
|
||||
|
||||
public StellaOpsCliOfflineOptions Offline { get; set; } = new();
|
||||
|
||||
public StellaOpsCliPluginOptions Plugins { get; set; } = new();
|
||||
}
|
||||
public StellaOpsCliPluginOptions Plugins { get; set; } = new();
|
||||
|
||||
public StellaOpsCryptoOptions Crypto { get; set; } = new();
|
||||
|
||||
}
|
||||
|
||||
public sealed class StellaOpsCliAuthorityOptions
|
||||
{
|
||||
@@ -79,15 +83,15 @@ public sealed class StellaOpsCliOfflineOptions
|
||||
public string? MirrorUrl { get; set; }
|
||||
}
|
||||
|
||||
public sealed class StellaOpsCliPluginOptions
|
||||
{
|
||||
public sealed class StellaOpsCliPluginOptions
|
||||
{
|
||||
public string BaseDirectory { get; set; } = string.Empty;
|
||||
|
||||
public string Directory { get; set; } = "plugins/cli";
|
||||
|
||||
public IList<string> SearchPatterns { get; set; } = new List<string>();
|
||||
|
||||
public IList<string> PluginOrder { get; set; } = new List<string>();
|
||||
|
||||
public string ManifestSearchPattern { get; set; } = "*.manifest.json";
|
||||
}
|
||||
public IList<string> PluginOrder { get; set; } = new List<string>();
|
||||
|
||||
public string ManifestSearchPattern { get; set; } = "*.manifest.json";
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Telemetry;
|
||||
using StellaOps.AirGap.Policy;
|
||||
using StellaOps.Configuration;
|
||||
|
||||
namespace StellaOps.Cli;
|
||||
|
||||
@@ -22,12 +23,14 @@ internal static class Program
|
||||
var (options, configuration) = CliBootstrapper.Build(args);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(configuration);
|
||||
services.AddSingleton(options);
|
||||
|
||||
var verbosityState = new VerbosityState();
|
||||
services.AddSingleton(configuration);
|
||||
services.AddSingleton(options);
|
||||
services.AddOptions();
|
||||
|
||||
var verbosityState = new VerbosityState();
|
||||
services.AddSingleton(verbosityState);
|
||||
services.AddAirGapEgressPolicy(configuration);
|
||||
services.AddStellaOpsCrypto(options.Crypto);
|
||||
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
@@ -168,6 +171,7 @@ internal static class Program
|
||||
finalExit = 130; // Typical POSIX cancellation exit code
|
||||
}
|
||||
|
||||
return finalExit;
|
||||
}
|
||||
}
|
||||
return finalExit;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Kms/StellaOps.Cryptography.Kms.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Plugin.CryptoPro/StellaOps.Cryptography.Plugin.CryptoPro.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Plugin.Pkcs11Gost/StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj" />
|
||||
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.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" />
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.|
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.|
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
@@ -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": "Множественные уязвимости криптопровайдера"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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/");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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?>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user