using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Scanner.Analyzers.OS; using StellaOps.Scanner.Analyzers.OS.Abstractions; using StellaOps.Scanner.Analyzers.OS.Plugin; using StellaOps.Scanner.Core.Contracts; using StellaOps.Scanner.Worker.Options; namespace StellaOps.Scanner.Worker.Processing; internal sealed class OsScanAnalyzerDispatcher : IScanAnalyzerDispatcher { private readonly IServiceScopeFactory _scopeFactory; private readonly OsAnalyzerPluginCatalog _catalog; private readonly ScannerWorkerOptions _options; private readonly ILogger _logger; private IReadOnlyList _pluginDirectories = Array.Empty(); public OsScanAnalyzerDispatcher( IServiceScopeFactory scopeFactory, OsAnalyzerPluginCatalog catalog, IOptions options, ILogger logger) { _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); _catalog = catalog ?? throw new ArgumentNullException(nameof(catalog)); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); LoadPlugins(); } public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(context); using var scope = _scopeFactory.CreateScope(); var services = scope.ServiceProvider; var analyzers = _catalog.CreateAnalyzers(services); if (analyzers.Count == 0) { _logger.LogWarning("No OS analyzers available; skipping analyzer stage for job {JobId}.", context.JobId); return; } var metadata = new Dictionary(context.Lease.Metadata, StringComparer.Ordinal); var rootfsPath = ResolvePath(metadata, _options.Analyzers.RootFilesystemMetadataKey); if (rootfsPath is null) { _logger.LogWarning( "Metadata key '{MetadataKey}' missing for job {JobId}; unable to locate root filesystem. OS analyzers skipped.", _options.Analyzers.RootFilesystemMetadataKey, context.JobId); return; } var workspacePath = ResolvePath(metadata, _options.Analyzers.WorkspaceMetadataKey); var loggerFactory = services.GetRequiredService(); var results = new List(analyzers.Count); foreach (var analyzer in analyzers) { cancellationToken.ThrowIfCancellationRequested(); var analyzerLogger = loggerFactory.CreateLogger(analyzer.GetType()); var analyzerContext = new OSPackageAnalyzerContext(rootfsPath, workspacePath, context.TimeProvider, analyzerLogger, metadata); try { var result = await analyzer.AnalyzeAsync(analyzerContext, cancellationToken).ConfigureAwait(false); results.Add(result); } catch (Exception ex) { _logger.LogError(ex, "Analyzer {AnalyzerId} failed for job {JobId}.", analyzer.AnalyzerId, context.JobId); } } if (results.Count > 0) { var dictionary = results.ToDictionary(result => result.AnalyzerId, StringComparer.OrdinalIgnoreCase); context.Analysis.Set(ScanAnalysisKeys.OsPackageAnalyzers, dictionary); } } private void LoadPlugins() { var directories = new List(); foreach (var configured in _options.Analyzers.PluginDirectories) { if (string.IsNullOrWhiteSpace(configured)) { continue; } var path = configured; if (!Path.IsPathRooted(path)) { path = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, path)); } directories.Add(path); } if (directories.Count == 0) { directories.Add(Path.Combine(AppContext.BaseDirectory, "plugins", "scanner", "analyzers", "os")); } _pluginDirectories = new ReadOnlyCollection(directories); for (var i = 0; i < _pluginDirectories.Count; i++) { var directory = _pluginDirectories[i]; var seal = i == _pluginDirectories.Count - 1; try { _catalog.LoadFromDirectory(directory, seal); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to load analyzer plug-ins from {Directory}.", directory); } } } private static string? ResolvePath(IReadOnlyDictionary metadata, string key) { if (string.IsNullOrWhiteSpace(key)) { return null; } if (!metadata.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) { return null; } var trimmed = value.Trim(); return Path.IsPathRooted(trimmed) ? trimmed : Path.GetFullPath(trimmed); } }