using Microsoft.Extensions.Logging; using StellaOps.Scanner.Surface.Models; namespace StellaOps.Scanner.Surface.Signals; /// /// Interface for emitting surface analysis signals for policy evaluation. /// public interface ISurfaceSignalEmitter { /// /// Emits signals for the given analysis result. /// Task EmitAsync( string scanId, SurfaceAnalysisResult result, CancellationToken cancellationToken = default); /// /// Emits custom signals. /// Task EmitAsync( string scanId, IDictionary signals, CancellationToken cancellationToken = default); } /// /// Default implementation of surface signal emitter. /// Converts analysis results to policy signals. /// public sealed class SurfaceSignalEmitter : ISurfaceSignalEmitter { private readonly ILogger _logger; private readonly ISurfaceSignalSink? _sink; public SurfaceSignalEmitter( ILogger logger, ISurfaceSignalSink? sink = null) { _logger = logger; _sink = sink; } public async Task EmitAsync( string scanId, SurfaceAnalysisResult result, CancellationToken cancellationToken = default) { var signals = BuildSignals(result); await EmitAsync(scanId, signals, cancellationToken); } public async Task EmitAsync( string scanId, IDictionary signals, CancellationToken cancellationToken = default) { _logger.LogDebug( "Emitting {SignalCount} surface signals for scan {ScanId}", signals.Count, scanId); if (_sink != null) { await _sink.WriteAsync(scanId, signals, cancellationToken); } else { _logger.LogDebug( "No signal sink configured, signals for scan {ScanId}: {Signals}", scanId, string.Join(", ", signals.Select(kv => $"{kv.Key}={kv.Value}"))); } } private static Dictionary BuildSignals(SurfaceAnalysisResult result) { var signals = new Dictionary { [SurfaceSignalKeys.TotalSurfaceArea] = result.Summary.TotalEntries, [SurfaceSignalKeys.RiskScore] = result.Summary.RiskScore, [SurfaceSignalKeys.HighConfidenceCount] = result.Entries .Count(e => e.Confidence >= ConfidenceLevel.High) }; // Add counts by type foreach (var (type, count) in result.Summary.ByType) { var key = type switch { SurfaceType.NetworkEndpoint => SurfaceSignalKeys.NetworkEndpoints, SurfaceType.FileOperation => SurfaceSignalKeys.FileOperations, SurfaceType.ProcessExecution => SurfaceSignalKeys.ProcessSpawns, SurfaceType.CryptoOperation => SurfaceSignalKeys.CryptoUsage, SurfaceType.AuthenticationPoint => SurfaceSignalKeys.AuthPoints, SurfaceType.InputHandling => SurfaceSignalKeys.InputHandlers, SurfaceType.SecretAccess => SurfaceSignalKeys.SecretAccess, SurfaceType.ExternalCall => SurfaceSignalKeys.ExternalCalls, SurfaceType.DatabaseOperation => SurfaceSignalKeys.DatabaseOperations, SurfaceType.Deserialization => SurfaceSignalKeys.DeserializationPoints, SurfaceType.DynamicCode => SurfaceSignalKeys.DynamicCodePoints, _ => $"{SurfaceSignalKeys.Prefix}{type.ToString().ToLowerInvariant()}" }; signals[key] = count; } // Add entry point count if available if (result.EntryPoints is { Count: > 0 }) { signals[SurfaceSignalKeys.EntryPointCount] = result.EntryPoints.Count; } // Add framework signals if metadata available if (result.Metadata?.Frameworks is { Count: > 0 } frameworks) { foreach (var framework in frameworks) { var normalizedName = framework.ToLowerInvariant().Replace(" ", "_").Replace(".", "_"); signals[$"{SurfaceSignalKeys.FrameworkPrefix}{normalizedName}"] = true; } } // Add language signals if metadata available if (result.Metadata?.Languages is { Count: > 0 } languages) { foreach (var language in languages) { var normalizedName = language.ToLowerInvariant(); signals[$"{SurfaceSignalKeys.LanguagePrefix}{normalizedName}"] = true; } } return signals; } } /// /// Sink for writing surface signals to storage. /// public interface ISurfaceSignalSink { /// /// Writes signals to storage. /// Task WriteAsync( string scanId, IDictionary signals, CancellationToken cancellationToken = default); } /// /// In-memory signal sink for testing. /// public sealed class InMemorySurfaceSignalSink : ISurfaceSignalSink { private readonly Dictionary> _signals = new(); public IReadOnlyDictionary> Signals => _signals; public Task WriteAsync( string scanId, IDictionary signals, CancellationToken cancellationToken = default) { _signals[scanId] = new Dictionary(signals); return Task.CompletedTask; } public IDictionary? GetSignals(string scanId) { return _signals.TryGetValue(scanId, out var signals) ? signals : null; } public void Clear() => _signals.Clear(); }