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

@@ -3,6 +3,12 @@ namespace StellaOps.Excititor.Core;
/// <summary>
/// Baseline consensus policy applying tier-based weights and enforcing justification gates.
/// </summary>
/// <remarks>
/// DEPRECATED: Consensus logic is being removed per AOC-19 contract.
/// Use append-only linksets with <see cref="StellaOps.Excititor.Core.Observations.IAppendOnlyLinksetStore"/>
/// and let downstream policy engines make verdicts.
/// </remarks>
[Obsolete("Consensus logic is deprecated per AOC-19. Use append-only linksets instead.", DiagnosticId = "EXCITITOR001")]
public sealed class BaselineVexConsensusPolicy : IVexConsensusPolicy
{
private readonly VexConsensusPolicyOptions _options;

View File

@@ -3,6 +3,12 @@ namespace StellaOps.Excititor.Core;
/// <summary>
/// Policy abstraction supplying trust weights and gating logic for consensus decisions.
/// </summary>
/// <remarks>
/// DEPRECATED: Consensus logic is being removed per AOC-19 contract.
/// Use append-only linksets with <see cref="StellaOps.Excititor.Core.Observations.IAppendOnlyLinksetStore"/>
/// and let downstream policy engines make verdicts.
/// </remarks>
[Obsolete("Consensus logic is deprecated per AOC-19. Use append-only linksets instead.", DiagnosticId = "EXCITITOR001")]
public interface IVexConsensusPolicy
{
/// <summary>

View File

@@ -0,0 +1,340 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Core.Canonicalization;
namespace StellaOps.Excititor.Core.Observations;
/// <summary>
/// Extracts linkset updates from VEX observations using append-only semantics (AOC-19-013).
/// Replaces consensus-based extraction with deterministic append-only operations.
/// </summary>
public sealed class AppendOnlyLinksetExtractionService
{
private readonly IAppendOnlyLinksetStore _store;
private readonly IVexLinksetEventPublisher? _eventPublisher;
private readonly ILogger<AppendOnlyLinksetExtractionService> _logger;
public AppendOnlyLinksetExtractionService(
IAppendOnlyLinksetStore store,
ILogger<AppendOnlyLinksetExtractionService> logger,
IVexLinksetEventPublisher? eventPublisher = null)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_eventPublisher = eventPublisher;
}
/// <summary>
/// Processes observations and appends them to linksets.
/// Returns linkset update events for downstream consumers.
/// </summary>
public async Task<ImmutableArray<LinksetAppendResult>> ProcessObservationsAsync(
string tenant,
IEnumerable<VexObservation> observations,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(tenant))
{
throw new ArgumentException("Tenant must be provided.", nameof(tenant));
}
if (observations is null)
{
return ImmutableArray<LinksetAppendResult>.Empty;
}
var normalizedTenant = tenant.Trim().ToLowerInvariant();
var observationList = observations.Where(o => o is not null).ToList();
if (observationList.Count == 0)
{
return ImmutableArray<LinksetAppendResult>.Empty;
}
// Group by (vulnerabilityId, productKey) deterministically
var groups = observationList
.SelectMany(obs => obs.Statements.Select(stmt => (obs, stmt)))
.GroupBy(x => new LinksetKey(
VulnerabilityId: Normalize(x.stmt.VulnerabilityId),
ProductKey: Normalize(x.stmt.ProductKey)))
.OrderBy(g => g.Key.VulnerabilityId, StringComparer.OrdinalIgnoreCase)
.ThenBy(g => g.Key.ProductKey, StringComparer.OrdinalIgnoreCase)
.ToList();
var results = new List<LinksetAppendResult>(groups.Count);
foreach (var group in groups)
{
try
{
var result = await ProcessGroupAsync(
normalizedTenant,
group.Key,
group.Select(x => x.obs).Distinct(),
cancellationToken);
results.Add(result);
if (result.HadChanges && _eventPublisher is not null)
{
await _eventPublisher.PublishLinksetUpdatedAsync(
normalizedTenant,
result.Linkset,
cancellationToken);
}
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Failed to process linkset for tenant {Tenant}, vulnerability {VulnerabilityId}, product {ProductKey}",
normalizedTenant,
group.Key.VulnerabilityId,
group.Key.ProductKey);
results.Add(LinksetAppendResult.Failed(
normalizedTenant,
group.Key.VulnerabilityId,
group.Key.ProductKey,
ex.Message));
}
}
_logger.LogInformation(
"Processed {ObservationCount} observations into {LinksetCount} linksets for tenant {Tenant}",
observationList.Count,
results.Count(r => r.Success),
normalizedTenant);
return results.ToImmutableArray();
}
/// <summary>
/// Appends a disagreement to a linkset.
/// </summary>
public async Task<LinksetAppendResult> AppendDisagreementAsync(
string tenant,
string vulnerabilityId,
string productKey,
VexObservationDisagreement disagreement,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(tenant))
{
throw new ArgumentException("Tenant must be provided.", nameof(tenant));
}
if (disagreement is null)
{
throw new ArgumentNullException(nameof(disagreement));
}
var normalizedTenant = tenant.Trim().ToLowerInvariant();
var normalizedVuln = Normalize(vulnerabilityId);
var normalizedProduct = Normalize(productKey);
try
{
var storeResult = await _store.AppendDisagreementAsync(
normalizedTenant,
normalizedVuln,
normalizedProduct,
disagreement,
cancellationToken);
if (storeResult.HadChanges && _eventPublisher is not null)
{
await _eventPublisher.PublishLinksetUpdatedAsync(
normalizedTenant,
storeResult.Linkset,
cancellationToken);
}
return LinksetAppendResult.Succeeded(
normalizedTenant,
normalizedVuln,
normalizedProduct,
storeResult.Linkset,
storeResult.WasCreated,
storeResult.ObservationsAdded,
storeResult.DisagreementsAdded,
storeResult.SequenceNumber);
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Failed to append disagreement for tenant {Tenant}, vulnerability {VulnerabilityId}, product {ProductKey}",
normalizedTenant,
normalizedVuln,
normalizedProduct);
return LinksetAppendResult.Failed(
normalizedTenant,
normalizedVuln,
normalizedProduct,
ex.Message);
}
}
private async Task<LinksetAppendResult> ProcessGroupAsync(
string tenant,
LinksetKey key,
IEnumerable<VexObservation> observations,
CancellationToken cancellationToken)
{
var scope = BuildScope(key.ProductKey);
var observationRefs = observations
.SelectMany(obs => obs.Statements
.Where(stmt => string.Equals(Normalize(stmt.VulnerabilityId), key.VulnerabilityId, StringComparison.OrdinalIgnoreCase)
&& string.Equals(Normalize(stmt.ProductKey), key.ProductKey, StringComparison.OrdinalIgnoreCase))
.Select(stmt => new VexLinksetObservationRefModel(
ObservationId: obs.ObservationId,
ProviderId: obs.ProviderId,
Status: stmt.Status.ToString().ToLowerInvariant(),
Confidence: null)))
.Distinct(VexLinksetObservationRefComparer.Instance)
.ToList();
if (observationRefs.Count == 0)
{
return LinksetAppendResult.NoChange(tenant, key.VulnerabilityId, key.ProductKey);
}
var storeResult = await _store.AppendObservationsBatchAsync(
tenant,
key.VulnerabilityId,
key.ProductKey,
observationRefs,
scope,
cancellationToken);
return LinksetAppendResult.Succeeded(
tenant,
key.VulnerabilityId,
key.ProductKey,
storeResult.Linkset,
storeResult.WasCreated,
storeResult.ObservationsAdded,
storeResult.DisagreementsAdded,
storeResult.SequenceNumber);
}
private static VexProductScope BuildScope(string productKey)
{
var canonicalizer = new VexProductKeyCanonicalizer();
try
{
var canonical = canonicalizer.Canonicalize(productKey);
var identifiers = canonical.Links
.Where(link => link is not null && !string.IsNullOrWhiteSpace(link.Identifier))
.Select(link => link.Identifier.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
var purl = canonical.Links.FirstOrDefault(link =>
string.Equals(link.Type, "purl", StringComparison.OrdinalIgnoreCase))?.Identifier;
var cpe = canonical.Links.FirstOrDefault(link =>
string.Equals(link.Type, "cpe", StringComparison.OrdinalIgnoreCase))?.Identifier;
var version = ExtractVersion(purl ?? canonical.ProductKey);
return new VexProductScope(
ProductKey: canonical.ProductKey,
Type: canonical.Scope.ToString().ToLowerInvariant(),
Version: version,
Purl: purl,
Cpe: cpe,
Identifiers: identifiers);
}
catch
{
return VexProductScope.Unknown(productKey);
}
}
private static string? ExtractVersion(string? key)
{
if (string.IsNullOrWhiteSpace(key))
{
return null;
}
var at = key.LastIndexOf('@');
return at >= 0 && at < key.Length - 1 ? key[(at + 1)..] : null;
}
private static string Normalize(string value) =>
VexObservation.EnsureNotNullOrWhiteSpace(value, nameof(value));
private sealed record LinksetKey(string VulnerabilityId, string ProductKey);
}
/// <summary>
/// Result of a linkset append operation.
/// </summary>
public sealed record LinksetAppendResult
{
private LinksetAppendResult(
string tenant,
string vulnerabilityId,
string productKey,
VexLinkset? linkset,
bool success,
bool wasCreated,
int observationsAdded,
int disagreementsAdded,
long sequenceNumber,
string? errorMessage)
{
Tenant = tenant;
VulnerabilityId = vulnerabilityId;
ProductKey = productKey;
Linkset = linkset;
Success = success;
WasCreated = wasCreated;
ObservationsAdded = observationsAdded;
DisagreementsAdded = disagreementsAdded;
SequenceNumber = sequenceNumber;
ErrorMessage = errorMessage;
}
public string Tenant { get; }
public string VulnerabilityId { get; }
public string ProductKey { get; }
public VexLinkset? Linkset { get; }
public bool Success { get; }
public bool WasCreated { get; }
public int ObservationsAdded { get; }
public int DisagreementsAdded { get; }
public long SequenceNumber { get; }
public string? ErrorMessage { get; }
public bool HadChanges => Success && (WasCreated || ObservationsAdded > 0 || DisagreementsAdded > 0);
public static LinksetAppendResult Succeeded(
string tenant,
string vulnerabilityId,
string productKey,
VexLinkset linkset,
bool wasCreated,
int observationsAdded,
int disagreementsAdded,
long sequenceNumber)
=> new(tenant, vulnerabilityId, productKey, linkset, success: true,
wasCreated, observationsAdded, disagreementsAdded, sequenceNumber, errorMessage: null);
public static LinksetAppendResult NoChange(
string tenant,
string vulnerabilityId,
string productKey)
=> new(tenant, vulnerabilityId, productKey, linkset: null, success: true,
wasCreated: false, observationsAdded: 0, disagreementsAdded: 0, sequenceNumber: 0, errorMessage: null);
public static LinksetAppendResult Failed(
string tenant,
string vulnerabilityId,
string productKey,
string errorMessage)
=> new(tenant, vulnerabilityId, productKey, linkset: null, success: false,
wasCreated: false, observationsAdded: 0, disagreementsAdded: 0, sequenceNumber: 0, errorMessage);
}

