324 lines
10 KiB
C#
324 lines
10 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
[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})");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests building a surface for a small well-known package.
|
|
/// Uses Humanizer.Core which is small and has version differences.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that invalid package name returns appropriate error.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that unsupported ecosystem returns error.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests surface building with trigger extraction.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests deterministic output for the same inputs.
|
|
/// </summary>
|
|
[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<IPackageDownloader>
|
|
{
|
|
new NuGetPackageDownloader(
|
|
new HttpClient(),
|
|
NullLogger<NuGetPackageDownloader>.Instance,
|
|
Microsoft.Extensions.Options.Options.Create(new NuGetDownloaderOptions()))
|
|
};
|
|
|
|
var fingerprinters = new List<IMethodFingerprinter>
|
|
{
|
|
new CecilMethodFingerprinter(NullLogger<CecilMethodFingerprinter>.Instance)
|
|
};
|
|
|
|
var diffEngine = new MethodDiffEngine(NullLogger<MethodDiffEngine>.Instance);
|
|
|
|
var triggerExtractor = new TriggerMethodExtractor(
|
|
NullLogger<TriggerMethodExtractor>.Instance);
|
|
|
|
var graphBuilders = new List<IInternalCallGraphBuilder>
|
|
{
|
|
new CecilInternalGraphBuilder(NullLogger<CecilInternalGraphBuilder>.Instance)
|
|
};
|
|
|
|
return new VulnSurfaceBuilder(
|
|
downloaders,
|
|
fingerprinters,
|
|
diffEngine,
|
|
triggerExtractor,
|
|
graphBuilders,
|
|
NullLogger<VulnSurfaceBuilder>.Instance);
|
|
}
|
|
}
|