feat: Implement Wine CSP HTTP provider for GOST cryptographic operations
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (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
Signals CI & Image / signals-ci (push) Has been cancelled

- Added WineCspHttpProvider class to interface with Wine-hosted CryptoPro CSP.
- Implemented ICryptoProvider, ICryptoProviderDiagnostics, and IDisposable interfaces.
- Introduced WineCspHttpSigner and WineCspHttpHasher for signing and hashing operations.
- Created WineCspProviderOptions for configuration settings including service URL and key options.
- Developed CryptoProGostSigningService to handle GOST signing operations and key management.
- Implemented HTTP service for the Wine CSP with endpoints for signing, verification, and hashing.
- Added Swagger documentation for API endpoints.
- Included health checks and error handling for service availability.
- Established DTOs for request and response models in the service.
This commit is contained in:
StellaOps Bot
2025-12-07 14:02:42 +02:00
parent 965cbf9574
commit bd2529502e
56 changed files with 9438 additions and 699 deletions

View File

@@ -0,0 +1,393 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Excititor.Core.Observations;
using Xunit;
namespace StellaOps.Excititor.Core.UnitTests.Observations;
public class AppendOnlyLinksetExtractionServiceTests
{
private readonly InMemoryAppendOnlyLinksetStore _store;
private readonly AppendOnlyLinksetExtractionService _service;
public AppendOnlyLinksetExtractionServiceTests()
{
_store = new InMemoryAppendOnlyLinksetStore();
_service = new AppendOnlyLinksetExtractionService(
_store,
NullLogger<AppendOnlyLinksetExtractionService>.Instance);
}
[Fact]
public async Task ProcessObservationsAsync_AppendsToStore_WithDeterministicOrdering()
{
var obs1 = BuildObservation(
id: "obs-1",
provider: "provider-a",
vuln: "CVE-2025-0001",
product: "pkg:npm/leftpad",
createdAt: DateTimeOffset.Parse("2025-11-20T10:00:00Z"));
var obs2 = BuildObservation(
id: "obs-2",
provider: "provider-b",
vuln: "CVE-2025-0001",
product: "pkg:npm/leftpad",
createdAt: DateTimeOffset.Parse("2025-11-20T11:00:00Z"));
var results = await _service.ProcessObservationsAsync("tenant-a", new[] { obs2, obs1 }, CancellationToken.None);
Assert.Single(results);
var result = results[0];
Assert.True(result.Success);
Assert.True(result.WasCreated);
Assert.Equal(2, result.ObservationsAdded);
Assert.NotNull(result.Linkset);
Assert.Equal("CVE-2025-0001", result.Linkset.VulnerabilityId);
Assert.Equal("pkg:npm/leftpad", result.Linkset.ProductKey);
}
[Fact]
public async Task ProcessObservationsAsync_DeduplicatesObservations()
{
var obs = BuildObservation(
id: "obs-1",
provider: "provider-a",
vuln: "CVE-2025-0001",
product: "pkg:npm/leftpad",
createdAt: DateTimeOffset.UtcNow);
// Process the same observation twice
await _service.ProcessObservationsAsync("tenant-a", new[] { obs }, CancellationToken.None);
var results = await _service.ProcessObservationsAsync("tenant-a", new[] { obs }, CancellationToken.None);
Assert.Single(results);
var result = results[0];
Assert.True(result.Success);
Assert.False(result.WasCreated); // Already exists
Assert.Equal(0, result.ObservationsAdded); // Deduplicated
}
[Fact]
public async Task ProcessObservationsAsync_GroupsByVulnerabilityAndProduct()
{
var obs1 = BuildObservation("obs-1", "provider-a", "CVE-2025-0001", "pkg:npm/foo", DateTimeOffset.UtcNow);
var obs2 = BuildObservation("obs-2", "provider-b", "CVE-2025-0001", "pkg:npm/bar", DateTimeOffset.UtcNow);
var obs3 = BuildObservation("obs-3", "provider-c", "CVE-2025-0002", "pkg:npm/foo", DateTimeOffset.UtcNow);
var results = await _service.ProcessObservationsAsync("tenant-a", new[] { obs1, obs2, obs3 }, CancellationToken.None);
Assert.Equal(3, results.Length);
Assert.True(results.All(r => r.Success));
Assert.True(results.All(r => r.WasCreated));
}
[Fact]
public async Task ProcessObservationsAsync_EnforcesTenantIsolation()
{
var obs = BuildObservation("obs-1", "provider-a", "CVE-2025-0001", "pkg:npm/leftpad", DateTimeOffset.UtcNow);
await _service.ProcessObservationsAsync("tenant-a", new[] { obs }, CancellationToken.None);
var linkset = await _store.GetByKeyAsync("tenant-b", "CVE-2025-0001", "pkg:npm/leftpad", CancellationToken.None);
Assert.Null(linkset); // Different tenant should not see it
}
[Fact]
public async Task ProcessObservationsAsync_ReturnsEmptyForNullOrEmpty()
{
var results1 = await _service.ProcessObservationsAsync("tenant-a", null!, CancellationToken.None);
var results2 = await _service.ProcessObservationsAsync("tenant-a", Array.Empty<VexObservation>(), CancellationToken.None);
Assert.Empty(results1);
Assert.Empty(results2);
}
[Fact]
public async Task AppendDisagreementAsync_AppendsToExistingLinkset()
{
var obs = BuildObservation("obs-1", "provider-a", "CVE-2025-0001", "pkg:npm/leftpad", DateTimeOffset.UtcNow);
await _service.ProcessObservationsAsync("tenant-a", new[] { obs }, CancellationToken.None);
var disagreement = new VexObservationDisagreement("provider-b", "not_affected", "inline_mitigations_already_exist", 0.9);
var result = await _service.AppendDisagreementAsync(
"tenant-a",
"CVE-2025-0001",
"pkg:npm/leftpad",
disagreement,
CancellationToken.None);
Assert.True(result.Success);
Assert.Equal(1, result.DisagreementsAdded);
Assert.NotNull(result.Linkset);
Assert.True(result.Linkset.HasConflicts);
}
[Fact]
public async Task AppendDisagreementAsync_CreatesLinksetIfNotExists()
{
var disagreement = new VexObservationDisagreement("provider-a", "affected", null, null);
var result = await _service.AppendDisagreementAsync(
"tenant-a",
"CVE-2025-9999",
"pkg:npm/new-package",
disagreement,
CancellationToken.None);
Assert.True(result.Success);
Assert.True(result.WasCreated);
Assert.Equal(1, result.DisagreementsAdded);
}
private static VexObservation BuildObservation(string id, string provider, string vuln, string product, DateTimeOffset createdAt)
{
var statement = new VexObservationStatement(
vulnerabilityId: vuln,
productKey: product,
status: VexClaimStatus.Affected,
lastObserved: null,
locator: null,
justification: null,
introducedVersion: null,
fixedVersion: null,
purl: product,
cpe: null,
evidence: null,
metadata: null);
var upstream = new VexObservationUpstream(
upstreamId: $"upstream-{id}",
documentVersion: "1",
fetchedAt: createdAt,
receivedAt: createdAt,
contentHash: "sha256:deadbeef",
signature: new VexObservationSignature(false, null, null, null));
var content = new VexObservationContent(
format: "openvex",
specVersion: "1.0.0",
raw: JsonNode.Parse("{}")!,
metadata: null);
var linkset = new VexObservationLinkset(
aliases: new[] { vuln },
purls: new[] { product },
cpes: Array.Empty<string>(),
references: Array.Empty<VexObservationReference>());
return new VexObservation(
observationId: id,
tenant: "tenant-a",
providerId: provider,
streamId: "ingest",
upstream: upstream,
statements: ImmutableArray.Create(statement),
content: content,
linkset: linkset,
createdAt: createdAt);
}
}
/// <summary>
/// In-memory implementation of IAppendOnlyLinksetStore for testing.
/// </summary>
internal class InMemoryAppendOnlyLinksetStore : IAppendOnlyLinksetStore
{
private readonly Dictionary<string, VexLinkset> _linksets = new();
private readonly List<LinksetMutationEvent> _mutations = new();
private long _sequenceNumber = 0;
private readonly object _lock = new();
public ValueTask<AppendLinksetResult> AppendObservationAsync(
string tenant,
string vulnerabilityId,
string productKey,
VexLinksetObservationRefModel observation,
VexProductScope scope,
CancellationToken cancellationToken)
{
return AppendObservationsBatchAsync(tenant, vulnerabilityId, productKey, new[] { observation }, scope, cancellationToken);
}
public ValueTask<AppendLinksetResult> AppendObservationsBatchAsync(
string tenant,
string vulnerabilityId,
string productKey,
IEnumerable<VexLinksetObservationRefModel> observations,
VexProductScope scope,
CancellationToken cancellationToken)
{
lock (_lock)
{
var linksetId = VexLinkset.CreateLinksetId(tenant, vulnerabilityId, productKey);
var key = $"{tenant}|{linksetId}";
var wasCreated = false;
var observationsAdded = 0;
if (!_linksets.TryGetValue(key, out var linkset))
{
wasCreated = true;
linkset = new VexLinkset(
linksetId, tenant, vulnerabilityId, productKey, scope,
Enumerable.Empty<VexLinksetObservationRefModel>(),
null, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow);
_linksets[key] = linkset;
_mutations.Add(new LinksetMutationEvent(
++_sequenceNumber, LinksetMutationEvent.MutationTypes.LinksetCreated,
DateTimeOffset.UtcNow, null, null, null, null, null));
}
var existingObsIds = new HashSet<string>(
linkset.Observations.Select(o => o.ObservationId),
StringComparer.Ordinal);
var newObservations = observations
.Where(o => !existingObsIds.Contains(o.ObservationId))
.ToList();
if (newObservations.Count > 0)
{
var allObservations = linkset.Observations.Concat(newObservations);
linkset = linkset.WithObservations(allObservations, linkset.Disagreements);
_linksets[key] = linkset;
observationsAdded = newObservations.Count;
foreach (var obs in newObservations)
{
_mutations.Add(new LinksetMutationEvent(
++_sequenceNumber, LinksetMutationEvent.MutationTypes.ObservationAdded,
DateTimeOffset.UtcNow, obs.ObservationId, obs.ProviderId, obs.Status, obs.Confidence, null));
}
}
return ValueTask.FromResult(wasCreated
? AppendLinksetResult.Created(linkset, observationsAdded, _sequenceNumber)
: (observationsAdded > 0
? AppendLinksetResult.Updated(linkset, observationsAdded, 0, _sequenceNumber)
: AppendLinksetResult.NoChange(linkset, _sequenceNumber)));
}
}
public ValueTask<AppendLinksetResult> AppendDisagreementAsync(
string tenant,
string vulnerabilityId,
string productKey,
VexObservationDisagreement disagreement,
CancellationToken cancellationToken)
{
lock (_lock)
{
var linksetId = VexLinkset.CreateLinksetId(tenant, vulnerabilityId, productKey);
var key = $"{tenant}|{linksetId}";
var wasCreated = false;
if (!_linksets.TryGetValue(key, out var linkset))
{
wasCreated = true;
var scope = VexProductScope.Unknown(productKey);
linkset = new VexLinkset(
linksetId, tenant, vulnerabilityId, productKey, scope,
Enumerable.Empty<VexLinksetObservationRefModel>(),
null, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow);
}
var allDisagreements = linkset.Disagreements.Append(disagreement);
linkset = linkset.WithObservations(linkset.Observations, allDisagreements);
_linksets[key] = linkset;
_mutations.Add(new LinksetMutationEvent(
++_sequenceNumber, LinksetMutationEvent.MutationTypes.DisagreementAdded,
DateTimeOffset.UtcNow, null, disagreement.ProviderId, disagreement.Status,
disagreement.Confidence, disagreement.Justification));
return ValueTask.FromResult(wasCreated
? AppendLinksetResult.Created(linkset, 0, _sequenceNumber)
: AppendLinksetResult.Updated(linkset, 0, 1, _sequenceNumber));
}
}
public ValueTask<VexLinkset?> GetByIdAsync(string tenant, string linksetId, CancellationToken cancellationToken)
{
lock (_lock)
{
var key = $"{tenant}|{linksetId}";
_linksets.TryGetValue(key, out var linkset);
return ValueTask.FromResult(linkset);
}
}
public ValueTask<VexLinkset?> GetByKeyAsync(string tenant, string vulnerabilityId, string productKey, CancellationToken cancellationToken)
{
var linksetId = VexLinkset.CreateLinksetId(tenant, vulnerabilityId, productKey);
return GetByIdAsync(tenant, linksetId, cancellationToken);
}
public ValueTask<IReadOnlyList<VexLinkset>> FindByVulnerabilityAsync(string tenant, string vulnerabilityId, int limit, CancellationToken cancellationToken)
{
lock (_lock)
{
var results = _linksets.Values
.Where(l => l.Tenant == tenant && string.Equals(l.VulnerabilityId, vulnerabilityId, StringComparison.OrdinalIgnoreCase))
.Take(limit)
.ToList();
return ValueTask.FromResult<IReadOnlyList<VexLinkset>>(results);
}
}
public ValueTask<IReadOnlyList<VexLinkset>> FindByProductKeyAsync(string tenant, string productKey, int limit, CancellationToken cancellationToken)
{
lock (_lock)
{
var results = _linksets.Values
.Where(l => l.Tenant == tenant && string.Equals(l.ProductKey, productKey, StringComparison.OrdinalIgnoreCase))
.Take(limit)
.ToList();
return ValueTask.FromResult<IReadOnlyList<VexLinkset>>(results);
}
}
public ValueTask<IReadOnlyList<VexLinkset>> FindWithConflictsAsync(string tenant, int limit, CancellationToken cancellationToken)
{
lock (_lock)
{
var results = _linksets.Values
.Where(l => l.Tenant == tenant && l.HasConflicts)
.Take(limit)
.ToList();
return ValueTask.FromResult<IReadOnlyList<VexLinkset>>(results);
}
}
public ValueTask<long> CountAsync(string tenant, CancellationToken cancellationToken)
{
lock (_lock)
{
var count = _linksets.Values.Count(l => l.Tenant == tenant);
return ValueTask.FromResult((long)count);
}
}
public ValueTask<long> CountWithConflictsAsync(string tenant, CancellationToken cancellationToken)
{
lock (_lock)
{
var count = _linksets.Values.Count(l => l.Tenant == tenant && l.HasConflicts);
return ValueTask.FromResult((long)count);
}
}
public ValueTask<IReadOnlyList<LinksetMutationEvent>> GetMutationLogAsync(string tenant, string linksetId, CancellationToken cancellationToken)
{
lock (_lock)
{
return ValueTask.FromResult<IReadOnlyList<LinksetMutationEvent>>(_mutations.ToList());
}
}
}

View File

@@ -0,0 +1,231 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Excititor.Core.Testing;
using Xunit;
namespace StellaOps.Excititor.Core.UnitTests.Testing;
public class AuthorityTenantSeederTests
{
[Fact]
public void DefaultTenant_HasExpectedValues()
{
var tenant = AuthorityTenantSeeder.DefaultTenant;
Assert.NotEqual(Guid.Empty, tenant.Id);
Assert.Equal("test", tenant.Slug);
Assert.Equal("Test Tenant", tenant.Name);
Assert.True(tenant.Enabled);
Assert.NotNull(tenant.Settings);
Assert.NotNull(tenant.Metadata);
}
[Fact]
public void MultiTenantFixtures_ContainsThreeTenants()
{
var fixtures = AuthorityTenantSeeder.MultiTenantFixtures;
Assert.Equal(3, fixtures.Length);
Assert.Contains(fixtures, t => t.Slug == "acme");
Assert.Contains(fixtures, t => t.Slug == "beta");
Assert.Contains(fixtures, t => t.Slug == "gamma");
}
[Fact]
public void MultiTenantFixtures_GammaIsDisabled()
{
var gamma = AuthorityTenantSeeder.MultiTenantFixtures.Single(t => t.Slug == "gamma");
Assert.False(gamma.Enabled);
}
[Fact]
public void AirgapTenant_HasRestrictedSettings()
{
var tenant = AuthorityTenantSeeder.AirgapTenant;
Assert.Equal("airgap-test", tenant.Slug);
Assert.False(tenant.Settings.AllowExternalConnectors);
Assert.True(tenant.Settings.AllowAirgapMode);
Assert.Equal("airgap", tenant.Metadata.Environment);
}
[Fact]
public void WithDefaultTenant_AddsTenantToSeedSet()
{
var seeder = new AuthorityTenantSeeder()
.WithDefaultTenant();
var tenants = seeder.GetTenants();
Assert.Single(tenants);
Assert.Equal("test", tenants[0].Slug);
}
[Fact]
public void WithMultiTenantFixtures_AddsAllFixtures()
{
var seeder = new AuthorityTenantSeeder()
.WithMultiTenantFixtures();
var tenants = seeder.GetTenants();
var slugs = seeder.GetSlugs();
Assert.Equal(3, tenants.Count);
Assert.Contains("acme", slugs);
Assert.Contains("beta", slugs);
Assert.Contains("gamma", slugs);
}
[Fact]
public void WithTenant_AddsDuplicateSlugOnce()
{
var seeder = new AuthorityTenantSeeder()
.WithDefaultTenant()
.WithDefaultTenant(); // Duplicate
var tenants = seeder.GetTenants();
Assert.Single(tenants);
}
[Fact]
public void WithCustomTenant_AddsToSeedSet()
{
var customTenant = new TestTenant(
Id: Guid.NewGuid(),
Slug: "custom",
Name: "Custom Tenant",
Description: "A custom test tenant",
Enabled: true,
Settings: TestTenantSettings.Default,
Metadata: new TestTenantMetadata("test", "local", "free", ImmutableArray<string>.Empty));
var seeder = new AuthorityTenantSeeder()
.WithTenant(customTenant);
var tenants = seeder.GetTenants();
Assert.Single(tenants);
Assert.Equal("custom", tenants[0].Slug);
}
[Fact]
public void WithTenant_SimpleOverload_CreatesMinimalTenant()
{
var seeder = new AuthorityTenantSeeder()
.WithTenant("simple", "Simple Tenant", enabled: false);
var tenants = seeder.GetTenants();
Assert.Single(tenants);
Assert.Equal("simple", tenants[0].Slug);
Assert.Equal("Simple Tenant", tenants[0].Name);
Assert.False(tenants[0].Enabled);
}
[Fact]
public void GenerateSql_ProducesValidInsertStatements()
{
var seeder = new AuthorityTenantSeeder()
.WithDefaultTenant();
var sql = seeder.GenerateSql();
Assert.Contains("INSERT INTO auth.tenants", sql);
Assert.Contains("'test'", sql);
Assert.Contains("'Test Tenant'", sql);
Assert.Contains("ON CONFLICT (slug) DO NOTHING", sql);
}
[Fact]
public void GenerateSql_ReturnsEmptyForNoTenants()
{
var seeder = new AuthorityTenantSeeder();
var sql = seeder.GenerateSql();
Assert.Equal(string.Empty, sql);
}
[Fact]
public void GenerateSql_EscapesSingleQuotes()
{
var tenant = new TestTenant(
Id: Guid.NewGuid(),
Slug: "test-escape",
Name: "O'Reilly's Tenant",
Description: "Contains 'quotes'",
Enabled: true,
Settings: TestTenantSettings.Default,
Metadata: TestTenantMetadata.Default);
var seeder = new AuthorityTenantSeeder()
.WithTenant(tenant);
var sql = seeder.GenerateSql();
Assert.Contains("O''Reilly''s Tenant", sql);
}
[Fact]
public void ChainedBuilderPattern_WorksCorrectly()
{
var seeder = new AuthorityTenantSeeder()
.WithDefaultTenant()
.WithMultiTenantFixtures()
.WithAirgapTenant()
.WithTenant("custom", "Custom");
var tenants = seeder.GetTenants();
Assert.Equal(5, tenants.Count); // 1 + 3 + 1 (custom)
// Note: airgap tenant is separate
}
[Fact]
public void TestTenantSettings_Default_HasExpectedValues()
{
var settings = TestTenantSettings.Default;
Assert.Equal(50, settings.MaxProviders);
Assert.Equal(1000, settings.MaxObservationsPerLinkset);
Assert.True(settings.AllowExternalConnectors);
Assert.False(settings.AllowAirgapMode);
Assert.Equal(365, settings.RetentionDays);
}
[Fact]
public void TestTenantSettings_Airgap_HasRestrictedValues()
{
var settings = TestTenantSettings.Airgap;
Assert.Equal(20, settings.MaxProviders);
Assert.Equal(500, settings.MaxObservationsPerLinkset);
Assert.False(settings.AllowExternalConnectors);
Assert.True(settings.AllowAirgapMode);
Assert.Equal(730, settings.RetentionDays);
}
[Fact]
public void TestTenantMetadata_Default_HasExpectedValues()
{
var metadata = TestTenantMetadata.Default;
Assert.Equal("test", metadata.Environment);
Assert.Equal("local", metadata.Region);
Assert.Equal("free", metadata.Tier);
Assert.Empty(metadata.Features);
}
[Fact]
public void MultiTenantFixtures_AcmeHasFeatures()
{
var acme = AuthorityTenantSeeder.MultiTenantFixtures.Single(t => t.Slug == "acme");
Assert.Contains("vex-ingestion", acme.Metadata.Features);
Assert.Contains("policy-engine", acme.Metadata.Features);
Assert.Contains("graph-explorer", acme.Metadata.Features);
}
}