View File

@@ -0,0 +1,250 @@
namespace StellaOps.Excititor.Core.Observations;
/// <summary>
/// Append-only linkset store interface enforcing AOC-19 contract.
/// Linksets can only be appended (new observations added), never modified or deleted.
/// This guarantees deterministic replay and audit trails.
/// </summary>
public interface IAppendOnlyLinksetStore
{
/// <summary>
/// Appends a new observation to an existing linkset or creates a new linkset.
/// Returns the updated linkset with the new observation appended.
/// Thread-safe and idempotent (duplicate observations are deduplicated).
/// </summary>
/// <param name="tenant">Tenant identifier.</param>
/// <param name="vulnerabilityId">Vulnerability identifier (CVE, GHSA, etc.).</param>
/// <param name="productKey">Product key (PURL, CPE, etc.).</param>
/// <param name="observation">The observation reference to append.</param>
/// <param name="scope">Product scope metadata.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The updated linkset with the appended observation.</returns>
ValueTask<AppendLinksetResult> AppendObservationAsync(
string tenant,
string vulnerabilityId,
string productKey,
VexLinksetObservationRefModel observation,
VexProductScope scope,
CancellationToken cancellationToken);
/// <summary>
/// Appends multiple observations to a linkset in a single atomic operation.
/// </summary>
ValueTask<AppendLinksetResult> AppendObservationsBatchAsync(
string tenant,
string vulnerabilityId,
string productKey,
IEnumerable<VexLinksetObservationRefModel> observations,
VexProductScope scope,
CancellationToken cancellationToken);
/// <summary>
/// Appends a disagreement annotation to an existing linkset.
/// Disagreements are append-only and never removed.
/// </summary>
ValueTask<AppendLinksetResult> AppendDisagreementAsync(
string tenant,
string vulnerabilityId,
string productKey,
VexObservationDisagreement disagreement,
CancellationToken cancellationToken);
/// <summary>
/// Retrieves a linkset by tenant and linkset ID (read-only).
/// </summary>
ValueTask<VexLinkset?> GetByIdAsync(
string tenant,
string linksetId,
CancellationToken cancellationToken);
/// <summary>
/// Retrieves a linkset by vulnerability and product key (read-only).
/// </summary>
ValueTask<VexLinkset?> GetByKeyAsync(
string tenant,
string vulnerabilityId,
string productKey,
CancellationToken cancellationToken);
/// <summary>
/// Finds linksets by vulnerability ID (read-only).
/// </summary>
ValueTask<IReadOnlyList<VexLinkset>> FindByVulnerabilityAsync(
string tenant,
string vulnerabilityId,
int limit,
CancellationToken cancellationToken);
/// <summary>
/// Finds linksets by product key (read-only).
/// </summary>
ValueTask<IReadOnlyList<VexLinkset>> FindByProductKeyAsync(
string tenant,
string productKey,
int limit,
CancellationToken cancellationToken);
/// <summary>
/// Finds linksets with conflicts/disagreements (read-only).
/// </summary>
ValueTask<IReadOnlyList<VexLinkset>> FindWithConflictsAsync(
string tenant,
int limit,
CancellationToken cancellationToken);
/// <summary>
/// Returns the count of linksets for the specified tenant.
/// </summary>
ValueTask<long> CountAsync(
string tenant,
CancellationToken cancellationToken);
/// <summary>
/// Returns the count of linksets with conflicts for the specified tenant.
/// </summary>
ValueTask<long> CountWithConflictsAsync(
string tenant,
CancellationToken cancellationToken);
/// <summary>
/// Gets the append-only event log for a specific linkset.
/// Returns all mutations in chronological order for audit/replay.
/// </summary>
ValueTask<IReadOnlyList<LinksetMutationEvent>> GetMutationLogAsync(
string tenant,
string linksetId,
CancellationToken cancellationToken);
}
/// <summary>
/// Result of an append operation on a linkset.
/// </summary>
public sealed record AppendLinksetResult
{
private AppendLinksetResult(
VexLinkset linkset,
bool wasCreated,
int observationsAdded,
int disagreementsAdded,
long sequenceNumber)
{
Linkset = linkset ?? throw new ArgumentNullException(nameof(linkset));
WasCreated = wasCreated;
ObservationsAdded = observationsAdded;
DisagreementsAdded = disagreementsAdded;
SequenceNumber = sequenceNumber;
}
/// <summary>
/// The updated linkset.
/// </summary>
public VexLinkset Linkset { get; }
/// <summary>
/// True if the linkset was newly created by this operation.
/// </summary>
public bool WasCreated { get; }
/// <summary>
/// Number of new observations added (0 if deduplicated).
/// </summary>
public int ObservationsAdded { get; }
/// <summary>
/// Number of new disagreements added (0 if deduplicated).
/// </summary>
public int DisagreementsAdded { get; }
/// <summary>
/// Monotonic sequence number for this mutation (for ordering/replay).
/// </summary>
public long SequenceNumber { get; }
/// <summary>
/// True if any data was actually appended.
/// </summary>
public bool HadChanges => WasCreated || ObservationsAdded > 0 || DisagreementsAdded > 0;
public static AppendLinksetResult Created(VexLinkset linkset, int observationsAdded, long sequenceNumber)
=> new(linkset, wasCreated: true, observationsAdded, disagreementsAdded: 0, sequenceNumber);
public static AppendLinksetResult Updated(VexLinkset linkset, int observationsAdded, int disagreementsAdded, long sequenceNumber)
=> new(linkset, wasCreated: false, observationsAdded, disagreementsAdded, sequenceNumber);
public static AppendLinksetResult NoChange(VexLinkset linkset, long sequenceNumber)
=> new(linkset, wasCreated: false, observationsAdded: 0, disagreementsAdded: 0, sequenceNumber);
}
/// <summary>
/// Represents a mutation event in the append-only linkset log.
/// Used for audit trails and deterministic replay.
/// </summary>
public sealed record LinksetMutationEvent
{
public LinksetMutationEvent(
long sequenceNumber,
string mutationType,
DateTimeOffset timestamp,
string? observationId,
string? providerId,
string? status,
double? confidence,
string? justification)
{
SequenceNumber = sequenceNumber;
MutationType = mutationType ?? throw new ArgumentNullException(nameof(mutationType));
Timestamp = timestamp.ToUniversalTime();
ObservationId = observationId;
ProviderId = providerId;
Status = status;
Confidence = confidence;
Justification = justification;
}
/// <summary>
/// Monotonic sequence number for ordering.
/// </summary>
public long SequenceNumber { get; }
/// <summary>
/// Type of mutation: "observation_added", "disagreement_added", "linkset_created".
/// </summary>
public string MutationType { get; }
/// <summary>
/// When this mutation occurred.
/// </summary>
public DateTimeOffset Timestamp { get; }
/// <summary>
/// Observation ID (for observation mutations).
/// </summary>
public string? ObservationId { get; }
/// <summary>
/// Provider ID.
/// </summary>
public string? ProviderId { get; }
/// <summary>
/// Status value.
/// </summary>
public string? Status { get; }
/// <summary>
/// Confidence value.
/// </summary>
public double? Confidence { get; }
/// <summary>
/// Justification (for disagreement mutations).
/// </summary>
public string? Justification { get; }
public static class MutationTypes
{
public const string LinksetCreated = "linkset_created";
public const string ObservationAdded = "observation_added";
public const string DisagreementAdded = "disagreement_added";
}
}

