release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
// Sprint: EVID-001-005 - Binary Patch Verifier Tests
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.BinaryIndex.Decompiler;
|
||||
using StellaOps.BinaryIndex.Ghidra;
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
using StellaOps.Scanner.Reachability.Binary;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="BinaryPatchVerifier"/>.
|
||||
/// </summary>
|
||||
public sealed class BinaryPatchVerifierTests
|
||||
{
|
||||
private readonly MockGhidraService _ghidraService;
|
||||
private readonly MockDecompilerService _decompilerService;
|
||||
private readonly BinaryPatchVerifier _sut;
|
||||
|
||||
public BinaryPatchVerifierTests()
|
||||
{
|
||||
_ghidraService = new MockGhidraService();
|
||||
_decompilerService = new MockDecompilerService();
|
||||
_sut = new BinaryPatchVerifier(
|
||||
_ghidraService,
|
||||
_decompilerService,
|
||||
NullLogger<BinaryPatchVerifier>.Instance);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(".so", true)]
|
||||
[InlineData(".dll", true)]
|
||||
[InlineData(".exe", true)]
|
||||
[InlineData(".dylib", true)]
|
||||
[InlineData(".elf", true)]
|
||||
[InlineData(".txt", false)]
|
||||
[InlineData(".js", false)]
|
||||
public void IsSupported_ChecksFileExtension(string extension, bool expected)
|
||||
{
|
||||
var result = _sut.IsSupported($"/path/to/binary{extension}");
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsSupported_EmptyPath_ReturnsFalse()
|
||||
{
|
||||
Assert.False(_sut.IsSupported(""));
|
||||
Assert.False(_sut.IsSupported(null!));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyPatchAsync_IdenticalFunctions_ReturnsVulnerable()
|
||||
{
|
||||
// Setup identical P-Code hashes
|
||||
var pCodeHash = new byte[] { 1, 2, 3, 4 };
|
||||
_ghidraService.SetupAnalysis("/vulnerable.so", CreateAnalysis("vuln_func", pCodeHash));
|
||||
_ghidraService.SetupAnalysis("/target.so", CreateAnalysis("vuln_func", pCodeHash));
|
||||
|
||||
var request = new PatchVerificationRequest
|
||||
{
|
||||
VulnerableBinaryReference = "/vulnerable.so",
|
||||
TargetBinaryPath = "/target.so",
|
||||
CveId = "CVE-2024-1234",
|
||||
TargetSymbols = [new VulnerableSymbol { Name = "vuln_func" }]
|
||||
};
|
||||
|
||||
var result = await _sut.VerifyPatchAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(PatchStatus.Vulnerable, result.Status);
|
||||
Assert.Single(result.FunctionResults);
|
||||
Assert.False(result.FunctionResults[0].IsPatched);
|
||||
Assert.Equal(1.0m, result.FunctionResults[0].Similarity);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyPatchAsync_DifferentFunctions_ReturnsPatched()
|
||||
{
|
||||
// Setup different P-Code hashes to force decompilation comparison
|
||||
_ghidraService.SetupAnalysis("/vulnerable.so", CreateAnalysis("vuln_func", new byte[] { 1, 2, 3 }));
|
||||
_ghidraService.SetupAnalysis("/target.so", CreateAnalysis("vuln_func", new byte[] { 4, 5, 6 }));
|
||||
|
||||
// Setup low similarity comparison result
|
||||
_decompilerService.SetupComparison(0.3m, ComparisonConfidence.High);
|
||||
|
||||
var request = new PatchVerificationRequest
|
||||
{
|
||||
VulnerableBinaryReference = "/vulnerable.so",
|
||||
TargetBinaryPath = "/target.so",
|
||||
CveId = "CVE-2024-1234",
|
||||
TargetSymbols = [new VulnerableSymbol { Name = "vuln_func" }]
|
||||
};
|
||||
|
||||
var result = await _sut.VerifyPatchAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(PatchStatus.Patched, result.Status);
|
||||
Assert.Single(result.FunctionResults);
|
||||
Assert.True(result.FunctionResults[0].IsPatched);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyPatchAsync_FunctionRemoved_ReturnsPatched()
|
||||
{
|
||||
// Function exists in vulnerable but not in target
|
||||
_ghidraService.SetupAnalysis("/vulnerable.so", CreateAnalysis("vuln_func", new byte[] { 1, 2, 3 }));
|
||||
_ghidraService.SetupAnalysis("/target.so", CreateAnalysis("other_func", new byte[] { 4, 5, 6 }));
|
||||
|
||||
var request = new PatchVerificationRequest
|
||||
{
|
||||
VulnerableBinaryReference = "/vulnerable.so",
|
||||
TargetBinaryPath = "/target.so",
|
||||
CveId = "CVE-2024-1234",
|
||||
TargetSymbols = [new VulnerableSymbol { Name = "vuln_func" }]
|
||||
};
|
||||
|
||||
var result = await _sut.VerifyPatchAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(PatchStatus.Patched, result.Status);
|
||||
Assert.Contains("removed", result.FunctionResults[0].Differences[0]);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyPatchAsync_MixedResults_ReturnsPartiallyPatched()
|
||||
{
|
||||
// Two functions: one patched, one not
|
||||
var analysis = CreateAnalysisWithMultipleFunctions(
|
||||
("func1", new byte[] { 1, 2, 3 }),
|
||||
("func2", new byte[] { 4, 5, 6 }));
|
||||
|
||||
_ghidraService.SetupAnalysis("/vulnerable.so", analysis);
|
||||
_ghidraService.SetupAnalysis("/target.so", CreateAnalysisWithMultipleFunctions(
|
||||
("func1", new byte[] { 1, 2, 3 }), // Same - not patched
|
||||
("func2", new byte[] { 7, 8, 9 }))); // Different - patched
|
||||
|
||||
_decompilerService.SetupComparison(0.3m, ComparisonConfidence.High);
|
||||
|
||||
var request = new PatchVerificationRequest
|
||||
{
|
||||
VulnerableBinaryReference = "/vulnerable.so",
|
||||
TargetBinaryPath = "/target.so",
|
||||
CveId = "CVE-2024-1234",
|
||||
TargetSymbols =
|
||||
[
|
||||
new VulnerableSymbol { Name = "func1" },
|
||||
new VulnerableSymbol { Name = "func2" }
|
||||
]
|
||||
};
|
||||
|
||||
var result = await _sut.VerifyPatchAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(PatchStatus.PartiallyPatched, result.Status);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyPatchAsync_AnalysisFails_ReturnsUnknown()
|
||||
{
|
||||
_ghidraService.SetupAnalysisFailure("/vulnerable.so");
|
||||
|
||||
var request = new PatchVerificationRequest
|
||||
{
|
||||
VulnerableBinaryReference = "/vulnerable.so",
|
||||
TargetBinaryPath = "/target.so",
|
||||
CveId = "CVE-2024-1234",
|
||||
TargetSymbols = [new VulnerableSymbol { Name = "vuln_func" }]
|
||||
};
|
||||
|
||||
var result = await _sut.VerifyPatchAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(PatchStatus.Unknown, result.Status);
|
||||
Assert.NotNull(result.Error);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyPatchAsync_BuildsCorrectLayer2()
|
||||
{
|
||||
var pCodeHash = new byte[] { 1, 2, 3, 4 };
|
||||
_ghidraService.SetupAnalysis("/vulnerable.so", CreateAnalysis("vuln_func", pCodeHash));
|
||||
_ghidraService.SetupAnalysis("/target.so", CreateAnalysis("vuln_func", pCodeHash));
|
||||
|
||||
var request = new PatchVerificationRequest
|
||||
{
|
||||
VulnerableBinaryReference = "/vulnerable.so",
|
||||
TargetBinaryPath = "/target.so",
|
||||
CveId = "CVE-2024-1234",
|
||||
TargetSymbols = [new VulnerableSymbol { Name = "vuln_func" }]
|
||||
};
|
||||
|
||||
var result = await _sut.VerifyPatchAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.True(result.Layer2.IsResolved);
|
||||
Assert.Equal(ConfidenceLevel.High, result.Layer2.Confidence);
|
||||
Assert.Contains("identical", result.Layer2.Reason);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompareFunctionAsync_ComparesSpecificFunction()
|
||||
{
|
||||
_ghidraService.SetupAnalysis("/vuln.so", CreateAnalysis("target_func", new byte[] { 1, 2, 3 }));
|
||||
_ghidraService.SetupAnalysis("/new.so", CreateAnalysis("target_func", new byte[] { 4, 5, 6 }));
|
||||
_decompilerService.SetupComparison(0.5m, ComparisonConfidence.Medium);
|
||||
|
||||
var result = await _sut.CompareFunctionAsync(
|
||||
"/vuln.so", "/new.so", "target_func", CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("target_func", result.SymbolName);
|
||||
Assert.Equal(0.5m, result.Similarity);
|
||||
}
|
||||
|
||||
private static GhidraAnalysisResult CreateAnalysis(string funcName, byte[] pCodeHash)
|
||||
{
|
||||
return new GhidraAnalysisResult(
|
||||
BinaryHash: "abc123",
|
||||
Functions: ImmutableArray.Create(new GhidraFunction(
|
||||
Name: funcName,
|
||||
Address: 0x1000,
|
||||
Size: 100,
|
||||
Signature: $"void {funcName}(void)",
|
||||
DecompiledCode: "void func() { }",
|
||||
PCodeHash: pCodeHash,
|
||||
CalledFunctions: [],
|
||||
CallingFunctions: [])),
|
||||
Imports: [],
|
||||
Exports: [],
|
||||
Strings: [],
|
||||
MemoryBlocks: [],
|
||||
Metadata: CreateMetadata());
|
||||
}
|
||||
|
||||
private static GhidraAnalysisResult CreateAnalysisWithMultipleFunctions(
|
||||
params (string name, byte[] hash)[] functions)
|
||||
{
|
||||
var ghidraFunctions = functions.Select(f => new GhidraFunction(
|
||||
Name: f.name,
|
||||
Address: 0x1000,
|
||||
Size: 100,
|
||||
Signature: $"void {f.name}(void)",
|
||||
DecompiledCode: "void func() { }",
|
||||
PCodeHash: f.hash,
|
||||
CalledFunctions: [],
|
||||
CallingFunctions: [])).ToImmutableArray();
|
||||
|
||||
return new GhidraAnalysisResult(
|
||||
BinaryHash: "abc123",
|
||||
Functions: ghidraFunctions,
|
||||
Imports: [],
|
||||
Exports: [],
|
||||
Strings: [],
|
||||
MemoryBlocks: [],
|
||||
Metadata: CreateMetadata());
|
||||
}
|
||||
|
||||
private static GhidraMetadata CreateMetadata()
|
||||
{
|
||||
return new GhidraMetadata(
|
||||
FileName: "test.so",
|
||||
Format: "ELF",
|
||||
Architecture: "x86_64",
|
||||
Processor: "x86:LE:64:default",
|
||||
Compiler: null,
|
||||
Endianness: "LE",
|
||||
AddressSize: 64,
|
||||
ImageBase: 0x400000,
|
||||
EntryPoint: 0x401000,
|
||||
AnalysisDate: DateTimeOffset.UtcNow,
|
||||
GhidraVersion: "11.0",
|
||||
AnalysisDuration: TimeSpan.FromSeconds(5));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MockGhidraService : IGhidraService
|
||||
{
|
||||
private readonly Dictionary<string, GhidraAnalysisResult> _analyses = new();
|
||||
private readonly HashSet<string> _failures = new();
|
||||
|
||||
public void SetupAnalysis(string path, GhidraAnalysisResult result)
|
||||
=> _analyses[path] = result;
|
||||
|
||||
public void SetupAnalysisFailure(string path)
|
||||
=> _failures.Add(path);
|
||||
|
||||
public Task<GhidraAnalysisResult> AnalyzeAsync(
|
||||
Stream binaryStream,
|
||||
GhidraAnalysisOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GhidraAnalysisResult> AnalyzeAsync(
|
||||
string binaryPath,
|
||||
GhidraAnalysisOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (_failures.Contains(binaryPath))
|
||||
throw new InvalidOperationException("Analysis failed");
|
||||
|
||||
if (_analyses.TryGetValue(binaryPath, out var result))
|
||||
return Task.FromResult(result);
|
||||
|
||||
throw new InvalidOperationException($"No analysis configured for {binaryPath}");
|
||||
}
|
||||
|
||||
public Task<bool> IsAvailableAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task<GhidraInfo> GetInfoAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult(new GhidraInfo("11.0", "21", [], "/opt/ghidra"));
|
||||
}
|
||||
|
||||
internal sealed class MockDecompilerService : IDecompilerService
|
||||
{
|
||||
private decimal _similarity = 0.9m;
|
||||
private ComparisonConfidence _confidence = ComparisonConfidence.High;
|
||||
|
||||
public void SetupComparison(decimal similarity, ComparisonConfidence confidence)
|
||||
{
|
||||
_similarity = similarity;
|
||||
_confidence = confidence;
|
||||
}
|
||||
|
||||
public Task<DecompiledFunction> DecompileAsync(
|
||||
GhidraFunction function,
|
||||
DecompileOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var ast = new DecompiledAst(
|
||||
Root: new BlockNode(ImmutableArray<AstNode>.Empty),
|
||||
NodeCount: 10,
|
||||
Depth: 3,
|
||||
Patterns: []);
|
||||
|
||||
return Task.FromResult(new DecompiledFunction(
|
||||
FunctionName: function.Name,
|
||||
Signature: function.Signature ?? "void func()",
|
||||
Code: function.DecompiledCode ?? "void func() { }",
|
||||
Ast: ast,
|
||||
Locals: [],
|
||||
CalledFunctions: [],
|
||||
Address: function.Address,
|
||||
SizeBytes: function.Size));
|
||||
}
|
||||
|
||||
public Task<DecompiledFunction> DecompileAtAddressAsync(
|
||||
string binaryPath,
|
||||
ulong address,
|
||||
DecompileOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<DecompiledAst> ParseToAstAsync(
|
||||
string decompiledCode,
|
||||
CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<DecompiledComparisonResult> CompareAsync(
|
||||
DecompiledFunction a,
|
||||
DecompiledFunction b,
|
||||
ComparisonOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(new DecompiledComparisonResult(
|
||||
Similarity: _similarity,
|
||||
StructuralSimilarity: _similarity,
|
||||
SemanticSimilarity: _similarity,
|
||||
EditDistance: new AstEditDistance(0, 0, 0, 0, 1 - _similarity),
|
||||
Equivalences: [],
|
||||
Differences: [],
|
||||
Confidence: _confidence));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
// Sprint: EVID-001-005 - CVE Symbol Mapping Service Tests
|
||||
|
||||
using StellaOps.Scanner.Reachability.Services;
|
||||
using StellaOps.Scanner.Reachability.Stack;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="ICveSymbolMappingService"/> implementations.
|
||||
/// Uses in-memory implementation for unit testing.
|
||||
/// </summary>
|
||||
public sealed class CveSymbolMappingServiceTests
|
||||
{
|
||||
private readonly InMemoryCveSymbolMappingService _sut;
|
||||
|
||||
public CveSymbolMappingServiceTests()
|
||||
{
|
||||
_sut = new InMemoryCveSymbolMappingService();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HasMappingAsync_NoMappings_ReturnsFalse()
|
||||
{
|
||||
var result = await _sut.HasMappingAsync("CVE-2024-1234", CancellationToken.None);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HasMappingAsync_WithMapping_ReturnsTrue()
|
||||
{
|
||||
await _sut.UpsertMappingAsync(CreateMapping("CVE-2024-1234"), CancellationToken.None);
|
||||
|
||||
var result = await _sut.HasMappingAsync("CVE-2024-1234", CancellationToken.None);
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetSinksForCveAsync_ReturnsMatchingMappings()
|
||||
{
|
||||
var mapping1 = CreateMapping("CVE-2024-1234", "pkg:npm/test@1.0", "sink1");
|
||||
var mapping2 = CreateMapping("CVE-2024-1234", "pkg:npm/test@1.0", "sink2");
|
||||
var mapping3 = CreateMapping("CVE-2024-1234", "pkg:npm/other@1.0", "sink3");
|
||||
|
||||
await _sut.UpsertMappingAsync(mapping1, CancellationToken.None);
|
||||
await _sut.UpsertMappingAsync(mapping2, CancellationToken.None);
|
||||
await _sut.UpsertMappingAsync(mapping3, CancellationToken.None);
|
||||
|
||||
var result = await _sut.GetSinksForCveAsync(
|
||||
"CVE-2024-1234", "pkg:npm/test@1.0", CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.All(result, m => Assert.Equal("pkg:npm/test@1.0", m.Purl));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAllMappingsForCveAsync_ReturnsAllMappingsForCve()
|
||||
{
|
||||
var mapping1 = CreateMapping("CVE-2024-1234", "pkg:npm/test@1.0", "sink1");
|
||||
var mapping2 = CreateMapping("CVE-2024-1234", "pkg:npm/other@1.0", "sink2");
|
||||
var mapping3 = CreateMapping("CVE-2024-5678", "pkg:npm/test@1.0", "sink3");
|
||||
|
||||
await _sut.UpsertMappingAsync(mapping1, CancellationToken.None);
|
||||
await _sut.UpsertMappingAsync(mapping2, CancellationToken.None);
|
||||
await _sut.UpsertMappingAsync(mapping3, CancellationToken.None);
|
||||
|
||||
var result = await _sut.GetAllMappingsForCveAsync("CVE-2024-1234", CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.All(result, m => Assert.Equal("CVE-2024-1234", m.CveId));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertMappingAsync_UpdatesExisting()
|
||||
{
|
||||
var original = CreateMapping("CVE-2024-1234", "pkg:npm/test@1.0", "sink1", 0.5m);
|
||||
var updated = CreateMapping("CVE-2024-1234", "pkg:npm/test@1.0", "sink1", 0.9m);
|
||||
|
||||
await _sut.UpsertMappingAsync(original, CancellationToken.None);
|
||||
await _sut.UpsertMappingAsync(updated, CancellationToken.None);
|
||||
|
||||
var result = await _sut.GetSinksForCveAsync(
|
||||
"CVE-2024-1234", "pkg:npm/test@1.0", CancellationToken.None);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(0.9m, result[0].Confidence);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetMappingCountAsync_ReturnsCorrectCount()
|
||||
{
|
||||
await _sut.UpsertMappingAsync(CreateMapping("CVE-1"), CancellationToken.None);
|
||||
await _sut.UpsertMappingAsync(CreateMapping("CVE-2"), CancellationToken.None);
|
||||
await _sut.UpsertMappingAsync(CreateMapping("CVE-3"), CancellationToken.None);
|
||||
|
||||
var count = await _sut.GetMappingCountAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal(3, count);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CveSinkMapping_ToVulnerableSymbol_ConvertsCorrectly()
|
||||
{
|
||||
var mapping = new CveSinkMapping
|
||||
{
|
||||
CveId = "CVE-2024-1234",
|
||||
SymbolName = "vulnerable_function",
|
||||
CanonicalId = "org.test.Vuln#vulnerable_function()",
|
||||
Purl = "pkg:maven/org.test/vuln@1.0.0",
|
||||
FilePath = "src/main/java/org/test/Vuln.java",
|
||||
VulnType = VulnerabilityType.Sink,
|
||||
Confidence = 0.95m,
|
||||
Source = MappingSource.NvdCpe
|
||||
};
|
||||
|
||||
var symbol = mapping.ToVulnerableSymbol();
|
||||
|
||||
Assert.Equal("vulnerable_function", symbol.Name);
|
||||
Assert.Equal("CVE-2024-1234", symbol.VulnerabilityId);
|
||||
Assert.Equal(SymbolType.Function, symbol.Type);
|
||||
}
|
||||
|
||||
private static CveSinkMapping CreateMapping(
|
||||
string cveId,
|
||||
string purl = "pkg:npm/test@1.0.0",
|
||||
string symbolName = "vulnerable_func",
|
||||
decimal confidence = 0.9m)
|
||||
{
|
||||
return new CveSinkMapping
|
||||
{
|
||||
CveId = cveId,
|
||||
SymbolName = symbolName,
|
||||
CanonicalId = $"{purl}#{symbolName}",
|
||||
Purl = purl,
|
||||
FilePath = null,
|
||||
VulnType = VulnerabilityType.Sink,
|
||||
Confidence = confidence,
|
||||
Source = MappingSource.ManualCuration
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation for testing.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryCveSymbolMappingService : ICveSymbolMappingService
|
||||
{
|
||||
private readonly List<CveSinkMapping> _mappings = [];
|
||||
|
||||
public Task<IReadOnlyList<CveSinkMapping>> GetSinksForCveAsync(
|
||||
string cveId, string purl, CancellationToken ct = default)
|
||||
{
|
||||
var result = _mappings
|
||||
.Where(m => m.CveId == cveId && m.Purl == purl)
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<CveSinkMapping>>(result);
|
||||
}
|
||||
|
||||
public Task<bool> HasMappingAsync(string cveId, CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(_mappings.Any(m => m.CveId == cveId));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<CveSinkMapping>> GetAllMappingsForCveAsync(
|
||||
string cveId, CancellationToken ct = default)
|
||||
{
|
||||
var result = _mappings.Where(m => m.CveId == cveId).ToList();
|
||||
return Task.FromResult<IReadOnlyList<CveSinkMapping>>(result);
|
||||
}
|
||||
|
||||
public Task<CveSinkMapping> UpsertMappingAsync(
|
||||
CveSinkMapping mapping, CancellationToken ct = default)
|
||||
{
|
||||
var existing = _mappings.FirstOrDefault(m =>
|
||||
m.CveId == mapping.CveId &&
|
||||
m.SymbolName == mapping.SymbolName &&
|
||||
m.Purl == mapping.Purl);
|
||||
|
||||
if (existing is not null)
|
||||
{
|
||||
_mappings.Remove(existing);
|
||||
}
|
||||
|
||||
_mappings.Add(mapping);
|
||||
return Task.FromResult(mapping);
|
||||
}
|
||||
|
||||
public Task<int> GetMappingCountAsync(CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(_mappings.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
// Sprint: EVID-001-005 - Runtime Reachability Collector Tests
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
using StellaOps.Scanner.Reachability.Runtime;
|
||||
using StellaOps.Scanner.Reachability.Stack;
|
||||
using StellaOps.Signals.Ebpf.Schema;
|
||||
using StellaOps.Signals.Ebpf.Services;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="EbpfRuntimeReachabilityCollector"/>.
|
||||
/// </summary>
|
||||
public sealed class RuntimeReachabilityCollectorTests
|
||||
{
|
||||
private readonly MockSignalCollector _signalCollector;
|
||||
private readonly MockObservationStore _observationStore;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly EbpfRuntimeReachabilityCollector _sut;
|
||||
|
||||
public RuntimeReachabilityCollectorTests()
|
||||
{
|
||||
_signalCollector = new MockSignalCollector();
|
||||
_observationStore = new MockObservationStore();
|
||||
_timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
|
||||
_sut = new EbpfRuntimeReachabilityCollector(
|
||||
_signalCollector,
|
||||
_observationStore,
|
||||
NullLogger<EbpfRuntimeReachabilityCollector>.Instance,
|
||||
_timeProvider);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsAvailable_ReturnsSignalCollectorAvailability()
|
||||
{
|
||||
_signalCollector.SetSupported(true);
|
||||
|
||||
// Note: Also requires Linux, so this will be false on Windows
|
||||
var result = _sut.IsAvailable;
|
||||
|
||||
// On non-Linux, this should be false even if signal collector is supported
|
||||
Assert.False(result); // Running on Windows
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Platform_ReturnsUnsupportedOnNonLinux()
|
||||
{
|
||||
Assert.Equal("unsupported", _sut.Platform);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ObserveAsync_WithHistoricalData_ReturnsHistoricalResult()
|
||||
{
|
||||
var observations = new List<SymbolObservation>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Symbol = "vulnerable_func",
|
||||
WasObserved = true,
|
||||
ObservationCount = 5,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
LastObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5)
|
||||
}
|
||||
};
|
||||
|
||||
_observationStore.SetObservations("container-123", observations);
|
||||
|
||||
var request = new RuntimeObservationRequest
|
||||
{
|
||||
ContainerId = "container-123",
|
||||
ImageDigest = "sha256:abc123",
|
||||
TargetSymbols = ["vulnerable_func"],
|
||||
UseHistoricalData = true
|
||||
};
|
||||
|
||||
var result = await _sut.ObserveAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(ObservationSource.Historical, result.Source);
|
||||
Assert.Single(result.Observations);
|
||||
Assert.True(result.Observations[0].WasObserved);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ObserveAsync_NoHistoricalData_ReturnsUnavailableOnNonLinux()
|
||||
{
|
||||
_observationStore.SetObservations("container-123", []);
|
||||
|
||||
var request = new RuntimeObservationRequest
|
||||
{
|
||||
ContainerId = "container-123",
|
||||
ImageDigest = "sha256:abc123",
|
||||
TargetSymbols = ["vulnerable_func"],
|
||||
UseHistoricalData = true
|
||||
};
|
||||
|
||||
var result = await _sut.ObserveAsync(request, CancellationToken.None);
|
||||
|
||||
// On non-Linux, live observation is not available
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(ObservationSource.None, result.Source);
|
||||
Assert.Contains("not available", result.Error);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ObserveAsync_SymbolsObserved_ReturnsNotGatedLayer3()
|
||||
{
|
||||
var observations = new List<SymbolObservation>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Symbol = "target_sink",
|
||||
WasObserved = true,
|
||||
ObservationCount = 10
|
||||
}
|
||||
};
|
||||
|
||||
_observationStore.SetObservations("container-abc", observations);
|
||||
|
||||
var request = new RuntimeObservationRequest
|
||||
{
|
||||
ContainerId = "container-abc",
|
||||
ImageDigest = "sha256:def456",
|
||||
TargetSymbols = ["target_sink"],
|
||||
UseHistoricalData = true
|
||||
};
|
||||
|
||||
var result = await _sut.ObserveAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.False(result.Layer3.IsGated);
|
||||
Assert.Equal(GatingOutcome.NotGated, result.Layer3.Outcome);
|
||||
Assert.Equal(ConfidenceLevel.High, result.Layer3.Confidence);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ObserveAsync_NoSymbolsObserved_ReturnsUnknownOutcome()
|
||||
{
|
||||
var observations = new List<SymbolObservation>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Symbol = "target_sink",
|
||||
WasObserved = false,
|
||||
ObservationCount = 0
|
||||
}
|
||||
};
|
||||
|
||||
_observationStore.SetObservations("container-xyz", observations);
|
||||
|
||||
var request = new RuntimeObservationRequest
|
||||
{
|
||||
ContainerId = "container-xyz",
|
||||
ImageDigest = "sha256:ghi789",
|
||||
TargetSymbols = ["target_sink"],
|
||||
UseHistoricalData = true
|
||||
};
|
||||
|
||||
var result = await _sut.ObserveAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(GatingOutcome.Unknown, result.Layer3.Outcome);
|
||||
Assert.Equal(ConfidenceLevel.Medium, result.Layer3.Confidence);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CheckObservationsAsync_DelegatesToStore()
|
||||
{
|
||||
var observations = new List<SymbolObservation>
|
||||
{
|
||||
new() { Symbol = "func1", WasObserved = true, ObservationCount = 3 },
|
||||
new() { Symbol = "func2", WasObserved = false, ObservationCount = 0 }
|
||||
};
|
||||
|
||||
_observationStore.SetObservations("container-check", observations);
|
||||
|
||||
var result = await _sut.CheckObservationsAsync(
|
||||
"container-check",
|
||||
["func1", "func2"],
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.True(result[0].WasObserved);
|
||||
Assert.False(result[1].WasObserved);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ObserveAsync_Exception_ReturnsFailedResult()
|
||||
{
|
||||
_observationStore.ThrowOnGet = true;
|
||||
|
||||
var request = new RuntimeObservationRequest
|
||||
{
|
||||
ContainerId = "container-fail",
|
||||
ImageDigest = "sha256:fail",
|
||||
TargetSymbols = ["func"],
|
||||
UseHistoricalData = true
|
||||
};
|
||||
|
||||
var result = await _sut.ObserveAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.NotNull(result.Error);
|
||||
Assert.Equal(GatingOutcome.Unknown, result.Layer3.Outcome);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MockSignalCollector : IRuntimeSignalCollector
|
||||
{
|
||||
private bool _isSupported = true;
|
||||
private readonly RuntimeSignalOptions _defaultOptions = new()
|
||||
{
|
||||
TargetSymbols = [],
|
||||
MaxEventsPerSecond = 10000
|
||||
};
|
||||
|
||||
public void SetSupported(bool supported) => _isSupported = supported;
|
||||
|
||||
public bool IsSupported() => _isSupported;
|
||||
|
||||
public IReadOnlyList<ProbeType> GetSupportedProbeTypes() => [ProbeType.Uprobe, ProbeType.Uretprobe];
|
||||
|
||||
public Task<SignalCollectionHandle> StartCollectionAsync(
|
||||
string containerId,
|
||||
RuntimeSignalOptions options,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(new SignalCollectionHandle
|
||||
{
|
||||
SessionId = Guid.NewGuid(),
|
||||
ContainerId = containerId,
|
||||
StartedAt = DateTimeOffset.UtcNow,
|
||||
Options = options ?? _defaultOptions
|
||||
});
|
||||
}
|
||||
|
||||
public Task<RuntimeSignalSummary> StopCollectionAsync(
|
||||
SignalCollectionHandle handle,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(new RuntimeSignalSummary
|
||||
{
|
||||
ContainerId = handle.ContainerId,
|
||||
StartedAt = handle.StartedAt,
|
||||
StoppedAt = DateTimeOffset.UtcNow,
|
||||
TotalEvents = 100,
|
||||
ObservedSymbols = ["func1", "func2"],
|
||||
CallPaths = []
|
||||
});
|
||||
}
|
||||
|
||||
public Task<SignalStatistics> GetStatisticsAsync(
|
||||
SignalCollectionHandle handle,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(new SignalStatistics
|
||||
{
|
||||
TotalEvents = 100,
|
||||
DroppedEvents = 0,
|
||||
EventsPerSecond = 50,
|
||||
UniqueCallPaths = 5,
|
||||
BufferUtilization = 0.25
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MockObservationStore : IRuntimeObservationStore
|
||||
{
|
||||
private readonly Dictionary<string, IReadOnlyList<SymbolObservation>> _observations = new();
|
||||
public bool ThrowOnGet { get; set; }
|
||||
|
||||
public void SetObservations(string containerId, IReadOnlyList<SymbolObservation> observations)
|
||||
=> _observations[containerId] = observations;
|
||||
|
||||
public Task<IReadOnlyList<SymbolObservation>> GetObservationsAsync(
|
||||
string containerId,
|
||||
IReadOnlyList<string> symbols,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (ThrowOnGet)
|
||||
throw new InvalidOperationException("Store error");
|
||||
|
||||
if (_observations.TryGetValue(containerId, out var obs))
|
||||
return Task.FromResult(obs);
|
||||
|
||||
return Task.FromResult<IReadOnlyList<SymbolObservation>>([]);
|
||||
}
|
||||
|
||||
public Task StoreObservationAsync(
|
||||
string containerId,
|
||||
string imageDigest,
|
||||
SymbolObservation observation,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
// Sprint: EVID-001-005 - VEX Status Determiner Tests
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
using StellaOps.Scanner.Reachability.Stack;
|
||||
using StellaOps.Scanner.Reachability.Vex;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
using StackCallPath = StellaOps.Scanner.Reachability.Stack.CallPath;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="VexStatusDeterminer"/>.
|
||||
/// </summary>
|
||||
public sealed class VexStatusDeterminerTests
|
||||
{
|
||||
private readonly VexStatusDeterminer _sut;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
|
||||
public VexStatusDeterminerTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 15, 12, 0, 0, TimeSpan.Zero));
|
||||
_sut = new VexStatusDeterminer(_timeProvider);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(ReachabilityVerdict.Exploitable, VexStatus.Affected)]
|
||||
[InlineData(ReachabilityVerdict.LikelyExploitable, VexStatus.Affected)]
|
||||
[InlineData(ReachabilityVerdict.PossiblyExploitable, VexStatus.UnderInvestigation)]
|
||||
[InlineData(ReachabilityVerdict.Unreachable, VexStatus.NotAffected)]
|
||||
[InlineData(ReachabilityVerdict.Unknown, VexStatus.UnderInvestigation)]
|
||||
public void DetermineStatus_MapsVerdictToVexStatus(
|
||||
ReachabilityVerdict verdict,
|
||||
VexStatus expectedStatus)
|
||||
{
|
||||
var result = _sut.DetermineStatus(verdict);
|
||||
|
||||
Assert.Equal(expectedStatus, result);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildJustification_Unreachable_ReturnsVulnerableCodeNotReachable()
|
||||
{
|
||||
var stack = CreateStack(ReachabilityVerdict.Unreachable, isL1Reachable: false);
|
||||
|
||||
var justification = _sut.BuildJustification(stack, ["evidence://bundle/123"]);
|
||||
|
||||
Assert.Equal(VexJustificationCategory.VulnerableCodeNotReachable, justification.Category);
|
||||
Assert.Contains("not reachable", justification.Detail);
|
||||
Assert.Single(justification.EvidenceReferences);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildJustification_Exploitable_ReturnsRequiresDependency()
|
||||
{
|
||||
var stack = CreateStack(ReachabilityVerdict.Exploitable, isL1Reachable: true);
|
||||
|
||||
var justification = _sut.BuildJustification(stack, []);
|
||||
|
||||
Assert.Equal(VexJustificationCategory.RequiresDependency, justification.Category);
|
||||
Assert.Contains("reachable", justification.Detail);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildJustification_LikelyExploitable_ReturnsRequiresConfiguration()
|
||||
{
|
||||
var stack = CreateStack(ReachabilityVerdict.LikelyExploitable, isL1Reachable: true);
|
||||
|
||||
var justification = _sut.BuildJustification(stack, []);
|
||||
|
||||
Assert.Equal(VexJustificationCategory.RequiresConfiguration, justification.Category);
|
||||
Assert.Contains("likely exploitable", justification.Detail);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildJustification_CalculatesConfidence_FromLayerConfidences()
|
||||
{
|
||||
var stack = CreateStack(
|
||||
ReachabilityVerdict.Exploitable,
|
||||
isL1Reachable: true,
|
||||
l1Confidence: ConfidenceLevel.High,
|
||||
l2Confidence: ConfidenceLevel.Medium,
|
||||
l3Confidence: ConfidenceLevel.Low);
|
||||
|
||||
var justification = _sut.BuildJustification(stack, []);
|
||||
|
||||
// Weighted: 0.5 * 1.0 + 0.25 * 0.7 + 0.25 * 0.4 = 0.775
|
||||
Assert.True(justification.Confidence >= 0.7m);
|
||||
Assert.True(justification.Confidence <= 0.8m);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateStatement_NotAffected_IncludesJustification()
|
||||
{
|
||||
var stack = CreateStack(ReachabilityVerdict.Unreachable, isL1Reachable: false);
|
||||
|
||||
var statement = _sut.CreateStatement(stack, "product/1.0", []);
|
||||
|
||||
Assert.Equal(VexStatus.NotAffected, statement.Status);
|
||||
Assert.NotNull(statement.Justification);
|
||||
Assert.Null(statement.ActionStatement);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateStatement_Affected_IncludesActionStatement()
|
||||
{
|
||||
var stack = CreateStack(ReachabilityVerdict.Exploitable, isL1Reachable: true);
|
||||
|
||||
var statement = _sut.CreateStatement(stack, "product/1.0", []);
|
||||
|
||||
Assert.Equal(VexStatus.Affected, statement.Status);
|
||||
Assert.Null(statement.Justification); // Justification only for NotAffected
|
||||
Assert.NotNull(statement.ActionStatement);
|
||||
Assert.Contains("affects", statement.ActionStatement);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateStatement_GeneratesDeterministicId()
|
||||
{
|
||||
var stack = CreateStack(ReachabilityVerdict.Exploitable, isL1Reachable: true);
|
||||
|
||||
var statement1 = _sut.CreateStatement(stack, "product/1.0", []);
|
||||
var statement2 = _sut.CreateStatement(stack, "product/1.0", []);
|
||||
|
||||
Assert.Equal(statement1.StatementId, statement2.StatementId);
|
||||
Assert.StartsWith("stmt-", statement1.StatementId);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateStatement_SetsTimestampFromTimeProvider()
|
||||
{
|
||||
var stack = CreateStack(ReachabilityVerdict.Exploitable, isL1Reachable: true);
|
||||
|
||||
var statement = _sut.CreateStatement(stack, "product/1.0", []);
|
||||
|
||||
Assert.Equal(_timeProvider.GetUtcNow(), statement.Timestamp);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateStatement_IncludesImpactStatement()
|
||||
{
|
||||
var stack = CreateStack(ReachabilityVerdict.Exploitable, isL1Reachable: true);
|
||||
|
||||
var statement = _sut.CreateStatement(stack, "product/1.0", []);
|
||||
|
||||
Assert.NotNull(statement.ImpactStatement);
|
||||
Assert.Contains("vulnerable_func", statement.ImpactStatement);
|
||||
}
|
||||
|
||||
private static ReachabilityStack CreateStack(
|
||||
ReachabilityVerdict verdict,
|
||||
bool isL1Reachable,
|
||||
ConfidenceLevel l1Confidence = ConfidenceLevel.High,
|
||||
ConfidenceLevel l2Confidence = ConfidenceLevel.High,
|
||||
ConfidenceLevel l3Confidence = ConfidenceLevel.High)
|
||||
{
|
||||
var paths = isL1Reachable
|
||||
? ImmutableArray.Create(new StackCallPath
|
||||
{
|
||||
Sites = ImmutableArray.Create(new CallSite("main", null, null, null, CallSiteType.Direct)),
|
||||
Entrypoint = new Entrypoint("main", EntrypointType.HttpEndpoint, null, null),
|
||||
Confidence = 1.0,
|
||||
HasConditionals = false
|
||||
})
|
||||
: ImmutableArray<StackCallPath>.Empty;
|
||||
|
||||
var entrypoints = isL1Reachable
|
||||
? ImmutableArray.Create(new Entrypoint("main", EntrypointType.HttpEndpoint, null, null))
|
||||
: ImmutableArray<Entrypoint>.Empty;
|
||||
|
||||
return new ReachabilityStack
|
||||
{
|
||||
Id = "test-stack-1",
|
||||
FindingId = "CVE-2024-1234:pkg:npm/test@1.0.0",
|
||||
Symbol = new VulnerableSymbol(
|
||||
Name: "vulnerable_func",
|
||||
Library: "test-lib",
|
||||
Version: "1.0.0",
|
||||
VulnerabilityId: "CVE-2024-1234",
|
||||
Type: SymbolType.Function),
|
||||
StaticCallGraph = new ReachabilityLayer1
|
||||
{
|
||||
IsReachable = isL1Reachable,
|
||||
Confidence = l1Confidence,
|
||||
Paths = paths,
|
||||
ReachingEntrypoints = entrypoints,
|
||||
AnalysisMethod = "BFS",
|
||||
Limitations = []
|
||||
},
|
||||
BinaryResolution = new ReachabilityLayer2
|
||||
{
|
||||
IsResolved = true,
|
||||
Confidence = l2Confidence,
|
||||
Reason = "Symbol linked"
|
||||
},
|
||||
RuntimeGating = new ReachabilityLayer3
|
||||
{
|
||||
IsGated = false,
|
||||
Outcome = GatingOutcome.NotGated,
|
||||
Confidence = l3Confidence,
|
||||
Description = "No gating detected"
|
||||
},
|
||||
Verdict = verdict,
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
Explanation = "Test stack"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FakeTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _utcNow;
|
||||
|
||||
public FakeTimeProvider(DateTimeOffset utcNow)
|
||||
{
|
||||
_utcNow = utcNow;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _utcNow;
|
||||
|
||||
public void Advance(TimeSpan duration) => _utcNow = _utcNow.Add(duration);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using StellaOps.Scanner.Contracts;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -18,5 +18,7 @@
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Cache\StellaOps.Scanner.Cache.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="../../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Decompiler/StellaOps.BinaryIndex.Decompiler.csproj" />
|
||||
<ProjectReference Include="../../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Ghidra/StellaOps.BinaryIndex.Ghidra.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user