feat: Add Go module and workspace test fixtures

- Created expected JSON files for Go modules and workspaces.
- Added go.mod and go.sum files for example projects.
- Implemented private module structure with expected JSON output.
- Introduced vendored dependencies with corresponding expected JSON.
- Developed PostgresGraphJobStore for managing graph jobs.
- Established SQL migration scripts for graph jobs schema.
- Implemented GraphJobRepository for CRUD operations on graph jobs.
- Created IGraphJobRepository interface for repository abstraction.
- Added unit tests for GraphJobRepository to ensure functionality.
This commit is contained in:
StellaOps Bot
2025-12-06 20:04:03 +02:00
parent a6f1406509
commit 05597616d6
178 changed files with 12022 additions and 4545 deletions

View File

@@ -1,36 +1,30 @@
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.
/// Registry for surface entry and entry point collectors.
/// </summary>
public interface ISurfaceEntryRegistry
{
/// <summary>
/// Registers a collector.
/// </summary>
void Register(ISurfaceEntryCollector collector);
/// <summary>Registers a surface entry collector.</summary>
void RegisterCollector(ISurfaceEntryCollector collector);
/// <summary>
/// Gets all registered collectors.
/// </summary>
/// <summary>Registers an entry point collector.</summary>
void RegisterEntryPointCollector(IEntryPointCollector collector);
/// <summary>Gets all registered surface entry collectors.</summary>
IReadOnlyList<ISurfaceEntryCollector> GetCollectors();
/// <summary>
/// Gets collectors that can analyze the given context.
/// </summary>
IReadOnlyList<ISurfaceEntryCollector> GetApplicableCollectors(SurfaceCollectionContext context);
/// <summary>Gets all registered entry point collectors.</summary>
IReadOnlyList<IEntryPointCollector> GetEntryPointCollectors();
/// <summary>
/// Collects entries using all applicable collectors.
/// </summary>
IAsyncEnumerable<SurfaceEntry> CollectAllAsync(
SurfaceCollectionContext context,
CancellationToken cancellationToken = default);
/// <summary>Gets collectors that support the specified surface type.</summary>
IReadOnlyList<ISurfaceEntryCollector> GetCollectorsForType(SurfaceType type);
/// <summary>Gets entry point collectors that support the specified language.</summary>
IReadOnlyList<IEntryPointCollector> GetEntryPointCollectorsForLanguage(string language);
}
/// <summary>
@@ -39,6 +33,7 @@ public interface ISurfaceEntryRegistry
public sealed class SurfaceEntryRegistry : ISurfaceEntryRegistry
{
private readonly List<ISurfaceEntryCollector> _collectors = [];
private readonly List<IEntryPointCollector> _entryPointCollectors = [];
private readonly ILogger<SurfaceEntryRegistry> _logger;
private readonly object _lock = new();
@@ -47,141 +42,61 @@ public sealed class SurfaceEntryRegistry : ISurfaceEntryRegistry
_logger = logger;
}
public void Register(ISurfaceEntryCollector collector)
public void RegisterCollector(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);
_logger.LogWarning("Collector {CollectorId} already registered, skipping", collector.CollectorId);
return;
}
_collectors.Add(collector);
_logger.LogDebug(
"Registered surface collector {CollectorId} ({Name}) for languages: {Languages}",
collector.CollectorId,
collector.Name,
string.Join(", ", collector.SupportedLanguages));
_logger.LogDebug("Registered surface collector: {CollectorId}", collector.CollectorId);
}
}
public void RegisterEntryPointCollector(IEntryPointCollector collector)
{
ArgumentNullException.ThrowIfNull(collector);
lock (_lock)
{
if (_entryPointCollectors.Any(c => c.CollectorId == collector.CollectorId))
{
_logger.LogWarning("Entry point collector {CollectorId} already registered, skipping", collector.CollectorId);
return;
}
_entryPointCollectors.Add(collector);
_logger.LogDebug("Registered entry point collector: {CollectorId}", collector.CollectorId);
}
}
public IReadOnlyList<ISurfaceEntryCollector> GetCollectors()
{
lock (_lock) return [.. _collectors];
}
public IReadOnlyList<IEntryPointCollector> GetEntryPointCollectors()
{
lock (_lock) return [.. _entryPointCollectors];
}
public IReadOnlyList<ISurfaceEntryCollector> GetCollectorsForType(SurfaceType type)
{
lock (_lock)
{
return _collectors
.OrderByDescending(c => c.Priority)
.ToList();
return [.. _collectors.Where(c => c.SupportedTypes.Contains(type))];
}
}
public IReadOnlyList<ISurfaceEntryCollector> GetApplicableCollectors(SurfaceCollectionContext context)
public IReadOnlyList<IEntryPointCollector> GetEntryPointCollectorsForLanguage(string language)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentException.ThrowIfNullOrWhiteSpace(language);
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;
return [.. _entryPointCollectors.Where(c =>
c.SupportedLanguages.Contains(language, StringComparer.OrdinalIgnoreCase))];
}
}
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);
}
}