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

@@ -17,6 +17,7 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
private readonly IReachabilityCache cache;
private readonly IEventsPublisher eventsPublisher;
private readonly IReachabilityScoringService scoringService;
private readonly IRuntimeFactsProvenanceNormalizer provenanceNormalizer;
private readonly ILogger<RuntimeFactsIngestionService> logger;
public RuntimeFactsIngestionService(
@@ -25,6 +26,7 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
IReachabilityCache cache,
IEventsPublisher eventsPublisher,
IReachabilityScoringService scoringService,
IRuntimeFactsProvenanceNormalizer provenanceNormalizer,
ILogger<RuntimeFactsIngestionService> logger)
{
this.factRepository = factRepository ?? throw new ArgumentNullException(nameof(factRepository));
@@ -32,6 +34,7 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
this.eventsPublisher = eventsPublisher ?? throw new ArgumentNullException(nameof(eventsPublisher));
this.scoringService = scoringService ?? throw new ArgumentNullException(nameof(scoringService));
this.provenanceNormalizer = provenanceNormalizer ?? throw new ArgumentNullException(nameof(provenanceNormalizer));
this.logger = logger ?? NullLogger<RuntimeFactsIngestionService>.Instance;
}
@@ -62,6 +65,14 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
document.Metadata["provenance.ingestedAt"] = document.ComputedAt.ToString("O");
document.Metadata["provenance.callgraphId"] = request.CallgraphId;
// Populate context_facts with AOC provenance (SIGNALS-24-003)
document.ContextFacts = provenanceNormalizer.CreateContextFacts(
request.Events,
request.Subject,
request.CallgraphId,
request.Metadata,
document.ComputedAt);
var persisted = await factRepository.UpsertAsync(document, cancellationToken).ConfigureAwait(false);
await cache.SetAsync(persisted, cancellationToken).ConfigureAwait(false);
await eventsPublisher.PublishFactUpdatedAsync(persisted, cancellationToken).ConfigureAwait(false);

View File