View File

@@ -0,0 +1,264 @@
using System.Collections.Immutable;
using System.Text.Json;
namespace StellaOps.Excititor.Core.Testing;
/// <summary>
/// Utility for seeding Authority tenants in test scenarios (AOC-19-004).
/// Provides deterministic tenant fixtures with configurable settings.
/// </summary>
public sealed class AuthorityTenantSeeder
{
private readonly List<TestTenant> _tenants = new();
private readonly HashSet<string> _usedSlugs = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Default test tenant for single-tenant scenarios.
/// </summary>
public static TestTenant DefaultTenant { get; } = new TestTenant(
Id: Guid.Parse("00000000-0000-0000-0000-000000000001"),
Slug: "test",
Name: "Test Tenant",
Description: "Default test tenant for unit tests",
Enabled: true,
Settings: TestTenantSettings.Default,
Metadata: TestTenantMetadata.Default);
/// <summary>
/// Multi-tenant test fixtures (Acme, Beta, Gamma).
/// </summary>
public static ImmutableArray<TestTenant> MultiTenantFixtures { get; } = ImmutableArray.Create(
new TestTenant(
Id: Guid.Parse("00000000-0000-0000-0000-000000000010"),
Slug: "acme",
Name: "Acme Corp",
Description: "Primary test tenant",
Enabled: true,
Settings: TestTenantSettings.Default,
Metadata: new TestTenantMetadata(
Environment: "test",
Region: "us-east-1",
Tier: "enterprise",
Features: ImmutableArray.Create("vex-ingestion", "policy-engine", "graph-explorer"))),
new TestTenant(
Id: Guid.Parse("00000000-0000-0000-0000-000000000020"),
Slug: "beta",
Name: "Beta Inc",
Description: "Secondary test tenant",
Enabled: true,
Settings: TestTenantSettings.Default with { MaxProviders = 5 },
Metadata: new TestTenantMetadata(
Environment: "test",
Region: "eu-west-1",
Tier: "professional",
Features: ImmutableArray.Create("vex-ingestion"))),
new TestTenant(
Id: Guid.Parse("00000000-0000-0000-0000-000000000030"),
Slug: "gamma",
Name: "Gamma Ltd",
Description: "Disabled test tenant",
Enabled: false,
Settings: TestTenantSettings.Default,
Metadata: TestTenantMetadata.Default));
/// <summary>
/// Airgap test tenant with restricted settings.
/// </summary>
public static TestTenant AirgapTenant { get; } = new TestTenant(
Id: Guid.Parse("00000000-0000-0000-0000-000000000099"),
Slug: "airgap-test",
Name: "Airgap Test Tenant",
Description: "Tenant for airgap/offline testing",
Enabled: true,
Settings: TestTenantSettings.Airgap,
Metadata: new TestTenantMetadata(
Environment: "airgap",
Region: "isolated",
Tier: "enterprise",
Features: ImmutableArray.Create("vex-ingestion", "offline-mode", "mirror-import")));
/// <summary>
/// Creates a new seeder instance.
/// </summary>
public AuthorityTenantSeeder()
{
}
/// <summary>
/// Adds the default test tenant to the seed set.
/// </summary>
public AuthorityTenantSeeder WithDefaultTenant()
{
AddTenant(DefaultTenant);
return this;
}
/// <summary>
/// Adds multi-tenant fixtures to the seed set.
/// </summary>
public AuthorityTenantSeeder WithMultiTenantFixtures()
{
foreach (var tenant in MultiTenantFixtures)
{
AddTenant(tenant);
}
return this;
}
/// <summary>
/// Adds the airgap test tenant to the seed set.
/// </summary>
public AuthorityTenantSeeder WithAirgapTenant()
{
AddTenant(AirgapTenant);
return this;
}
/// <summary>
/// Adds a custom tenant to the seed set.
/// </summary>
public AuthorityTenantSeeder WithTenant(TestTenant tenant)
{
AddTenant(tenant);
return this;
}
/// <summary>
/// Adds a custom tenant with minimal configuration.
/// </summary>
public AuthorityTenantSeeder WithTenant(string slug, string name, bool enabled = true)
{
var tenant = new TestTenant(
Id: Guid.NewGuid(),
Slug: slug,
Name: name,
Description: null,
Enabled: enabled,
Settings: TestTenantSettings.Default,
Metadata: TestTenantMetadata.Default);
AddTenant(tenant);
return this;
}
/// <summary>
/// Gets all tenants in the seed set.
/// </summary>
public IReadOnlyList<TestTenant> GetTenants() => _tenants.ToList();
/// <summary>
/// Gets tenant slugs for use in test data generation.
/// </summary>
public IReadOnlyList<string> GetSlugs() => _tenants.Select(t => t.Slug).ToList();
/// <summary>
/// Generates SQL INSERT statements for seeding tenants.
/// </summary>
public string GenerateSql()
{
if (_tenants.Count == 0)
{
return string.Empty;
}
var sb = new System.Text.StringBuilder();
sb.AppendLine("-- Authority tenant seed data (AOC-19-004)");
sb.AppendLine("INSERT INTO auth.tenants (id, slug, name, description, contact_email, enabled, settings, metadata, created_at, updated_at, created_by)");
sb.AppendLine("VALUES");
var now = DateTimeOffset.UtcNow;
var lines = new List<string>();
foreach (var tenant in _tenants)
{
var settingsJson = JsonSerializer.Serialize(tenant.Settings, JsonOptions);
var metadataJson = JsonSerializer.Serialize(tenant.Metadata, JsonOptions);
lines.Add($" ('{tenant.Id}', '{EscapeSql(tenant.Slug)}', '{EscapeSql(tenant.Name)}', {NullableString(tenant.Description)}, NULL, {(tenant.Enabled ? "TRUE" : "FALSE")}, '{EscapeSql(settingsJson)}', '{EscapeSql(metadataJson)}', '{now:O}', '{now:O}', 'test-seeder')");
}
sb.AppendLine(string.Join(",\n", lines));
sb.AppendLine("ON CONFLICT (slug) DO NOTHING;");
return sb.ToString();
}
private void AddTenant(TestTenant tenant)
{
if (_usedSlugs.Contains(tenant.Slug))
{
return;
}
_usedSlugs.Add(tenant.Slug);
_tenants.Add(tenant);
}
private static string EscapeSql(string value) => value.Replace("'", "''");
private static string NullableString(string? value) =>
value is null ? "NULL" : $"'{EscapeSql(value)}'";
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = false
};
}
/// <summary>
/// Test tenant fixture record.
/// </summary>
public sealed record TestTenant(
Guid Id,
string Slug,
string Name,
string? Description,
bool Enabled,
TestTenantSettings Settings,
TestTenantMetadata Metadata);
/// <summary>
/// Test tenant settings.
/// </summary>
public sealed record TestTenantSettings(
int MaxProviders,
int MaxObservationsPerLinkset,
bool AllowExternalConnectors,
bool AllowAirgapMode,
int RetentionDays)
{
/// <summary>
/// Default test tenant settings.
/// </summary>
public static TestTenantSettings Default { get; } = new TestTenantSettings(
MaxProviders: 50,
MaxObservationsPerLinkset: 1000,
AllowExternalConnectors: true,
AllowAirgapMode: false,
RetentionDays: 365);
/// <summary>
/// Airgap-mode tenant settings.
/// </summary>
public static TestTenantSettings Airgap { get; } = new TestTenantSettings(
MaxProviders: 20,
MaxObservationsPerLinkset: 500,
AllowExternalConnectors: false,
AllowAirgapMode: true,
RetentionDays: 730);
}
/// <summary>
/// Test tenant metadata.
/// </summary>
public sealed record TestTenantMetadata(
string Environment,
string Region,
string Tier,
ImmutableArray<string> Features)
{
/// <summary>
/// Default test tenant metadata.
/// </summary>
public static TestTenantMetadata Default { get; } = new TestTenantMetadata(
Environment: "test",
Region: "local",
Tier: "free",
Features: ImmutableArray<string>.Empty);
}

