Add call graph fixtures for various languages and scenarios
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
- Introduced `all-edge-reasons.json` to test edge resolution reasons in .NET. - Added `all-visibility-levels.json` to validate method visibility levels in .NET. - Created `dotnet-aspnetcore-minimal.json` for a minimal ASP.NET Core application. - Included `go-gin-api.json` for a Go Gin API application structure. - Added `java-spring-boot.json` for the Spring PetClinic application in Java. - Introduced `legacy-no-schema.json` for legacy application structure without schema. - Created `node-express-api.json` for an Express.js API application structure.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "observation::node-phase22",
|
||||
"name": "Node Observation (Phase 22)",
|
||||
"type": "node-observation",
|
||||
"usedByEntrypoint": false,
|
||||
"capabilities": [],
|
||||
"threatVectors": [],
|
||||
"metadata": {
|
||||
"node.observation.components": "2",
|
||||
"node.observation.edges": "2",
|
||||
"node.observation.entrypoints": "0",
|
||||
"node.observation.native": "1",
|
||||
"node.observation.wasm": "1"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "node.observation",
|
||||
"locator": "phase22.ndjson",
|
||||
"value": "{\u0022type\u0022:\u0022component\u0022,\u0022componentType\u0022:\u0022native\u0022,\u0022path\u0022:\u0022/native/addon.node\u0022,\u0022reason\u0022:\u0022native-addon-file\u0022,\u0022confidence\u0022:0.82,\u0022resolverTrace\u0022:[\u0022file:/native/addon.node\u0022],\u0022arch\u0022:\u0022x86_64\u0022,\u0022platform\u0022:\u0022linux\u0022}\r\n{\u0022type\u0022:\u0022component\u0022,\u0022componentType\u0022:\u0022wasm\u0022,\u0022path\u0022:\u0022/pkg/pkg.wasm\u0022,\u0022reason\u0022:\u0022wasm-file\u0022,\u0022confidence\u0022:0.8,\u0022resolverTrace\u0022:[\u0022file:/pkg/pkg.wasm\u0022]}\r\n{\u0022type\u0022:\u0022edge\u0022,\u0022edgeType\u0022:\u0022wasm\u0022,\u0022from\u0022:\u0022/src/app.js\u0022,\u0022to\u0022:\u0022/src/pkg/pkg.wasm\u0022,\u0022reason\u0022:\u0022wasm-import\u0022,\u0022confidence\u0022:0.74,\u0022resolverTrace\u0022:[\u0022source:/src/app.js\u0022,\u0022call:WebAssembly.instantiate(\\u0027./pkg/pkg.wasm\\u0027)\u0022]}\r\n{\u0022type\u0022:\u0022edge\u0022,\u0022edgeType\u0022:\u0022capability\u0022,\u0022from\u0022:\u0022/src/app.js\u0022,\u0022to\u0022:\u0022child_process.execFile\u0022,\u0022reason\u0022:\u0022capability-child-process\u0022,\u0022confidence\u0022:0.7,\u0022resolverTrace\u0022:[\u0022source:/src/app.js\u0022,\u0022call:child_process.execFile\u0022]}",
|
||||
"sha256": "1329f1c41716d8430b5bdb6d02d1d5f2be1be80877ac15a7e72d3a079fffa4fb"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Core.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Core.Tests;
|
||||
|
||||
public sealed class OfflineKitOptionsValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Validate_WhenDisabled_SucceedsEvenWithDefaults()
|
||||
{
|
||||
var validator = new OfflineKitOptionsValidator();
|
||||
var result = validator.Validate(null, new OfflineKitOptions());
|
||||
Assert.Equal(ValidateOptionsResult.Success, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenEnabled_RequiresRekorSnapshotDirectory()
|
||||
{
|
||||
var validator = new OfflineKitOptionsValidator();
|
||||
var options = new OfflineKitOptions
|
||||
{
|
||||
Enabled = true,
|
||||
TrustAnchors = new List<TrustAnchorConfig>()
|
||||
};
|
||||
|
||||
var result = validator.Validate(null, options);
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.NotNull(result.Failures);
|
||||
Assert.Contains(result.Failures!, message => message.Contains("RekorSnapshotDirectory", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenEnabled_RequiresTrustRootDirectoryWhenAnchorsPresent()
|
||||
{
|
||||
var validator = new OfflineKitOptionsValidator();
|
||||
var options = new OfflineKitOptions
|
||||
{
|
||||
Enabled = true,
|
||||
RekorOfflineMode = false,
|
||||
TrustAnchors = new List<TrustAnchorConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AnchorId = "default",
|
||||
PurlPattern = "*",
|
||||
AllowedKeyIds = new List<string> { "sha256:abcdef" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = validator.Validate(null, options);
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.NotNull(result.Failures);
|
||||
Assert.Contains(result.Failures!, message => message.Contains("TrustRootDirectory", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenEnabled_WithMinimalValidConfig_Succeeds()
|
||||
{
|
||||
var validator = new OfflineKitOptionsValidator();
|
||||
|
||||
var trustRootDirectory = CreateTempDirectory("offline-kit-trust-roots");
|
||||
var rekorSnapshotDirectory = CreateTempDirectory("offline-kit-rekor");
|
||||
|
||||
try
|
||||
{
|
||||
var options = new OfflineKitOptions
|
||||
{
|
||||
Enabled = true,
|
||||
RequireDsse = true,
|
||||
RekorOfflineMode = true,
|
||||
TrustRootDirectory = trustRootDirectory,
|
||||
RekorSnapshotDirectory = rekorSnapshotDirectory,
|
||||
TrustAnchors = new List<TrustAnchorConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AnchorId = "default",
|
||||
PurlPattern = "*",
|
||||
AllowedKeyIds = new List<string> { "sha256:abcdef" },
|
||||
MinSignatures = 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = validator.Validate(null, options);
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDeleteDirectory(trustRootDirectory);
|
||||
TryDeleteDirectory(rekorSnapshotDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenEnabled_DetectsDuplicateAnchorIds()
|
||||
{
|
||||
var validator = new OfflineKitOptionsValidator();
|
||||
|
||||
var trustRootDirectory = CreateTempDirectory("offline-kit-trust-roots");
|
||||
var rekorSnapshotDirectory = CreateTempDirectory("offline-kit-rekor");
|
||||
|
||||
try
|
||||
{
|
||||
var options = new OfflineKitOptions
|
||||
{
|
||||
Enabled = true,
|
||||
RekorOfflineMode = true,
|
||||
TrustRootDirectory = trustRootDirectory,
|
||||
RekorSnapshotDirectory = rekorSnapshotDirectory,
|
||||
TrustAnchors = new List<TrustAnchorConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AnchorId = "duplicate",
|
||||
PurlPattern = "*",
|
||||
AllowedKeyIds = new List<string> { "sha256:aaaa" },
|
||||
},
|
||||
new()
|
||||
{
|
||||
AnchorId = "DUPLICATE",
|
||||
PurlPattern = "*",
|
||||
AllowedKeyIds = new List<string> { "sha256:bbbb" },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = validator.Validate(null, options);
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.NotNull(result.Failures);
|
||||
Assert.Contains(result.Failures!, message => message.Contains("Duplicate", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDeleteDirectory(trustRootDirectory);
|
||||
TryDeleteDirectory(rekorSnapshotDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateTempDirectory(string prefix)
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"{prefix}-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void TryDeleteDirectory(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
Directory.Delete(path, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ public class ReachabilityUnionPublisherTests
|
||||
|
||||
var entry = await cas.TryGetAsync(result.Sha256);
|
||||
Assert.NotNull(entry);
|
||||
Assert.True(entry!.Value.SizeBytes > 0);
|
||||
Assert.True(entry!.SizeBytes > 0);
|
||||
}
|
||||
|
||||
private sealed class TempDir : IDisposable
|
||||
|
||||
@@ -53,10 +53,19 @@ public class ReachabilityUnionWriterTests
|
||||
Assert.Contains("sym:dotnet:B", nodeLines[1]);
|
||||
|
||||
// Hashes recorded in meta match content
|
||||
var meta = await JsonDocument.ParseAsync(File.OpenRead(result.MetaPath));
|
||||
var files = meta.RootElement.GetProperty("files").EnumerateArray().ToList();
|
||||
Assert.Contains(files, f => f.GetProperty("path").GetString() == result.Nodes.Path && f.GetProperty("sha256").GetString() == result.Nodes.Sha256);
|
||||
Assert.Contains(files, f => f.GetProperty("path").GetString() == result.Edges.Path && f.GetProperty("sha256").GetString() == result.Edges.Sha256);
|
||||
List<(string? Path, string? Sha256)> files;
|
||||
await using (var metaStream = File.OpenRead(result.MetaPath))
|
||||
using (var meta = await JsonDocument.ParseAsync(metaStream))
|
||||
{
|
||||
files = meta.RootElement
|
||||
.GetProperty("files")
|
||||
.EnumerateArray()
|
||||
.Select(file => (Path: file.GetProperty("path").GetString(), Sha256: file.GetProperty("sha256").GetString()))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
Assert.Contains(files, file => file.Path == result.Nodes.Path && file.Sha256 == result.Nodes.Sha256);
|
||||
Assert.Contains(files, file => file.Path == result.Edges.Path && file.Sha256 == result.Edges.Sha256);
|
||||
|
||||
// Determinism: re-run with shuffled inputs yields identical hashes
|
||||
var shuffled = new ReachabilityUnionGraph(
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using StellaOps.Scanner.Core.TrustAnchors;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Core.Tests;
|
||||
|
||||
public sealed class PurlPatternMatcherTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("*", "pkg:npm/foo@1.0.0", true)]
|
||||
[InlineData("*", "anything", true)]
|
||||
[InlineData("*", "", false)]
|
||||
[InlineData("*", null, false)]
|
||||
[InlineData("pkg:npm/*", "pkg:npm/foo@1.0.0", true)]
|
||||
[InlineData("pkg:npm/*", "pkg:maven/org.apache.logging.log4j@2.0.0", false)]
|
||||
[InlineData("pkg:maven/org.apache.*", "pkg:maven/org.apache.logging.log4j@2.0.0", true)]
|
||||
[InlineData("pkg:maven/org.apache.*", "pkg:maven/org.eclipse.jetty@11.0.0", false)]
|
||||
[InlineData("pkg:npm/@scope/pkg@1.0.0", "PKG:NPM/@SCOPE/PKG@1.0.0", true)]
|
||||
public void IsMatch_HandlesGlobPatterns(string pattern, string? purl, bool expected)
|
||||
{
|
||||
var matcher = new PurlPatternMatcher(pattern);
|
||||
Assert.Equal(expected, matcher.IsMatch(purl));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Constructor_RejectsEmptyPattern(string pattern)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new PurlPatternMatcher(pattern));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Core.Configuration;
|
||||
using StellaOps.Scanner.Core.TrustAnchors;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Core.Tests;
|
||||
|
||||
public sealed class TrustAnchorRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResolveForPurl_ReturnsNullWhenDisabled()
|
||||
{
|
||||
var options = new OfflineKitOptions
|
||||
{
|
||||
Enabled = false,
|
||||
TrustAnchors = new List<TrustAnchorConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AnchorId = "default",
|
||||
PurlPattern = "*",
|
||||
AllowedKeyIds = new List<string> { "sha256:abcdef" },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var registry = new TrustAnchorRegistry(
|
||||
new StaticOptionsMonitor<OfflineKitOptions>(options),
|
||||
new StubKeyLoader(new Dictionary<string, byte[]>()),
|
||||
NullLogger<TrustAnchorRegistry>.Instance,
|
||||
TimeProvider.System);
|
||||
|
||||
Assert.Null(registry.ResolveForPurl("pkg:npm/foo@1.0.0"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveForPurl_FirstMatchWins()
|
||||
{
|
||||
var options = new OfflineKitOptions
|
||||
{
|
||||
Enabled = true,
|
||||
TrustAnchors = new List<TrustAnchorConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AnchorId = "catch-all",
|
||||
PurlPattern = "*",
|
||||
AllowedKeyIds = new List<string> { "sha256:aaaa" },
|
||||
},
|
||||
new()
|
||||
{
|
||||
AnchorId = "npm",
|
||||
PurlPattern = "pkg:npm/*",
|
||||
AllowedKeyIds = new List<string> { "sha256:bbbb" },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var keys = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["aaaa"] = new byte[] { 0x01, 0x02 },
|
||||
["bbbb"] = new byte[] { 0x03, 0x04 },
|
||||
};
|
||||
|
||||
var registry = new TrustAnchorRegistry(
|
||||
new StaticOptionsMonitor<OfflineKitOptions>(options),
|
||||
new StubKeyLoader(keys),
|
||||
NullLogger<TrustAnchorRegistry>.Instance,
|
||||
TimeProvider.System);
|
||||
|
||||
var resolution = registry.ResolveForPurl("pkg:npm/foo@1.0.0");
|
||||
Assert.NotNull(resolution);
|
||||
Assert.Equal("catch-all", resolution!.AnchorId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveForPurl_SkipsExpiredAnchors()
|
||||
{
|
||||
var options = new OfflineKitOptions
|
||||
{
|
||||
Enabled = true,
|
||||
TrustAnchors = new List<TrustAnchorConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AnchorId = "expired",
|
||||
PurlPattern = "*",
|
||||
AllowedKeyIds = new List<string> { "sha256:aaaa" },
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(-1)
|
||||
},
|
||||
new()
|
||||
{
|
||||
AnchorId = "active",
|
||||
PurlPattern = "*",
|
||||
AllowedKeyIds = new List<string> { "sha256:bbbb" },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var keys = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["aaaa"] = new byte[] { 0x01, 0x02 },
|
||||
["bbbb"] = new byte[] { 0x03, 0x04 },
|
||||
};
|
||||
|
||||
var registry = new TrustAnchorRegistry(
|
||||
new StaticOptionsMonitor<OfflineKitOptions>(options),
|
||||
new StubKeyLoader(keys),
|
||||
NullLogger<TrustAnchorRegistry>.Instance,
|
||||
TimeProvider.System);
|
||||
|
||||
var resolution = registry.ResolveForPurl("pkg:maven/org.example/app@1.0.0");
|
||||
Assert.NotNull(resolution);
|
||||
Assert.Equal("active", resolution!.AnchorId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveForPurl_NormalizesKeyIdsAndAddsSha256Alias()
|
||||
{
|
||||
var options = new OfflineKitOptions
|
||||
{
|
||||
Enabled = true,
|
||||
TrustAnchors = new List<TrustAnchorConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AnchorId = "npm",
|
||||
PurlPattern = "pkg:npm/*",
|
||||
AllowedKeyIds = new List<string> { "sha256:ABCDEF" },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var keys = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["abcdef"] = new byte[] { 0x01, 0x02, 0x03 },
|
||||
};
|
||||
|
||||
var registry = new TrustAnchorRegistry(
|
||||
new StaticOptionsMonitor<OfflineKitOptions>(options),
|
||||
new StubKeyLoader(keys),
|
||||
NullLogger<TrustAnchorRegistry>.Instance,
|
||||
TimeProvider.System);
|
||||
|
||||
var resolution = registry.ResolveForPurl("pkg:npm/foo@1.0.0");
|
||||
Assert.NotNull(resolution);
|
||||
Assert.Equal(new[] { "abcdef" }, resolution!.AllowedKeyIds);
|
||||
Assert.True(resolution.PublicKeys.ContainsKey("abcdef"));
|
||||
Assert.True(resolution.PublicKeys.ContainsKey("sha256:abcdef"));
|
||||
}
|
||||
|
||||
private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>
|
||||
{
|
||||
public StaticOptionsMonitor(T currentValue) => CurrentValue = currentValue;
|
||||
|
||||
public T CurrentValue { get; }
|
||||
|
||||
public T Get(string? name) => CurrentValue;
|
||||
|
||||
public IDisposable? OnChange(Action<T, string?> listener) => NullDisposable.Instance;
|
||||
|
||||
private sealed class NullDisposable : IDisposable
|
||||
{
|
||||
public static readonly NullDisposable Instance = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubKeyLoader : IPublicKeyLoader
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, byte[]> _keys;
|
||||
|
||||
public StubKeyLoader(IReadOnlyDictionary<string, byte[]> keys) => _keys = keys;
|
||||
|
||||
public byte[]? LoadKey(string keyId, string? keyDirectory)
|
||||
=> _keys.TryGetValue(keyId, out var bytes) ? bytes : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Infrastructure.Postgres.Testing;
|
||||
using StellaOps.Scanner.Storage;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
using StellaOps.Scanner.WebService.Diagnostics;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
|
||||
internal sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceStatus>
|
||||
{
|
||||
private readonly ScannerWebServicePostgresFixture postgresFixture;
|
||||
private readonly Dictionary<string, string?> configuration = new()
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
using StellaOps.Scanner.Worker.Determinism;
|
||||
using StellaOps.Scanner.Worker.Determinism.Calculators;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests.Determinism;
|
||||
|
||||
public sealed class BitwiseFidelityCalculatorTests
|
||||
{
|
||||
private readonly BitwiseFidelityCalculator _calculator = new();
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithEmptyReplays_ReturnsFullScore()
|
||||
{
|
||||
var baseline = new Dictionary<string, string>
|
||||
{
|
||||
["file1.json"] = "hash1",
|
||||
["file2.json"] = "hash2"
|
||||
};
|
||||
var replays = Array.Empty<IReadOnlyDictionary<string, string>>();
|
||||
|
||||
var (score, identicalCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(1.0, score);
|
||||
Assert.Equal(0, identicalCount);
|
||||
Assert.Empty(mismatches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithIdenticalReplays_ReturnsFullScore()
|
||||
{
|
||||
var baseline = new Dictionary<string, string>
|
||||
{
|
||||
["sbom.json"] = "sha256:abc",
|
||||
["findings.ndjson"] = "sha256:def"
|
||||
};
|
||||
var replays = new List<IReadOnlyDictionary<string, string>>
|
||||
{
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["sbom.json"] = "sha256:abc",
|
||||
["findings.ndjson"] = "sha256:def"
|
||||
},
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["sbom.json"] = "sha256:abc",
|
||||
["findings.ndjson"] = "sha256:def"
|
||||
}
|
||||
};
|
||||
|
||||
var (score, identicalCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(1.0, score);
|
||||
Assert.Equal(2, identicalCount);
|
||||
Assert.Empty(mismatches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithPartialMismatch_ReturnsPartialScore()
|
||||
{
|
||||
var baseline = new Dictionary<string, string>
|
||||
{
|
||||
["sbom.json"] = "sha256:abc",
|
||||
["findings.ndjson"] = "sha256:def"
|
||||
};
|
||||
var replays = new List<IReadOnlyDictionary<string, string>>
|
||||
{
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["sbom.json"] = "sha256:abc",
|
||||
["findings.ndjson"] = "sha256:def"
|
||||
},
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["sbom.json"] = "sha256:abc",
|
||||
["findings.ndjson"] = "sha256:DIFFERENT" // Mismatch
|
||||
},
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["sbom.json"] = "sha256:abc",
|
||||
["findings.ndjson"] = "sha256:def"
|
||||
}
|
||||
};
|
||||
|
||||
var (score, identicalCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(2.0 / 3, score, precision: 4);
|
||||
Assert.Equal(2, identicalCount);
|
||||
Assert.Single(mismatches);
|
||||
Assert.Equal(1, mismatches[0].RunIndex);
|
||||
Assert.Equal(FidelityMismatchType.BitwiseOnly, mismatches[0].Type);
|
||||
Assert.Contains("findings.ndjson", mismatches[0].AffectedArtifacts!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithMissingArtifact_DetectsMismatch()
|
||||
{
|
||||
var baseline = new Dictionary<string, string>
|
||||
{
|
||||
["file1.json"] = "hash1",
|
||||
["file2.json"] = "hash2"
|
||||
};
|
||||
var replays = new List<IReadOnlyDictionary<string, string>>
|
||||
{
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["file1.json"] = "hash1"
|
||||
// file2.json missing
|
||||
}
|
||||
};
|
||||
|
||||
var (score, identicalCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(0.0, score);
|
||||
Assert.Equal(0, identicalCount);
|
||||
Assert.Single(mismatches);
|
||||
Assert.Contains("file2.json", mismatches[0].AffectedArtifacts!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithExtraArtifact_DetectsMismatch()
|
||||
{
|
||||
var baseline = new Dictionary<string, string>
|
||||
{
|
||||
["file1.json"] = "hash1"
|
||||
};
|
||||
var replays = new List<IReadOnlyDictionary<string, string>>
|
||||
{
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["file1.json"] = "hash1",
|
||||
["extra.json"] = "extra_hash" // Extra artifact
|
||||
}
|
||||
};
|
||||
|
||||
var (score, identicalCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(0.0, score);
|
||||
Assert.Single(mismatches);
|
||||
Assert.Contains("extra.json", mismatches[0].AffectedArtifacts!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_IsCaseInsensitiveForHashes()
|
||||
{
|
||||
var baseline = new Dictionary<string, string>
|
||||
{
|
||||
["file.json"] = "SHA256:ABCDEF"
|
||||
};
|
||||
var replays = new List<IReadOnlyDictionary<string, string>>
|
||||
{
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["file.json"] = "sha256:abcdef" // Different case
|
||||
}
|
||||
};
|
||||
|
||||
var (score, identicalCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(1.0, score);
|
||||
Assert.Equal(1, identicalCount);
|
||||
Assert.Empty(mismatches);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using StellaOps.Scanner.Worker.Determinism;
|
||||
using StellaOps.Scanner.Worker.Determinism.Calculators;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests.Determinism;
|
||||
|
||||
public sealed class SemanticFidelityCalculatorTests
|
||||
{
|
||||
private readonly SemanticFidelityCalculator _calculator = new();
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithEmptyReplays_ReturnsFullScore()
|
||||
{
|
||||
var baseline = CreateBaseline();
|
||||
var replays = Array.Empty<NormalizedFindings>();
|
||||
|
||||
var (score, matchCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(1.0, score);
|
||||
Assert.Equal(0, matchCount);
|
||||
Assert.Empty(mismatches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithIdenticalFindings_ReturnsFullScore()
|
||||
{
|
||||
var baseline = CreateBaseline();
|
||||
var replays = new List<NormalizedFindings>
|
||||
{
|
||||
CreateBaseline(),
|
||||
CreateBaseline()
|
||||
};
|
||||
|
||||
var (score, matchCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(1.0, score);
|
||||
Assert.Equal(2, matchCount);
|
||||
Assert.Empty(mismatches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithDifferentPackages_DetectsMismatch()
|
||||
{
|
||||
var baseline = CreateBaseline();
|
||||
var replays = new List<NormalizedFindings>
|
||||
{
|
||||
new NormalizedFindings
|
||||
{
|
||||
Packages = new List<NormalizedPackage>
|
||||
{
|
||||
new("pkg:npm/lodash@4.17.21", "4.17.21"),
|
||||
new("pkg:npm/extra@1.0.0", "1.0.0") // Extra package
|
||||
},
|
||||
Cves = new HashSet<string> { "CVE-2021-23337" },
|
||||
SeverityCounts = new Dictionary<string, int> { ["HIGH"] = 1 },
|
||||
Verdicts = new Dictionary<string, string> { ["overall"] = "fail" }
|
||||
}
|
||||
};
|
||||
|
||||
var (score, matchCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(0.0, score);
|
||||
Assert.Equal(0, matchCount);
|
||||
Assert.Single(mismatches);
|
||||
Assert.Contains("packages", mismatches[0].AffectedArtifacts!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithDifferentCves_DetectsMismatch()
|
||||
{
|
||||
var baseline = CreateBaseline();
|
||||
var replays = new List<NormalizedFindings>
|
||||
{
|
||||
new NormalizedFindings
|
||||
{
|
||||
Packages = new List<NormalizedPackage>
|
||||
{
|
||||
new("pkg:npm/lodash@4.17.21", "4.17.21")
|
||||
},
|
||||
Cves = new HashSet<string> { "CVE-2021-23337", "CVE-2022-12345" }, // Extra CVE
|
||||
SeverityCounts = new Dictionary<string, int> { ["HIGH"] = 1 },
|
||||
Verdicts = new Dictionary<string, string> { ["overall"] = "fail" }
|
||||
}
|
||||
};
|
||||
|
||||
var (score, matchCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(0.0, score);
|
||||
Assert.Contains("cves", mismatches[0].AffectedArtifacts!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithDifferentSeverities_DetectsMismatch()
|
||||
{
|
||||
var baseline = CreateBaseline();
|
||||
var replays = new List<NormalizedFindings>
|
||||
{
|
||||
new NormalizedFindings
|
||||
{
|
||||
Packages = new List<NormalizedPackage>
|
||||
{
|
||||
new("pkg:npm/lodash@4.17.21", "4.17.21")
|
||||
},
|
||||
Cves = new HashSet<string> { "CVE-2021-23337" },
|
||||
SeverityCounts = new Dictionary<string, int> { ["CRITICAL"] = 1 }, // Different severity
|
||||
Verdicts = new Dictionary<string, string> { ["overall"] = "fail" }
|
||||
}
|
||||
};
|
||||
|
||||
var (score, matchCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(0.0, score);
|
||||
Assert.Contains("severities", mismatches[0].AffectedArtifacts!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithDifferentVerdicts_DetectsMismatch()
|
||||
{
|
||||
var baseline = CreateBaseline();
|
||||
var replays = new List<NormalizedFindings>
|
||||
{
|
||||
new NormalizedFindings
|
||||
{
|
||||
Packages = new List<NormalizedPackage>
|
||||
{
|
||||
new("pkg:npm/lodash@4.17.21", "4.17.21")
|
||||
},
|
||||
Cves = new HashSet<string> { "CVE-2021-23337" },
|
||||
SeverityCounts = new Dictionary<string, int> { ["HIGH"] = 1 },
|
||||
Verdicts = new Dictionary<string, string> { ["overall"] = "pass" } // Different verdict
|
||||
}
|
||||
};
|
||||
|
||||
var (score, matchCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(0.0, score);
|
||||
Assert.Contains("verdicts", mismatches[0].AffectedArtifacts!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithPartialMatches_ReturnsCorrectScore()
|
||||
{
|
||||
var baseline = CreateBaseline();
|
||||
var replays = new List<NormalizedFindings>
|
||||
{
|
||||
CreateBaseline(), // Match
|
||||
new NormalizedFindings // Mismatch
|
||||
{
|
||||
Packages = new List<NormalizedPackage>(),
|
||||
Cves = new HashSet<string>(),
|
||||
SeverityCounts = new Dictionary<string, int>(),
|
||||
Verdicts = new Dictionary<string, string>()
|
||||
},
|
||||
CreateBaseline() // Match
|
||||
};
|
||||
|
||||
var (score, matchCount, mismatches) = _calculator.Calculate(baseline, replays);
|
||||
|
||||
Assert.Equal(2.0 / 3, score, precision: 4);
|
||||
Assert.Equal(2, matchCount);
|
||||
Assert.Single(mismatches);
|
||||
}
|
||||
|
||||
private static NormalizedFindings CreateBaseline() => new()
|
||||
{
|
||||
Packages = new List<NormalizedPackage>
|
||||
{
|
||||
new("pkg:npm/lodash@4.17.21", "4.17.21")
|
||||
},
|
||||
Cves = new HashSet<string> { "CVE-2021-23337" },
|
||||
SeverityCounts = new Dictionary<string, int> { ["HIGH"] = 1 },
|
||||
Verdicts = new Dictionary<string, string> { ["overall"] = "fail" }
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user