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:
StellaOps Bot
2025-12-06 16:28:12 +02:00
parent 2b892ad1b2
commit efd6850c38
132 changed files with 16675 additions and 5428 deletions

View File

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