Add Canonical JSON serialization library with tests and documentation
- Implemented CanonJson class for deterministic JSON serialization and hashing. - Added unit tests for CanonJson functionality, covering various scenarios including key sorting, handling of nested objects, arrays, and special characters. - Created project files for the Canonical JSON library and its tests, including necessary package references. - Added README.md for library usage and API reference. - Introduced RabbitMqIntegrationFactAttribute for conditional RabbitMQ integration tests.
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Category", "SlowTests")]
|
||||
public sealed class VulnSurfaceIntegrationTests : IDisposable
|
||||
{
|
||||
private readonly string _workDir;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(Skip = "Requires network access and ~30s runtime")]
|
||||
public async Task BuildAsync_NewtonsoftJson_CVE_2024_21907_DetectsSinks()
|
||||
{
|
||||
// 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(Skip = "Requires network access and ~15s runtime")]
|
||||
public async Task BuildAsync_HumanizerCore_DetectsMethodChanges()
|
||||
{
|
||||
// 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(Skip = "Requires network access")]
|
||||
public async Task BuildAsync_InvalidPackage_ReturnsFailed()
|
||||
{
|
||||
// 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(Skip = "Requires network access and ~45s runtime")]
|
||||
public async Task BuildAsync_WithTriggers_ExtractsTriggerMethods()
|
||||
{
|
||||
// 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(Skip = "Requires network access and ~60s runtime")]
|
||||
public async Task BuildAsync_SameInput_ProducesDeterministicOutput()
|
||||
{
|
||||
// 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,
|
||||
TimeProvider.System)
|
||||
};
|
||||
|
||||
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 CecilInternalCallGraphBuilder(NullLogger<CecilInternalCallGraphBuilder>.Instance)
|
||||
};
|
||||
|
||||
return new VulnSurfaceBuilder(
|
||||
downloaders,
|
||||
fingerprinters,
|
||||
diffEngine,
|
||||
triggerExtractor,
|
||||
graphBuilders,
|
||||
NullLogger<VulnSurfaceBuilder>.Instance);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user