Add unit tests for VexLens normalizer, CPE parser, product mapper, and PURL parser
- 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.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user