Add post-quantum cryptography support with PqSoftCryptoProvider
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled

- Implemented PqSoftCryptoProvider for software-only post-quantum algorithms (Dilithium3, Falcon512) using BouncyCastle.
- Added PqSoftProviderOptions and PqSoftKeyOptions for configuration.
- Created unit tests for Dilithium3 and Falcon512 signing and verification.
- Introduced EcdsaPolicyCryptoProvider for compliance profiles (FIPS/eIDAS) with explicit allow-lists.
- Added KcmvpHashOnlyProvider for KCMVP baseline compliance.
- Updated project files and dependencies for new libraries and testing frameworks.
This commit is contained in:
StellaOps Bot
2025-12-07 15:04:19 +02:00
parent 862bb6ed80
commit 98e6b76584
119 changed files with 11436 additions and 1732 deletions

View File

@@ -0,0 +1,434 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Serialization;
namespace StellaOps.Scanner.WebService.Tests;
/// <summary>
/// Tests verifying Notifier service can ingest scanner events per orchestrator-envelope.schema.json.
/// </summary>
public sealed class NotifierIngestionTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
[Fact]
public void NotifierMetadata_SerializesCorrectly()
{
var metadata = new NotifierIngestionMetadata
{
SeverityThresholdMet = true,
NotificationChannels = new[] { "email", "slack" },
DigestEligible = false,
ImmediateDispatch = true,
Priority = "critical"
};
var orchestratorEvent = CreateTestEvent(metadata);
var json = OrchestratorEventSerializer.Serialize(orchestratorEvent);
var node = JsonNode.Parse(json)?.AsObject();
Assert.NotNull(node);
Assert.NotNull(node["notifier"]);
var notifierNode = node["notifier"]!.AsObject();
Assert.True(notifierNode["severityThresholdMet"]?.GetValue<bool>());
Assert.False(notifierNode["digestEligible"]?.GetValue<bool>());
Assert.True(notifierNode["immediateDispatch"]?.GetValue<bool>());
Assert.Equal("critical", notifierNode["priority"]?.GetValue<string>());
var channels = notifierNode["notificationChannels"]?.AsArray();
Assert.NotNull(channels);
Assert.Equal(2, channels.Count);
Assert.Contains("email", channels.Select(c => c?.GetValue<string>()));
Assert.Contains("slack", channels.Select(c => c?.GetValue<string>()));
}
[Fact]
public void NotifierMetadata_OmittedWhenNull()
{
var orchestratorEvent = new OrchestratorEvent
{
EventId = Guid.NewGuid(),
Kind = OrchestratorEventKinds.ScannerReportReady,
Version = 1,
Tenant = "test-tenant",
OccurredAt = DateTimeOffset.UtcNow,
Source = "scanner.webservice",
IdempotencyKey = "test-key",
Payload = new ReportReadyEventPayload
{
ReportId = "report-123",
ImageDigest = "sha256:abc123",
GeneratedAt = DateTimeOffset.UtcNow,
Verdict = "pass",
Summary = new ReportSummaryDto(),
Policy = new ReportPolicyDto(),
Links = new ReportLinksPayload(),
Report = new ReportDocumentDto()
},
Notifier = null // Explicitly null
};
var json = OrchestratorEventSerializer.Serialize(orchestratorEvent);
var node = JsonNode.Parse(json)?.AsObject();
Assert.NotNull(node);
Assert.Null(node["notifier"]); // Should be omitted when null
}
[Theory]
[InlineData("critical", true, true)]
[InlineData("high", true, false)]
[InlineData("medium", false, false)]
[InlineData("low", false, false)]
public void NotifierMetadata_SeverityThresholdCalculation(string severity, bool expectedThresholdMet, bool expectedImmediate)
{
var metadata = CreateNotifierMetadataForSeverity(severity);
Assert.Equal(expectedThresholdMet, metadata.SeverityThresholdMet);
Assert.Equal(expectedImmediate, metadata.ImmediateDispatch);
}
[Fact]
public void ScanStartedEvent_SerializesForNotifier()
{
var orchestratorEvent = new OrchestratorEvent
{
EventId = Guid.NewGuid(),
Kind = OrchestratorEventKinds.ScannerScanStarted,
Version = 1,
Tenant = "test-tenant",
OccurredAt = DateTimeOffset.Parse("2025-12-07T10:00:00Z"),
Source = "scanner.webservice",
IdempotencyKey = "scanner.event.scan.started:test-tenant:scan-001",
Payload = new ScanStartedEventPayload
{
ScanId = "scan-001",
JobId = "job-001",
Target = new ScanTargetPayload
{
Type = "container_image",
Identifier = "registry.example/app:v1.0.0",
Digest = "sha256:abc123def456"
},
StartedAt = DateTimeOffset.Parse("2025-12-07T10:00:00Z"),
Status = "started"
},
Notifier = new NotifierIngestionMetadata
{
SeverityThresholdMet = false,
DigestEligible = true,
ImmediateDispatch = false
}
};
var json = OrchestratorEventSerializer.Serialize(orchestratorEvent);
var node = JsonNode.Parse(json)?.AsObject();
Assert.NotNull(node);
Assert.Equal(OrchestratorEventKinds.ScannerScanStarted, node["kind"]?.GetValue<string>());
var payload = node["payload"]?.AsObject();
Assert.NotNull(payload);
Assert.Equal("scan-001", payload["scanId"]?.GetValue<string>());
Assert.Equal("started", payload["status"]?.GetValue<string>());
var target = payload["target"]?.AsObject();
Assert.NotNull(target);
Assert.Equal("container_image", target["type"]?.GetValue<string>());
}
[Fact]
public void ScanFailedEvent_SerializesWithErrorDetails()
{
var orchestratorEvent = new OrchestratorEvent
{
EventId = Guid.NewGuid(),
Kind = OrchestratorEventKinds.ScannerScanFailed,
Version = 1,
Tenant = "test-tenant",
OccurredAt = DateTimeOffset.Parse("2025-12-07T10:05:00Z"),
Source = "scanner.webservice",
IdempotencyKey = "scanner.event.scan.failed:test-tenant:scan-002",
Payload = new ScanFailedEventPayload
{
ScanId = "scan-002",
Target = new ScanTargetPayload
{
Type = "container_image",
Identifier = "registry.example/broken:latest"
},
StartedAt = DateTimeOffset.Parse("2025-12-07T10:00:00Z"),
FailedAt = DateTimeOffset.Parse("2025-12-07T10:05:00Z"),
DurationMs = 300000,
Status = "failed",
Error = new ScanErrorPayload
{
Code = "IMAGE_PULL_FAILED",
Message = "Unable to pull image: authentication required",
Details = ImmutableDictionary.CreateRange(new[]
{
KeyValuePair.Create("registry", "registry.example"),
KeyValuePair.Create("httpStatus", "401")
}),
Recoverable = true
}
},
Notifier = new NotifierIngestionMetadata
{
SeverityThresholdMet = true,
NotificationChannels = new[] { "email", "slack", "pagerduty" },
DigestEligible = false,
ImmediateDispatch = true,
Priority = "high"
}
};
var json = OrchestratorEventSerializer.Serialize(orchestratorEvent);
var node = JsonNode.Parse(json)?.AsObject();
Assert.NotNull(node);
Assert.Equal(OrchestratorEventKinds.ScannerScanFailed, node["kind"]?.GetValue<string>());
var payload = node["payload"]?.AsObject();
Assert.NotNull(payload);
Assert.Equal("failed", payload["status"]?.GetValue<string>());
Assert.Equal(300000, payload["durationMs"]?.GetValue<long>());
var error = payload["error"]?.AsObject();
Assert.NotNull(error);
Assert.Equal("IMAGE_PULL_FAILED", error["code"]?.GetValue<string>());
Assert.True(error["recoverable"]?.GetValue<bool>());
var notifier = node["notifier"]?.AsObject();
Assert.NotNull(notifier);
Assert.True(notifier["immediateDispatch"]?.GetValue<bool>());
}
[Fact]
public void VulnerabilityDetectedEvent_SerializesForNotifier()
{
var orchestratorEvent = new OrchestratorEvent
{
EventId = Guid.NewGuid(),
Kind = OrchestratorEventKinds.ScannerVulnerabilityDetected,
Version = 1,
Tenant = "test-tenant",
OccurredAt = DateTimeOffset.Parse("2025-12-07T10:00:00Z"),
Source = "scanner.webservice",
IdempotencyKey = "scanner.event.vulnerability.detected:test-tenant:CVE-2024-9999:pkg:npm/lodash@4.17.20",
Payload = new VulnerabilityDetectedEventPayload
{
ScanId = "scan-001",
Vulnerability = new VulnerabilityInfoPayload
{
Id = "CVE-2024-9999",
Severity = "critical",
CvssScore = 9.8,
CvssVector = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
Title = "Remote Code Execution in lodash",
FixAvailable = true,
FixedVersion = "4.17.21",
KevListed = true,
EpssScore = 0.95
},
AffectedComponent = new ComponentInfoPayload
{
Purl = "pkg:npm/lodash@4.17.20",
Name = "lodash",
Version = "4.17.20",
Ecosystem = "npm",
Location = "/app/node_modules/lodash"
},
Reachability = "reachable",
DetectedAt = DateTimeOffset.Parse("2025-12-07T10:00:00Z")
},
Notifier = new NotifierIngestionMetadata
{
SeverityThresholdMet = true,
NotificationChannels = new[] { "email", "slack", "pagerduty" },
DigestEligible = false,
ImmediateDispatch = true,
Priority = "critical"
}
};
var json = OrchestratorEventSerializer.Serialize(orchestratorEvent);
var node = JsonNode.Parse(json)?.AsObject();
Assert.NotNull(node);
Assert.Equal(OrchestratorEventKinds.ScannerVulnerabilityDetected, node["kind"]?.GetValue<string>());
var payload = node["payload"]?.AsObject();
Assert.NotNull(payload);
var vuln = payload["vulnerability"]?.AsObject();
Assert.NotNull(vuln);
Assert.Equal("CVE-2024-9999", vuln["id"]?.GetValue<string>());
Assert.Equal("critical", vuln["severity"]?.GetValue<string>());
Assert.Equal(9.8, vuln["cvssScore"]?.GetValue<double>());
Assert.True(vuln["kevListed"]?.GetValue<bool>());
var component = payload["affectedComponent"]?.AsObject();
Assert.NotNull(component);
Assert.Equal("pkg:npm/lodash@4.17.20", component["purl"]?.GetValue<string>());
Assert.Equal("reachable", payload["reachability"]?.GetValue<string>());
}
[Fact]
public void SbomGeneratedEvent_SerializesForNotifier()
{
var orchestratorEvent = new OrchestratorEvent
{
EventId = Guid.NewGuid(),
Kind = OrchestratorEventKinds.ScannerSbomGenerated,
Version = 1,
Tenant = "test-tenant",
OccurredAt = DateTimeOffset.Parse("2025-12-07T10:00:00Z"),
Source = "scanner.webservice",
IdempotencyKey = "scanner.event.sbom.generated:test-tenant:sbom-001",
Payload = new SbomGeneratedEventPayload
{
ScanId = "scan-001",
SbomId = "sbom-001",
Target = new ScanTargetPayload
{
Type = "container_image",
Identifier = "registry.example/app:v1.0.0",
Digest = "sha256:abc123def456"
},
GeneratedAt = DateTimeOffset.Parse("2025-12-07T10:00:00Z"),
Format = "cyclonedx",
SpecVersion = "1.6",
ComponentCount = 127,
SbomRef = "s3://sboms/sbom-001.json",
Digest = "sha256:sbom-digest-789"
},
Notifier = new NotifierIngestionMetadata
{
SeverityThresholdMet = false,
DigestEligible = true,
ImmediateDispatch = false
}
};
var json = OrchestratorEventSerializer.Serialize(orchestratorEvent);
var node = JsonNode.Parse(json)?.AsObject();
Assert.NotNull(node);
Assert.Equal(OrchestratorEventKinds.ScannerSbomGenerated, node["kind"]?.GetValue<string>());
var payload = node["payload"]?.AsObject();
Assert.NotNull(payload);
Assert.Equal("sbom-001", payload["sbomId"]?.GetValue<string>());
Assert.Equal("cyclonedx", payload["format"]?.GetValue<string>());
Assert.Equal("1.6", payload["specVersion"]?.GetValue<string>());
Assert.Equal(127, payload["componentCount"]?.GetValue<int>());
}
[Fact]
public void AllEventKinds_HaveCorrectFormat()
{
Assert.Matches(@"^scanner\.event\.[a-z]+\.[a-z]+$", OrchestratorEventKinds.ScannerReportReady);
Assert.Matches(@"^scanner\.event\.[a-z]+\.[a-z]+$", OrchestratorEventKinds.ScannerScanCompleted);
Assert.Matches(@"^scanner\.event\.[a-z]+\.[a-z]+$", OrchestratorEventKinds.ScannerScanStarted);
Assert.Matches(@"^scanner\.event\.[a-z]+\.[a-z]+$", OrchestratorEventKinds.ScannerScanFailed);
Assert.Matches(@"^scanner\.event\.[a-z]+\.[a-z]+$", OrchestratorEventKinds.ScannerSbomGenerated);
Assert.Matches(@"^scanner\.event\.[a-z]+\.[a-z]+$", OrchestratorEventKinds.ScannerVulnerabilityDetected);
}
[Fact]
public void NotifierChannels_SupportAllChannelTypes()
{
var validChannels = new[] { "email", "slack", "teams", "webhook", "pagerduty" };
foreach (var channel in validChannels)
{
var metadata = new NotifierIngestionMetadata
{
SeverityThresholdMet = true,
NotificationChannels = new[] { channel },
DigestEligible = true,
ImmediateDispatch = false
};
var orchestratorEvent = CreateTestEvent(metadata);
var json = OrchestratorEventSerializer.Serialize(orchestratorEvent);
var node = JsonNode.Parse(json)?.AsObject();
Assert.NotNull(node);
var notifier = node["notifier"]?.AsObject();
Assert.NotNull(notifier);
var channels = notifier["notificationChannels"]?.AsArray();
Assert.NotNull(channels);
Assert.Contains(channel, channels.Select(c => c?.GetValue<string>()));
}
}
private static OrchestratorEvent CreateTestEvent(NotifierIngestionMetadata? notifier)
{
return new OrchestratorEvent
{
EventId = Guid.NewGuid(),
Kind = OrchestratorEventKinds.ScannerReportReady,
Version = 1,
Tenant = "test-tenant",
OccurredAt = DateTimeOffset.UtcNow,
Source = "scanner.webservice",
IdempotencyKey = "test-key",
Payload = new ReportReadyEventPayload
{
ReportId = "report-123",
ImageDigest = "sha256:abc123",
GeneratedAt = DateTimeOffset.UtcNow,
Verdict = "pass",
Summary = new ReportSummaryDto(),
Policy = new ReportPolicyDto(),
Links = new ReportLinksPayload(),
Report = new ReportDocumentDto()
},
Notifier = notifier
};
}
private static NotifierIngestionMetadata CreateNotifierMetadataForSeverity(string severity)
{
return severity.ToLowerInvariant() switch
{
"critical" => new NotifierIngestionMetadata
{
SeverityThresholdMet = true,
NotificationChannels = new[] { "email", "slack", "pagerduty" },
DigestEligible = false,
ImmediateDispatch = true,
Priority = "critical"
},
"high" => new NotifierIngestionMetadata
{
SeverityThresholdMet = true,
NotificationChannels = new[] { "email", "slack" },
DigestEligible = false,
ImmediateDispatch = false,
Priority = "high"
},
_ => new NotifierIngestionMetadata
{
SeverityThresholdMet = false,
DigestEligible = true,
ImmediateDispatch = false,
Priority = "normal"
}
};
}
}