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,187 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Surface.Models;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Discovery;
|
||||
|
||||
/// <summary>
|
||||
/// Registry for surface entry collectors.
|
||||
/// Manages collector registration and orchestrates collection.
|
||||
/// </summary>
|
||||
public interface ISurfaceEntryRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a collector.
|
||||
/// </summary>
|
||||
void Register(ISurfaceEntryCollector collector);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered collectors.
|
||||
/// </summary>
|
||||
IReadOnlyList<ISurfaceEntryCollector> GetCollectors();
|
||||
|
||||
/// <summary>
|
||||
/// Gets collectors that can analyze the given context.
|
||||
/// </summary>
|
||||
IReadOnlyList<ISurfaceEntryCollector> GetApplicableCollectors(SurfaceCollectionContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Collects entries using all applicable collectors.
|
||||
/// </summary>
|
||||
IAsyncEnumerable<SurfaceEntry> CollectAllAsync(
|
||||
SurfaceCollectionContext context,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of surface entry registry.
|
||||
/// </summary>
|
||||
public sealed class SurfaceEntryRegistry : ISurfaceEntryRegistry
|
||||
{
|
||||
private readonly List<ISurfaceEntryCollector> _collectors = [];
|
||||
private readonly ILogger<SurfaceEntryRegistry> _logger;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public SurfaceEntryRegistry(ILogger<SurfaceEntryRegistry> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Register(ISurfaceEntryCollector collector)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(collector);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// Check for duplicate
|
||||
if (_collectors.Any(c => c.CollectorId == collector.CollectorId))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Collector {CollectorId} already registered, skipping duplicate",
|
||||
collector.CollectorId);
|
||||
return;
|
||||
}
|
||||
|
||||
_collectors.Add(collector);
|
||||
_logger.LogDebug(
|
||||
"Registered surface collector {CollectorId} ({Name}) for languages: {Languages}",
|
||||
collector.CollectorId,
|
||||
collector.Name,
|
||||
string.Join(", ", collector.SupportedLanguages));
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<ISurfaceEntryCollector> GetCollectors()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _collectors
|
||||
.OrderByDescending(c => c.Priority)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<ISurfaceEntryCollector> GetApplicableCollectors(SurfaceCollectionContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var applicable = _collectors
|
||||
.Where(c => c.CanCollect(context))
|
||||
.OrderByDescending(c => c.Priority)
|
||||
.ToList();
|
||||
|
||||
// Filter by options if specified
|
||||
if (context.Options?.Collectors is { Count: > 0 } allowedCollectors)
|
||||
{
|
||||
applicable = applicable
|
||||
.Where(c => allowedCollectors.Contains(c.CollectorId))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return applicable;
|
||||
}
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<SurfaceEntry> CollectAllAsync(
|
||||
SurfaceCollectionContext context,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var collectors = GetApplicableCollectors(context);
|
||||
|
||||
if (collectors.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("No applicable collectors for scan {ScanId}", context.ScanId);
|
||||
yield break;
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Running {CollectorCount} collectors for scan {ScanId}",
|
||||
collectors.Count,
|
||||
context.ScanId);
|
||||
|
||||
var seenIds = new HashSet<string>();
|
||||
var entryCount = 0;
|
||||
var maxEntries = context.Options?.MaxEntries;
|
||||
|
||||
foreach (var collector in collectors)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
if (maxEntries.HasValue && entryCount >= maxEntries.Value)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Reached max entries limit ({MaxEntries}) for scan {ScanId}",
|
||||
maxEntries.Value,
|
||||
context.ScanId);
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Running collector {CollectorId} for scan {ScanId}",
|
||||
collector.CollectorId,
|
||||
context.ScanId);
|
||||
|
||||
await foreach (var entry in collector.CollectAsync(context, cancellationToken))
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
// Apply confidence threshold
|
||||
if (context.Options?.ConfidenceThreshold is double threshold)
|
||||
{
|
||||
var confidenceValue = (int)entry.Confidence / 4.0;
|
||||
if (confidenceValue < threshold)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply type filters
|
||||
if (context.Options?.ExcludeTypes?.Contains(entry.Type) == true)
|
||||
continue;
|
||||
|
||||
if (context.Options?.IncludeTypes is { Count: > 0 } includeTypes &&
|
||||
!includeTypes.Contains(entry.Type))
|
||||
continue;
|
||||
|
||||
// Deduplicate by ID
|
||||
if (!seenIds.Add(entry.Id))
|
||||
continue;
|
||||
|
||||
entryCount++;
|
||||
yield return entry;
|
||||
|
||||
if (maxEntries.HasValue && entryCount >= maxEntries.Value)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Collected {EntryCount} surface entries for scan {ScanId}",
|
||||
entryCount,
|
||||
context.ScanId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user