View File

@@ -3,6 +3,15 @@ using System.Runtime.Serialization;
namespace StellaOps.Excititor.Core;
/// <summary>
/// Represents a VEX consensus result from weighted voting.
/// </summary>
/// <remarks>
/// DEPRECATED: Consensus logic is being removed per AOC-19 contract.
/// Use append-only linksets with <see cref="StellaOps.Excititor.Core.Observations.IAppendOnlyLinksetStore"/>
/// and let downstream policy engines make verdicts.
/// </remarks>
[Obsolete("Consensus logic is deprecated per AOC-19. Use append-only linksets instead.", DiagnosticId = "EXCITITOR001")]
public sealed record VexConsensus
{
public VexConsensus(

View File

@@ -2,6 +2,15 @@ using System.Collections.Immutable;
namespace StellaOps.Excititor.Core;
/// <summary>
/// Configuration options for consensus policy weights.
/// </summary>
/// <remarks>
/// DEPRECATED: Consensus logic is being removed per AOC-19 contract.
/// Use append-only linksets with <see cref="StellaOps.Excititor.Core.Observations.IAppendOnlyLinksetStore"/>
/// and let downstream policy engines make verdicts.
/// </remarks>
[Obsolete("Consensus logic is deprecated per AOC-19. Use append-only linksets instead.", DiagnosticId = "EXCITITOR001")]
public sealed record VexConsensusPolicyOptions
{
public const string BaselineVersion = "baseline/v1";

View File

@@ -3,6 +3,15 @@ using System.Globalization;
namespace StellaOps.Excititor.Core;
/// <summary>
/// Resolves VEX consensus from multiple claims using weighted voting.
/// </summary>
/// <remarks>
/// DEPRECATED: Consensus logic is being removed per AOC-19 contract.
/// Use append-only linksets with <see cref="StellaOps.Excititor.Core.Observations.IAppendOnlyLinksetStore"/>
/// and let downstream policy engines make verdicts.
/// </remarks>
[Obsolete("Consensus logic is deprecated per AOC-19. Use append-only linksets instead.", DiagnosticId = "EXCITITOR001")]
public sealed class VexConsensusResolver
{
private readonly IVexConsensusPolicy _policy;
@@ -273,6 +282,14 @@ public sealed class VexConsensusResolver
};
}
/// <summary>
/// Request model for consensus resolution.
/// </summary>
/// <remarks>
/// DEPRECATED: Consensus logic is being removed per AOC-19 contract.
/// </remarks>
#pragma warning disable EXCITITOR001 // Using obsolete VexConsensusPolicyOptions
[Obsolete("Consensus logic is deprecated per AOC-19. Use append-only linksets instead.", DiagnosticId = "EXCITITOR001")]
public sealed record VexConsensusRequest(
string VulnerabilityId,
VexProduct Product,
@@ -283,11 +300,26 @@ public sealed record VexConsensusRequest(
VexSignalSnapshot? Signals = null,
string? PolicyRevisionId = null,
string? PolicyDigest = null);
#pragma warning restore EXCITITOR001
/// <summary>
/// Result of consensus resolution including decision log.
/// </summary>
/// <remarks>
/// DEPRECATED: Consensus logic is being removed per AOC-19 contract.
/// </remarks>
[Obsolete("Consensus logic is deprecated per AOC-19. Use append-only linksets instead.", DiagnosticId = "EXCITITOR001")]
public sealed record VexConsensusResolution(
VexConsensus Consensus,
ImmutableArray<VexConsensusDecisionTelemetry> DecisionLog);
/// <summary>
/// Telemetry record for consensus decision auditing.
/// </summary>
/// <remarks>
/// DEPRECATED: Consensus logic is being removed per AOC-19 contract.
/// </remarks>
[Obsolete("Consensus logic is deprecated per AOC-19. Use append-only linksets instead.", DiagnosticId = "EXCITITOR001")]
public sealed record VexConsensusDecisionTelemetry(
string ProviderId,
string DocumentDigest,