- Implemented comprehensive tests for VexLensNormalizer including format detection and normalization scenarios. - Added tests for CpeParser covering CPE 2.3 and 2.2 formats, invalid inputs, and canonical key generation. - Created tests for ProductMapper to validate parsing and matching logic across different strictness levels. - Developed tests for PurlParser to ensure correct parsing of various PURL formats and validation of identifiers. - Introduced stubs for Monaco editor and worker to facilitate testing in the web application. - Updated project file for the test project to include necessary dependencies.
178 lines
5.8 KiB
C#
178 lines
5.8 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Scanner.Surface.Models;
|
|
|
|
namespace StellaOps.Scanner.Surface.Signals;
|
|
|
|
/// <summary>
|
|
/// Interface for emitting surface analysis signals for policy evaluation.
|
|
/// </summary>
|
|
public interface ISurfaceSignalEmitter
|
|
{
|
|
/// <summary>
|
|
/// Emits signals for the given analysis result.
|
|
/// </summary>
|
|
Task EmitAsync(
|
|
string scanId,
|
|
SurfaceAnalysisResult result,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Emits custom signals.
|
|
/// </summary>
|
|
Task EmitAsync(
|
|
string scanId,
|
|
IDictionary<string, object> signals,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Default implementation of surface signal emitter.
|
|
/// Converts analysis results to policy signals.
|
|
/// </summary>
|
|
public sealed class SurfaceSignalEmitter : ISurfaceSignalEmitter
|
|
{
|
|
private readonly ILogger<SurfaceSignalEmitter> _logger;
|
|
private readonly ISurfaceSignalSink? _sink;
|
|
|
|
public SurfaceSignalEmitter(
|
|
ILogger<SurfaceSignalEmitter> 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<string, object> 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<string, object> BuildSignals(SurfaceAnalysisResult result)
|
|
{
|
|
var signals = new Dictionary<string, object>
|
|
{
|
|
[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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sink for writing surface signals to storage.
|
|
/// </summary>
|
|
public interface ISurfaceSignalSink
|
|
{
|
|
/// <summary>
|
|
/// Writes signals to storage.
|
|
/// </summary>
|
|
Task WriteAsync(
|
|
string scanId,
|
|
IDictionary<string, object> signals,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// In-memory signal sink for testing.
|
|
/// </summary>
|
|
public sealed class InMemorySurfaceSignalSink : ISurfaceSignalSink
|
|
{
|
|
private readonly Dictionary<string, IDictionary<string, object>> _signals = new();
|
|
|
|
public IReadOnlyDictionary<string, IDictionary<string, object>> Signals => _signals;
|
|
|
|
public Task WriteAsync(
|
|
string scanId,
|
|
IDictionary<string, object> signals,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
_signals[scanId] = new Dictionary<string, object>(signals);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public IDictionary<string, object>? GetSignals(string scanId)
|
|
{
|
|
return _signals.TryGetValue(scanId, out var signals) ? signals : null;
|
|
}
|
|
|
|
public void Clear() => _signals.Clear();
|
|
}
|