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();
}