@@ -0,0 +1,385 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StellaOps.Signals.Models;
namespace StellaOps.Signals.Services;
/// <summary>
/// Normalizes runtime fact events into AOC provenance records per SIGNALS-24-003.
/// Converts process, socket, and container metadata to <see cref="ProvenanceRecord"/> format.
/// </summary>
public interface IRuntimeFactsProvenanceNormalizer
{
/// <summary>
/// Normalizes runtime fact events into a provenance feed.
/// </summary>
ProvenanceFeed NormalizeToFeed(
IEnumerable<RuntimeFactEvent> events,
ReachabilitySubject subject,
string callgraphId,
Dictionary<string, string?>? metadata,
DateTimeOffset generatedAt);
/// <summary>
/// Creates or updates context facts from runtime events.
/// </summary>
ContextFacts CreateContextFacts(
IEnumerable<RuntimeFactEvent> events,
ReachabilitySubject subject,
string callgraphId,
Dictionary<string, string?>? metadata,
DateTimeOffset timestamp);
}
/// <summary>
/// Default implementation of runtime facts provenance normalizer.
/// </summary>
public sealed class RuntimeFactsProvenanceNormalizer : IRuntimeFactsProvenanceNormalizer
{
private const string SourceService = "signals-runtime-ingestion";
private const double DefaultConfidence = 0.95;
public ProvenanceFeed NormalizeToFeed(
IEnumerable<RuntimeFactEvent> events,
ReachabilitySubject subject,
string callgraphId,
Dictionary<string, string?>? metadata,
DateTimeOffset generatedAt)
{
ArgumentNullException.ThrowIfNull(events);
ArgumentNullException.ThrowIfNull(subject);
var eventsList = events.Where(e => e is not null && !string.IsNullOrWhiteSpace(e.SymbolId)).ToList();
var records = new List<ProvenanceRecord>(eventsList.Count);
foreach (var evt in eventsList)
{
var record = NormalizeEvent(evt, subject, callgraphId, generatedAt);
if (record is not null)
{
records.Add(record);
}
}
var feedMetadata = new Dictionary<string, string?>(StringComparer.Ordinal)
{
["aoc.version"] = "1",
["aoc.contract"] = "SGSI0101",
["callgraphId"] = callgraphId,
["subjectKey"] = subject.ToSubjectKey()
};
if (metadata is not null)
{
foreach (var (key, value) in metadata)
{
feedMetadata[$"request.{key}"] = value;
}
}
return new ProvenanceFeed
{
SchemaVersion = ProvenanceFeed.CurrentSchemaVersion,
FeedId = Guid.NewGuid().ToString("D"),
FeedType = ProvenanceFeedType.RuntimeFacts,
GeneratedAt = generatedAt,
SourceService = SourceService,
CorrelationId = callgraphId,
Records = records,
Metadata = feedMetadata
};
}
public ContextFacts CreateContextFacts(
IEnumerable<RuntimeFactEvent> events,
ReachabilitySubject subject,
string callgraphId,
Dictionary<string, string?>? metadata,
DateTimeOffset timestamp)
{
var feed = NormalizeToFeed(events, subject, callgraphId, metadata, timestamp);
return new ContextFacts
{
Provenance = feed,
LastUpdatedAt = timestamp,
RecordCount = feed.Records.Count
};
}
private static ProvenanceRecord? NormalizeEvent(
RuntimeFactEvent evt,
ReachabilitySubject subject,
string callgraphId,
DateTimeOffset generatedAt)
{
if (string.IsNullOrWhiteSpace(evt.SymbolId))
{
return null;
}
var recordType = DetermineRecordType(evt);
var subjectType = DetermineSubjectType(evt, subject);
var provenanceSubject = new ProvenanceSubject
{
Type = subjectType,
Identifier = BuildSubjectIdentifier(evt, subject),
Digest = NormalizeDigest(evt.SymbolDigest),
Namespace = ExtractNamespace(evt.ContainerId, subject)
};
var facts = new RuntimeProvenanceFacts
{
SymbolId = evt.SymbolId.Trim(),
ProcessName = Normalize(evt.ProcessName),
ProcessId = evt.ProcessId,
SocketAddress = Normalize(evt.SocketAddress),
ContainerId = Normalize(evt.ContainerId),
HitCount = Math.Max(evt.HitCount, 1),
Purl = Normalize(evt.Purl),
CodeId = Normalize(evt.CodeId),
BuildId = Normalize(evt.BuildId),
LoaderBase = Normalize(evt.LoaderBase),
Metadata = evt.Metadata
};
var evidence = BuildEvidence(evt);
return new ProvenanceRecord
{
RecordId = Guid.NewGuid().ToString("D"),
RecordType = recordType,
Subject = provenanceSubject,
OccurredAt = evt.ObservedAt ?? generatedAt,
ObservedBy = DetermineObserver(evt),
Confidence = ComputeConfidence(evt),
Facts = facts,
Evidence = evidence
};
}
private static string DetermineRecordType(RuntimeFactEvent evt)
{
// Determine record type based on available metadata
if (!string.IsNullOrWhiteSpace(evt.ProcessName) || evt.ProcessId.HasValue)
{
return "runtime.process.observed";
}
if (!string.IsNullOrWhiteSpace(evt.SocketAddress))
{
return "runtime.network.connection";
}
if (!string.IsNullOrWhiteSpace(evt.ContainerId))
{
return "runtime.container.activity";
}
if (!string.IsNullOrWhiteSpace(evt.Purl))
{
return "runtime.package.loaded";
}
return "runtime.symbol.invoked";
}
private static ProvenanceSubjectType DetermineSubjectType(RuntimeFactEvent evt, ReachabilitySubject subject)
{
// Priority: container > process > package > file
if (!string.IsNullOrWhiteSpace(evt.ContainerId))
{
return ProvenanceSubjectType.Container;
}
if (!string.IsNullOrWhiteSpace(evt.ProcessName) || evt.ProcessId.HasValue)
{
return ProvenanceSubjectType.Process;
}
if (!string.IsNullOrWhiteSpace(evt.Purl))
{
return ProvenanceSubjectType.Package;
}
if (!string.IsNullOrWhiteSpace(subject.ImageDigest))
{
return ProvenanceSubjectType.Image;
}
return ProvenanceSubjectType.Package;
}
private static string BuildSubjectIdentifier(RuntimeFactEvent evt, ReachabilitySubject subject)
{
// Build identifier based on available data
if (!string.IsNullOrWhiteSpace(evt.Purl))
{
return evt.Purl.Trim();
}
if (!string.IsNullOrWhiteSpace(evt.ContainerId))
{
return evt.ContainerId.Trim();
}
if (!string.IsNullOrWhiteSpace(subject.ImageDigest))
{
return subject.ImageDigest;
}
if (!string.IsNullOrWhiteSpace(subject.Component))
{
return string.IsNullOrWhiteSpace(subject.Version)
? subject.Component
: $"{subject.Component}@{subject.Version}";
}
return evt.SymbolId.Trim();
}
private static string? NormalizeDigest(string? digest)
{
if (string.IsNullOrWhiteSpace(digest))
{
return null;
}
var trimmed = digest.Trim();
// Ensure sha256: prefix for valid hex digests
if (trimmed.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
{
return trimmed.ToLowerInvariant();
}
// If it looks like a hex digest (64 chars), add prefix
if (trimmed.Length == 64 && IsHexString(trimmed))
{
return $"sha256:{trimmed.ToLowerInvariant()}";
}
return trimmed;
}
private static bool IsHexString(string value)
{
foreach (var c in value)
{
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
{
return false;
}
}
return true;
}
private static string? ExtractNamespace(string? containerId, ReachabilitySubject subject)
{
// Try to extract namespace from container ID or subject metadata
if (!string.IsNullOrWhiteSpace(containerId) && containerId.Contains('/'))
{
var parts = containerId.Split('/');
if (parts.Length > 1)
{
return parts[0];
}
}
return null;
}
private static RecordEvidence? BuildEvidence(RuntimeFactEvent evt)
{
if (string.IsNullOrWhiteSpace(evt.EvidenceUri) && string.IsNullOrWhiteSpace(evt.SymbolDigest))
{
return null;
}
var captureMethod = DetermineCaptureMethod(evt);
return new RecordEvidence
{
SourceDigest = NormalizeDigest(evt.SymbolDigest),
CaptureMethod = captureMethod,
RawDataRef = Normalize(evt.EvidenceUri)
};
}
private static EvidenceCaptureMethod? DetermineCaptureMethod(RuntimeFactEvent evt)
{
// Infer capture method from event metadata
if (evt.Metadata is not null)
{
if (evt.Metadata.TryGetValue("captureMethod", out var method) && !string.IsNullOrWhiteSpace(method))
{
return method.ToUpperInvariant() switch
{
"EBPF" => EvidenceCaptureMethod.EBpf,
"PROC_SCAN" => EvidenceCaptureMethod.ProcScan,
"API_CALL" => EvidenceCaptureMethod.ApiCall,
"LOG_ANALYSIS" => EvidenceCaptureMethod.LogAnalysis,
"STATIC_ANALYSIS" => EvidenceCaptureMethod.StaticAnalysis,
_ => null
};
}
}
// Default based on available data
if (evt.ProcessId.HasValue || !string.IsNullOrWhiteSpace(evt.ProcessName))
{
return EvidenceCaptureMethod.ProcScan;
}
return EvidenceCaptureMethod.ApiCall;
}
private static string? DetermineObserver(RuntimeFactEvent evt)
{
if (evt.Metadata is not null && evt.Metadata.TryGetValue("observer", out var observer))
{
return Normalize(observer);
}
if (!string.IsNullOrWhiteSpace(evt.ContainerId))
{
return "container-runtime-agent";
}
if (evt.ProcessId.HasValue)
{
return "process-monitor-agent";
}
return "signals-ingestion";
}
private static double ComputeConfidence(RuntimeFactEvent evt)
{
// Base confidence
var confidence = DefaultConfidence;
// Adjust based on available evidence
if (!string.IsNullOrWhiteSpace(evt.SymbolDigest))
{
confidence = Math.Min(confidence + 0.02, 1.0);
}
if (!string.IsNullOrWhiteSpace(evt.EvidenceUri))
{
confidence = Math.Min(confidence + 0.01, 1.0);
}
if (evt.ProcessId.HasValue && !string.IsNullOrWhiteSpace(evt.ProcessName))
{
confidence = Math.Min(confidence + 0.01, 1.0);
}
return Math.Round(confidence, 2);
}
private static string? Normalize(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}