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
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:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user