Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/CompositeScanAnalyzerDispatcherTests.cs
master 536f6249a6
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Add SBOM, symbols, traces, and VEX files for CVE-2022-21661 SQLi case
- Created CycloneDX and SPDX SBOM files for both reachable and unreachable images.
- Added symbols.json detailing function entry and sink points in the WordPress code.
- Included runtime traces for function calls in both reachable and unreachable scenarios.
- Developed OpenVEX files indicating vulnerability status and justification for both cases.
- Updated README for evaluator harness to guide integration with scanner output.
2025-11-08 20:53:45 +02:00

267 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics.Metrics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Plugin;
using StellaOps.Scanner.Analyzers.OS.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Plugin;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.Surface.FS;
using StellaOps.Scanner.Surface.Secrets;
using StellaOps.Scanner.Surface.Validation;
using StellaOps.Scanner.Worker.Diagnostics;
using StellaOps.Scanner.Worker.Processing;
using StellaOps.Scanner.Worker.Tests.TestInfrastructure;
using Xunit;
using WorkerOptions = StellaOps.Scanner.Worker.Options.ScannerWorkerOptions;
namespace StellaOps.Scanner.Worker.Tests;
public sealed class CompositeScanAnalyzerDispatcherTests
{
[Fact]
public async Task ExecuteAsync_RunsLanguageAnalyzers_StoresResults()
{
using var workspace = new TempDirectory();
using var cacheRoot = new TempDirectory();
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_ENDPOINT", "https://surface.test");
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_BUCKET", "unit-test-bucket");
Environment.SetEnvironmentVariable("SCANNER_SURFACE_CACHE_ROOT", cacheRoot.Path);
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_PROVIDER", "inline");
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_TENANT", "testtenant");
Environment.SetEnvironmentVariable(
"SURFACE_SECRET_TESTTENANT_SCANNERWORKERLANGUAGEANALYZERS_REGISTRY_DEFAULT",
Convert.ToBase64String(Encoding.UTF8.GetBytes("token-placeholder")));
var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
{
{ ScanMetadataKeys.RootFilesystemPath, workspace.Path },
{ ScanMetadataKeys.WorkspacePath, workspace.Path },
};
var osCatalog = new FakeOsCatalog();
var analyzer = new FakeLanguageAnalyzer();
var languageCatalog = new FakeLanguageCatalog(analyzer);
long hits = 0;
long misses = 0;
MeterListener? meterListener = null;
ServiceProvider? services = null;
try
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Debug));
serviceCollection.AddSingleton(TimeProvider.System);
serviceCollection.AddSurfaceEnvironment(options => options.ComponentName = "Scanner.Worker");
serviceCollection.AddSurfaceValidation();
serviceCollection.AddSurfaceFileCache(options => options.RootDirectory = cacheRoot.Path);
serviceCollection.AddSurfaceSecrets();
var metrics = new ScannerWorkerMetrics();
serviceCollection.AddSingleton(metrics);
meterListener = new MeterListener();
meterListener.InstrumentPublished = (instrument, listener) =>
{
if (instrument.Meter.Name == ScannerWorkerInstrumentation.MeterName &&
(instrument.Name == "scanner_worker_language_cache_hits_total" || instrument.Name == "scanner_worker_language_cache_misses_total"))
{
listener.EnableMeasurementEvents(instrument);
}
};
meterListener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
if (instrument.Name == "scanner_worker_language_cache_hits_total")
{
Interlocked.Add(ref hits, measurement);
}
else if (instrument.Name == "scanner_worker_language_cache_misses_total")
{
Interlocked.Add(ref misses, measurement);
}
});
meterListener.Start();
services = serviceCollection.BuildServiceProvider();
var scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var options = Microsoft.Extensions.Options.Options.Create(new WorkerOptions());
var dispatcher = new CompositeScanAnalyzerDispatcher(
scopeFactory,
osCatalog,
languageCatalog,
options,
loggerFactory.CreateLogger<CompositeScanAnalyzerDispatcher>(),
metrics,
new TestCryptoHash());
var lease = new TestJobLease(metadata);
var context = new ScanJobContext(lease, TimeProvider.System, TimeProvider.System.GetUtcNow(), CancellationToken.None);
await dispatcher.ExecuteAsync(context, CancellationToken.None);
// Re-run with a new context to exercise cache reuse.
var leaseSecond = new TestJobLease(metadata);
var contextSecond = new ScanJobContext(leaseSecond, TimeProvider.System, TimeProvider.System.GetUtcNow(), CancellationToken.None);
await dispatcher.ExecuteAsync(contextSecond, CancellationToken.None);
meterListener.RecordObservableInstruments();
Assert.Equal(1, analyzer.InvocationCount);
Assert.True(context.Analysis.TryGet<ReadOnlyDictionary<string, LanguageAnalyzerResult>>(ScanAnalysisKeys.LanguageAnalyzerResults, out var results));
Assert.Single(results);
Assert.True(context.Analysis.TryGet<ImmutableArray<LayerComponentFragment>>(ScanAnalysisKeys.LanguageComponentFragments, out var fragments));
Assert.False(fragments.IsDefaultOrEmpty);
Assert.True(context.Analysis.GetLayerFragments().Any(fragment => fragment.Components.Any(component => component.Identity.Name == "demo-package")));
Assert.Equal(1, hits);
Assert.Equal(1, misses);
}
finally
{
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_ENDPOINT", null);
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_BUCKET", null);
Environment.SetEnvironmentVariable("SCANNER_SURFACE_CACHE_ROOT", null);
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_PROVIDER", null);
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_TENANT", null);
Environment.SetEnvironmentVariable("SURFACE_SECRET_TESTTENANT_SCANNERWORKERLANGUAGEANALYZERS_REGISTRY_DEFAULT", null);
meterListener?.Dispose();
services?.Dispose();
}
}
private sealed class FakeOsCatalog : IOSAnalyzerPluginCatalog
{
public IReadOnlyCollection<IOSAnalyzerPlugin> Plugins => Array.Empty<IOSAnalyzerPlugin>();
public void LoadFromDirectory(string directory, bool seal = true)
{
}
public IReadOnlyList<IOSPackageAnalyzer> CreateAnalyzers(IServiceProvider services) => Array.Empty<IOSPackageAnalyzer>();
}
private sealed class FakeLanguageCatalog : ILanguageAnalyzerPluginCatalog
{
private readonly IReadOnlyList<ILanguageAnalyzer> _analyzers;
public FakeLanguageCatalog(params ILanguageAnalyzer[] analyzers)
{
_analyzers = analyzers ?? Array.Empty<ILanguageAnalyzer>();
}
public IReadOnlyCollection<ILanguageAnalyzerPlugin> Plugins => Array.Empty<ILanguageAnalyzerPlugin>();
public void LoadFromDirectory(string directory, bool seal = true)
{
}
public IReadOnlyList<ILanguageAnalyzer> CreateAnalyzers(IServiceProvider services) => _analyzers;
}
private sealed class FakeLanguageAnalyzer : ILanguageAnalyzer
{
public string Id => "lang.fake";
public string DisplayName => "Fake Language Analyzer";
public int InvocationCount { get; private set; }
public ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
{
Interlocked.Increment(ref _invocationCount);
InvocationCount = _invocationCount;
writer.AddFromPurl(
analyzerId: Id,
purl: "pkg:npm/demo-package@1.0.0",
name: "demo-package",
version: "1.0.0",
type: "npm");
return ValueTask.CompletedTask;
}
private int _invocationCount;
}
private sealed class TestJobLease : IScanJobLease
{
private readonly Dictionary<string, string> _metadata;
public TestJobLease(Dictionary<string, string> metadata)
{
_metadata = metadata;
JobId = Guid.NewGuid().ToString("n");
ScanId = $"scan-{Guid.NewGuid():n}";
Attempt = 1;
EnqueuedAtUtc = DateTimeOffset.UtcNow.AddSeconds(-1);
LeasedAtUtc = DateTimeOffset.UtcNow;
LeaseDuration = TimeSpan.FromMinutes(5);
}
public string JobId { get; }
public string ScanId { get; }
public int Attempt { get; }
public DateTimeOffset EnqueuedAtUtc { get; }
public DateTimeOffset LeasedAtUtc { get; }
public TimeSpan LeaseDuration { get; }
public IReadOnlyDictionary<string, string> Metadata => _metadata;
public ValueTask RenewAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask CompleteAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask AbandonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask PoisonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
private sealed class TempDirectory : IDisposable
{
public TempDirectory()
{
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stellaops-test-{Guid.NewGuid():n}");
Directory.CreateDirectory(Path);
}
public string Path { get; }
public void Dispose()
{
try
{
if (Directory.Exists(Path))
{
Directory.Delete(Path, recursive: true);
}
}
catch
{
}
}
}
}