// ----------------------------------------------------------------------------- // VulnSurfaceIntegrationTests.cs // Sprint: SPRINT_3700_0002_0001_vuln_surfaces_core // Task: SURF-023 // Description: Integration tests with real CVE data (Newtonsoft.Json). // ----------------------------------------------------------------------------- using System.Collections.Generic; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Scanner.VulnSurfaces.Builder; using StellaOps.Scanner.VulnSurfaces.CallGraph; using StellaOps.Scanner.VulnSurfaces.Download; using StellaOps.Scanner.VulnSurfaces.Fingerprint; using StellaOps.Scanner.VulnSurfaces.Triggers; using Xunit; namespace StellaOps.Scanner.VulnSurfaces.Tests; /// /// Integration tests for VulnSurfaceBuilder using real packages. /// These tests require network access and may be slow. /// Set STELLA_NETWORK_TESTS=1 to enable these tests. /// [Trait("Category", "Integration")] [Trait("Category", "SlowTests")] [Trait("Category", "NetworkTests")] public sealed class VulnSurfaceIntegrationTests : IDisposable { private readonly string _workDir; private static readonly bool NetworkTestsEnabled = Environment.GetEnvironmentVariable("STELLA_NETWORK_TESTS") == "1" || Environment.GetEnvironmentVariable("CI") == "true"; public VulnSurfaceIntegrationTests() { _workDir = Path.Combine(Path.GetTempPath(), "vuln-surface-tests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_workDir); } public void Dispose() { try { if (Directory.Exists(_workDir)) { Directory.Delete(_workDir, recursive: true); } } catch { // Ignore cleanup errors } } private void SkipIfNoNetwork() { if (!NetworkTestsEnabled) { Assert.True(true, "Network tests disabled. Set STELLA_NETWORK_TESTS=1 to enable."); return; } } /// /// Tests vulnerability surface extraction for Newtonsoft.Json CVE-2024-21907. /// This CVE relates to type confusion in TypeNameHandling. /// Vuln: 13.0.1, Fixed: 13.0.3 /// [Fact] public async Task BuildAsync_NewtonsoftJson_CVE_2024_21907_DetectsSinks() { if (!NetworkTestsEnabled) { Assert.True(true, "Network tests disabled"); return; } // Arrange var builder = CreateBuilder(); var request = new VulnSurfaceBuildRequest { CveId = "CVE-2024-21907", PackageName = "Newtonsoft.Json", Ecosystem = "nuget", VulnVersion = "13.0.1", FixedVersion = "13.0.3", WorkingDirectory = _workDir, ExtractTriggers = true }; // Act var result = await builder.BuildAsync(request); // Assert Assert.True(result.Success, result.Error ?? "Build should succeed"); Assert.NotNull(result.Surface); Assert.Equal("CVE-2024-21907", result.Surface.CveId); Assert.Equal("nuget", result.Surface.Ecosystem); // Should detect changed methods in the security fix Assert.NotEmpty(result.Surface.Sinks); // Log for visibility foreach (var sink in result.Surface.Sinks) { System.Diagnostics.Debug.WriteLine($"Sink: {sink.MethodKey} ({sink.ChangeType})"); } } /// /// Tests building a surface for a small well-known package. /// Uses Humanizer.Core which is small and has version differences. /// [Fact] public async Task BuildAsync_HumanizerCore_DetectsMethodChanges() { if (!NetworkTestsEnabled) { Assert.True(true, "Network tests disabled"); return; } // Arrange var builder = CreateBuilder(); var request = new VulnSurfaceBuildRequest { CveId = "TEST-0001", PackageName = "Humanizer.Core", Ecosystem = "nuget", VulnVersion = "2.14.0", FixedVersion = "2.14.1", WorkingDirectory = _workDir, ExtractTriggers = false // Skip trigger extraction for speed }; // Act var result = await builder.BuildAsync(request); // Assert Assert.True(result.Success, result.Error ?? "Build should succeed"); Assert.NotNull(result.Surface); // Even if no sinks are found, the surface should be created successfully Assert.NotNull(result.Surface.Sinks); } /// /// Tests that invalid package name returns appropriate error. /// [Fact] public async Task BuildAsync_InvalidPackage_ReturnsFailed() { if (!NetworkTestsEnabled) { Assert.True(true, "Network tests disabled"); return; } // Arrange var builder = CreateBuilder(); var request = new VulnSurfaceBuildRequest { CveId = "TEST-INVALID", PackageName = "This.Package.Does.Not.Exist.12345", Ecosystem = "nuget", VulnVersion = "1.0.0", FixedVersion = "1.0.1", WorkingDirectory = _workDir, ExtractTriggers = false }; // Act var result = await builder.BuildAsync(request); // Assert Assert.False(result.Success); Assert.NotNull(result.Error); Assert.Contains("Failed to download", result.Error); } /// /// Tests that unsupported ecosystem returns error. /// [Fact] public async Task BuildAsync_UnsupportedEcosystem_ReturnsFailed() { // Arrange var builder = CreateBuilder(); var request = new VulnSurfaceBuildRequest { CveId = "TEST-UNSUPPORTED", PackageName = "some-package", Ecosystem = "cargo", // Not supported yet VulnVersion = "1.0.0", FixedVersion = "1.0.1", WorkingDirectory = _workDir, ExtractTriggers = false }; // Act var result = await builder.BuildAsync(request); // Assert Assert.False(result.Success); Assert.Contains("No downloader for ecosystem", result.Error); } /// /// Tests surface building with trigger extraction. /// [Fact] public async Task BuildAsync_WithTriggers_ExtractsTriggerMethods() { if (!NetworkTestsEnabled) { Assert.True(true, "Network tests disabled"); return; } // Arrange var builder = CreateBuilder(); var request = new VulnSurfaceBuildRequest { CveId = "CVE-2024-21907", PackageName = "Newtonsoft.Json", Ecosystem = "nuget", VulnVersion = "13.0.1", FixedVersion = "13.0.3", WorkingDirectory = _workDir, ExtractTriggers = true }; // Act var result = await builder.BuildAsync(request); // Assert Assert.True(result.Success, result.Error ?? "Build should succeed"); Assert.NotNull(result.Surface); // When trigger extraction is enabled, we should have trigger info // Note: TriggerCount may be 0 if no public API calls into the changed methods Assert.True(result.Surface.TriggerCount >= 0); } /// /// Tests deterministic output for the same inputs. /// [Fact] public async Task BuildAsync_SameInput_ProducesDeterministicOutput() { if (!NetworkTestsEnabled) { Assert.True(true, "Network tests disabled"); return; } // Arrange var builder = CreateBuilder(); var request = new VulnSurfaceBuildRequest { CveId = "CVE-2024-21907", PackageName = "Newtonsoft.Json", Ecosystem = "nuget", VulnVersion = "13.0.1", FixedVersion = "13.0.3", WorkingDirectory = Path.Combine(_workDir, "run1"), ExtractTriggers = false }; // Act var result1 = await builder.BuildAsync(request); // Reset for second run request = request with { WorkingDirectory = Path.Combine(_workDir, "run2") }; var result2 = await builder.BuildAsync(request); // Assert Assert.True(result1.Success && result2.Success); Assert.NotNull(result1.Surface); Assert.NotNull(result2.Surface); // Sink count should be identical Assert.Equal(result1.Surface.Sinks.Count, result2.Surface.Sinks.Count); // Method keys should be identical var keys1 = result1.Surface.Sinks.Select(s => s.MethodKey).OrderBy(k => k).ToList(); var keys2 = result2.Surface.Sinks.Select(s => s.MethodKey).OrderBy(k => k).ToList(); Assert.Equal(keys1, keys2); } private VulnSurfaceBuilder CreateBuilder() { var downloaders = new List { new NuGetPackageDownloader( new HttpClient(), NullLogger.Instance, Microsoft.Extensions.Options.Options.Create(new NuGetDownloaderOptions())) }; var fingerprinters = new List { new CecilMethodFingerprinter(NullLogger.Instance) }; var diffEngine = new MethodDiffEngine(NullLogger.Instance); var triggerExtractor = new TriggerMethodExtractor( NullLogger.Instance); var graphBuilders = new List { new CecilInternalGraphBuilder(NullLogger.Instance) }; return new VulnSurfaceBuilder( downloaders, fingerprinters, diffEngine, triggerExtractor, graphBuilders, NullLogger.Instance); } }