Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -36,6 +36,7 @@ public static class BinaryIndexServiceExtensions
|
||||
services.AddScoped<IBinaryVulnerabilityService, BinaryVulnerabilityService>();
|
||||
services.AddScoped<IBinaryFeatureExtractor, ElfFeatureExtractor>();
|
||||
services.AddScoped<BinaryVulnerabilityAnalyzer>();
|
||||
services.AddScoped<Processing.BinaryFindingMapper>();
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -87,4 +88,40 @@ internal sealed class NullBinaryVulnerabilityService : IBinaryVulnerabilityServi
|
||||
{
|
||||
return Task.FromResult(System.Collections.Immutable.ImmutableDictionary<string, System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>>.Empty);
|
||||
}
|
||||
|
||||
public Task<StellaOps.BinaryIndex.FixIndex.Models.FixStatusResult?> GetFixStatusAsync(
|
||||
string distro,
|
||||
string release,
|
||||
string sourcePkg,
|
||||
string cveId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult<StellaOps.BinaryIndex.FixIndex.Models.FixStatusResult?>(null);
|
||||
}
|
||||
|
||||
public Task<System.Collections.Immutable.ImmutableDictionary<string, StellaOps.BinaryIndex.FixIndex.Models.FixStatusResult>> GetFixStatusBatchAsync(
|
||||
string distro,
|
||||
string release,
|
||||
string sourcePkg,
|
||||
IEnumerable<string> cveIds,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(System.Collections.Immutable.ImmutableDictionary<string, StellaOps.BinaryIndex.FixIndex.Models.FixStatusResult>.Empty);
|
||||
}
|
||||
|
||||
public Task<System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>> LookupByFingerprintAsync(
|
||||
byte[] fingerprint,
|
||||
FingerprintLookupOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>.Empty);
|
||||
}
|
||||
|
||||
public Task<System.Collections.Immutable.ImmutableDictionary<string, System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>>> LookupByFingerprintBatchAsync(
|
||||
IEnumerable<(string Key, byte[] Fingerprint)> fingerprints,
|
||||
FingerprintLookupOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(System.Collections.Immutable.ImmutableDictionary<string, System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>>.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BinaryFindingMapper.cs
|
||||
// Sprint: SPRINT_20251226_014_BINIDX
|
||||
// Task: SCANINT-08 — Create BinaryFindingMapper to convert matches to findings
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.BinaryIndex.Core.Services;
|
||||
using FixStatusResult = StellaOps.BinaryIndex.Core.Services.FixStatusResult;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing;
|
||||
|
||||
/// <summary>
|
||||
/// Maps binary vulnerability findings to the standard scanner finding format.
|
||||
/// Enables integration with the Findings Ledger and triage workflow.
|
||||
/// </summary>
|
||||
public sealed class BinaryFindingMapper
|
||||
{
|
||||
private readonly IBinaryVulnerabilityService _binaryVulnService;
|
||||
private readonly ILogger<BinaryFindingMapper> _logger;
|
||||
|
||||
public BinaryFindingMapper(
|
||||
IBinaryVulnerabilityService binaryVulnService,
|
||||
ILogger<BinaryFindingMapper> logger)
|
||||
{
|
||||
_binaryVulnService = binaryVulnService ?? throw new ArgumentNullException(nameof(binaryVulnService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a single binary finding to a standard finding.
|
||||
/// </summary>
|
||||
public Finding MapToFinding(BinaryVulnerabilityFinding finding, string? distro = null, string? release = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(finding);
|
||||
|
||||
var findingId = GenerateFindingId(finding);
|
||||
var severity = GetSeverityFromCve(finding.CveId);
|
||||
|
||||
return new Finding
|
||||
{
|
||||
Id = findingId,
|
||||
Type = FindingType.BinaryVulnerability,
|
||||
Severity = severity,
|
||||
Title = $"Binary contains vulnerable code: {finding.CveId}",
|
||||
Description = GenerateDescription(finding),
|
||||
CveId = finding.CveId,
|
||||
Purl = finding.VulnerablePurl,
|
||||
Evidence = new BinaryFindingEvidence
|
||||
{
|
||||
BinaryKey = finding.BinaryKey,
|
||||
LayerDigest = finding.LayerDigest,
|
||||
MatchMethod = finding.MatchMethod,
|
||||
Confidence = finding.Confidence,
|
||||
Similarity = finding.Evidence?.Similarity,
|
||||
MatchedFunction = finding.Evidence?.MatchedFunction,
|
||||
BuildId = finding.Evidence?.BuildId
|
||||
},
|
||||
Remediation = GenerateRemediation(finding),
|
||||
ScanId = finding.ScanId,
|
||||
DetectedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps multiple binary findings to standard findings with fix status enrichment.
|
||||
/// </summary>
|
||||
public async Task<ImmutableArray<Finding>> MapToFindingsAsync(
|
||||
IEnumerable<BinaryVulnerabilityFinding> findings,
|
||||
string? distro,
|
||||
string? release,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var result = new List<Finding>();
|
||||
var findingsList = findings.ToList();
|
||||
|
||||
// Group by source package for batch fix status lookup
|
||||
var groupedByPurl = findingsList
|
||||
.GroupBy(f => ExtractSourcePackage(f.VulnerablePurl))
|
||||
.Where(g => !string.IsNullOrEmpty(g.Key));
|
||||
|
||||
foreach (var group in groupedByPurl)
|
||||
{
|
||||
var sourcePkg = group.Key!;
|
||||
var cveIds = group.Select(f => f.CveId).Distinct().ToList();
|
||||
|
||||
// Batch fix status lookup
|
||||
ImmutableDictionary<string, FixStatusResult>? fixStatuses = null;
|
||||
if (!string.IsNullOrEmpty(distro) && !string.IsNullOrEmpty(release))
|
||||
{
|
||||
try
|
||||
{
|
||||
fixStatuses = await _binaryVulnService.GetFixStatusBatchAsync(
|
||||
distro, release, sourcePkg, cveIds, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to get fix status for {SourcePkg}", sourcePkg);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var finding in group)
|
||||
{
|
||||
var mapped = MapToFinding(finding, distro, release);
|
||||
|
||||
// Enrich with fix status if available
|
||||
if (fixStatuses != null && fixStatuses.TryGetValue(finding.CveId, out var fixStatus))
|
||||
{
|
||||
mapped = mapped with
|
||||
{
|
||||
FixStatus = MapFixStatus(fixStatus),
|
||||
FixedVersion = fixStatus.FixedVersion
|
||||
};
|
||||
}
|
||||
|
||||
result.Add(mapped);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle findings without valid PURLs
|
||||
foreach (var finding in findingsList.Where(f => string.IsNullOrEmpty(ExtractSourcePackage(f.VulnerablePurl))))
|
||||
{
|
||||
result.Add(MapToFinding(finding, distro, release));
|
||||
}
|
||||
|
||||
_logger.LogInformation("Mapped {Count} binary findings", result.Count);
|
||||
return result.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static Guid GenerateFindingId(BinaryVulnerabilityFinding finding)
|
||||
{
|
||||
// Generate deterministic ID based on scan, CVE, and binary key
|
||||
var input = $"{finding.ScanId}:{finding.CveId}:{finding.BinaryKey}:{finding.LayerDigest}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return new Guid(hash.AsSpan()[..16]);
|
||||
}
|
||||
|
||||
private static Severity GetSeverityFromCve(string cveId)
|
||||
{
|
||||
// In production, this would look up CVSS from advisory data
|
||||
// For now, return Unknown and let downstream enrichment set it
|
||||
return Severity.Unknown;
|
||||
}
|
||||
|
||||
private static string GenerateDescription(BinaryVulnerabilityFinding finding)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"A binary file in the container image contains code affected by {finding.CveId}.");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"**Detection Method:** {finding.MatchMethod}");
|
||||
sb.AppendLine($"**Confidence:** {finding.Confidence:P0}");
|
||||
|
||||
if (finding.Evidence?.MatchedFunction is not null)
|
||||
{
|
||||
sb.AppendLine($"**Vulnerable Function:** {finding.Evidence.MatchedFunction}");
|
||||
}
|
||||
|
||||
if (finding.Evidence?.BuildId is not null)
|
||||
{
|
||||
sb.AppendLine($"**Build-ID:** {finding.Evidence.BuildId}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateRemediation(BinaryVulnerabilityFinding finding)
|
||||
{
|
||||
return $"Update the package containing the binary to a version that includes the fix for {finding.CveId}. " +
|
||||
$"If using a distro package, check if a backported security update is available.";
|
||||
}
|
||||
|
||||
private static string? ExtractSourcePackage(string purl)
|
||||
{
|
||||
// Extract package name from PURL
|
||||
// e.g., "pkg:deb/debian/openssl@1.1.1" -> "openssl"
|
||||
if (string.IsNullOrEmpty(purl))
|
||||
return null;
|
||||
|
||||
var atIndex = purl.IndexOf('@');
|
||||
var slashIndex = purl.LastIndexOf('/', atIndex > 0 ? atIndex : purl.Length);
|
||||
|
||||
if (slashIndex >= 0)
|
||||
{
|
||||
var endIndex = atIndex > slashIndex ? atIndex : purl.Length;
|
||||
return purl[(slashIndex + 1)..endIndex];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static FindingFixStatus MapFixStatus(FixStatusResult status)
|
||||
{
|
||||
return status.State switch
|
||||
{
|
||||
FixState.Fixed => FindingFixStatus.Fixed,
|
||||
FixState.Vulnerable => FindingFixStatus.Vulnerable,
|
||||
FixState.NotAffected => FindingFixStatus.NotAffected,
|
||||
FixState.WontFix => FindingFixStatus.WontFix,
|
||||
_ => FindingFixStatus.Unknown
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Standard scanner finding.
|
||||
/// </summary>
|
||||
public sealed record Finding
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required FindingType Type { get; init; }
|
||||
public required Severity Severity { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public string? CveId { get; init; }
|
||||
public string? Purl { get; init; }
|
||||
public required BinaryFindingEvidence Evidence { get; init; }
|
||||
public required string Remediation { get; init; }
|
||||
public Guid ScanId { get; init; }
|
||||
public DateTimeOffset DetectedAt { get; init; }
|
||||
public FindingFixStatus FixStatus { get; init; } = FindingFixStatus.Unknown;
|
||||
public string? FixedVersion { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evidence specific to binary vulnerability findings.
|
||||
/// </summary>
|
||||
public sealed record BinaryFindingEvidence
|
||||
{
|
||||
public required string BinaryKey { get; init; }
|
||||
public required string LayerDigest { get; init; }
|
||||
public required string MatchMethod { get; init; }
|
||||
public required decimal Confidence { get; init; }
|
||||
public decimal? Similarity { get; init; }
|
||||
public string? MatchedFunction { get; init; }
|
||||
public string? BuildId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finding type enumeration.
|
||||
/// </summary>
|
||||
public enum FindingType
|
||||
{
|
||||
PackageVulnerability,
|
||||
BinaryVulnerability,
|
||||
PolicyViolation,
|
||||
SecretExposure,
|
||||
MisconfigurationDebian
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Severity levels for findings.
|
||||
/// </summary>
|
||||
public enum Severity
|
||||
{
|
||||
Unknown,
|
||||
None,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Critical
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix status for findings.
|
||||
/// </summary>
|
||||
public enum FindingFixStatus
|
||||
{
|
||||
Unknown,
|
||||
Vulnerable,
|
||||
Fixed,
|
||||
NotAffected,
|
||||
WontFix
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix state from the binary index.
|
||||
/// </summary>
|
||||
public enum FixState
|
||||
{
|
||||
Unknown,
|
||||
Vulnerable,
|
||||
Fixed,
|
||||
NotAffected,
|
||||
WontFix
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BinaryLookupStageExecutor.cs
|
||||
// Sprint: SPRINT_20251226_014_BINIDX
|
||||
// Task: SCANINT-02 — Create IBinaryLookupStep in scan pipeline
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.BinaryIndex.Core.Models;
|
||||
using StellaOps.BinaryIndex.Core.Services;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Worker.Extensions;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing;
|
||||
|
||||
/// <summary>
|
||||
/// Scan pipeline stage that performs binary vulnerability lookups.
|
||||
/// Runs after analyzers to correlate binary identities with known vulnerabilities.
|
||||
/// </summary>
|
||||
public sealed class BinaryLookupStageExecutor : IScanStageExecutor
|
||||
{
|
||||
private readonly BinaryVulnerabilityAnalyzer _analyzer;
|
||||
private readonly BinaryIndexOptions _options;
|
||||
private readonly ILogger<BinaryLookupStageExecutor> _logger;
|
||||
|
||||
public BinaryLookupStageExecutor(
|
||||
BinaryVulnerabilityAnalyzer analyzer,
|
||||
BinaryIndexOptions options,
|
||||
ILogger<BinaryLookupStageExecutor> logger)
|
||||
{
|
||||
_analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public string StageName => ScanStageNames.BinaryLookup;
|
||||
|
||||
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
_logger.LogDebug("Binary vulnerability analysis disabled, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Starting binary vulnerability lookup for scan {ScanId}",
|
||||
context.ScanId);
|
||||
|
||||
var allFindings = new List<BinaryVulnerabilityFinding>();
|
||||
var layerContexts = BuildLayerContexts(context);
|
||||
|
||||
foreach (var layerContext in layerContexts)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _analyzer.AnalyzeLayerAsync(layerContext, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (result.Findings.Length > 0)
|
||||
{
|
||||
allFindings.AddRange(result.Findings);
|
||||
_logger.LogInformation(
|
||||
"Found {Count} binary vulnerabilities in layer {Layer}",
|
||||
result.Findings.Length,
|
||||
layerContext.LayerDigest);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Failed to analyze layer {Layer} for binary vulnerabilities",
|
||||
layerContext.LayerDigest);
|
||||
}
|
||||
}
|
||||
|
||||
// Store findings in analysis context for downstream stages
|
||||
context.Analysis.SetBinaryFindings(allFindings.ToImmutableArray());
|
||||
|
||||
_logger.LogInformation(
|
||||
"Binary vulnerability lookup complete for scan {ScanId}: {Count} findings",
|
||||
context.ScanId,
|
||||
allFindings.Count);
|
||||
}
|
||||
|
||||
private IReadOnlyList<BinaryLayerContext> BuildLayerContexts(ScanJobContext context)
|
||||
{
|
||||
var contexts = new List<BinaryLayerContext>();
|
||||
|
||||
// Get layer information from the scan context
|
||||
var layers = context.Analysis.GetLayers();
|
||||
if (layers == null || layers.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("No layers found in scan context");
|
||||
return contexts;
|
||||
}
|
||||
|
||||
var distro = context.Analysis.GetDetectedDistro();
|
||||
var release = context.Analysis.GetDetectedRelease();
|
||||
|
||||
foreach (var layer in layers)
|
||||
{
|
||||
var binaryPaths = context.Analysis.GetBinaryPathsForLayer(layer.Digest);
|
||||
if (binaryPaths == null || binaryPaths.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
contexts.Add(new BinaryLayerContext
|
||||
{
|
||||
ScanId = Guid.Parse(context.ScanId),
|
||||
LayerDigest = layer.Digest,
|
||||
BinaryPaths = binaryPaths,
|
||||
DetectedDistro = distro,
|
||||
DetectedRelease = release,
|
||||
OpenFile = path => context.Analysis.OpenLayerFile(layer.Digest, path)
|
||||
});
|
||||
}
|
||||
|
||||
return contexts;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for ScanAnalysisStore to support binary analysis.
|
||||
/// </summary>
|
||||
public static class BinaryScanAnalysisStoreExtensions
|
||||
{
|
||||
private const string BinaryFindingsKey = "binary_findings";
|
||||
private const string LayersKey = "layers";
|
||||
private const string DistroKey = "detected_distro";
|
||||
private const string ReleaseKey = "detected_release";
|
||||
|
||||
public static void SetBinaryFindings(
|
||||
this ScanAnalysisStore store,
|
||||
ImmutableArray<BinaryVulnerabilityFinding> findings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
store.Set(BinaryFindingsKey, findings);
|
||||
}
|
||||
|
||||
public static ImmutableArray<BinaryVulnerabilityFinding> GetBinaryFindings(
|
||||
this ScanAnalysisStore store)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
if (store.TryGet<ImmutableArray<BinaryVulnerabilityFinding>>(BinaryFindingsKey, out var findings) && !findings.IsDefault)
|
||||
{
|
||||
return findings;
|
||||
}
|
||||
return ImmutableArray<BinaryVulnerabilityFinding>.Empty;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<LayerInfo>? GetLayers(this ScanAnalysisStore store)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
if (store.TryGet<IReadOnlyList<LayerInfo>>(LayersKey, out var layers))
|
||||
{
|
||||
return layers;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string? GetDetectedDistro(this ScanAnalysisStore store)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
if (store.TryGet<string>(DistroKey, out var distro))
|
||||
{
|
||||
return distro;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string? GetDetectedRelease(this ScanAnalysisStore store)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
if (store.TryGet<string>(ReleaseKey, out var release))
|
||||
{
|
||||
return release;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string>? GetBinaryPathsForLayer(
|
||||
this ScanAnalysisStore store,
|
||||
string layerDigest)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
var key = $"binary_paths_{layerDigest}";
|
||||
if (store.TryGet<IReadOnlyList<string>>(key, out var paths))
|
||||
{
|
||||
return paths;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Stream? OpenLayerFile(
|
||||
this ScanAnalysisStore store,
|
||||
string layerDigest,
|
||||
string path)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
if (store.TryGet<Func<string, string, Stream?>>("layer_file_opener", out var opener))
|
||||
{
|
||||
return opener?.Invoke(layerDigest, path);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Layer metadata for binary analysis.
|
||||
/// </summary>
|
||||
public sealed record LayerInfo
|
||||
{
|
||||
public required string Digest { get; init; }
|
||||
public required string MediaType { get; init; }
|
||||
public long Size { get; init; }
|
||||
}
|
||||
@@ -20,6 +20,9 @@ public static class ScanStageNames
|
||||
// Sprint: SPRINT_3500_0001_0001 - Proof of Exposure
|
||||
public const string GeneratePoE = "generate-poe";
|
||||
|
||||
// Sprint: SPRINT_20251226_014_BINIDX - Binary Vulnerability Lookup
|
||||
public const string BinaryLookup = "binary-lookup";
|
||||
|
||||
public static readonly IReadOnlyList<string> Ordered = new[]
|
||||
{
|
||||
IngestReplay,
|
||||
@@ -27,6 +30,7 @@ public static class ScanStageNames
|
||||
PullLayers,
|
||||
BuildFilesystem,
|
||||
ExecuteAnalyzers,
|
||||
BinaryLookup,
|
||||
EpssEnrichment,
|
||||
ComposeArtifacts,
|
||||
Entropy,
|
||||
|
||||
@@ -27,6 +27,7 @@ using StellaOps.Scanner.Worker.Options;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using StellaOps.Scanner.Worker.Processing.Entropy;
|
||||
using StellaOps.Scanner.Worker.Determinism;
|
||||
using StellaOps.Scanner.Worker.Extensions;
|
||||
using StellaOps.Scanner.Worker.Processing.Surface;
|
||||
using StellaOps.Scanner.Storage.Extensions;
|
||||
using StellaOps.Scanner.Storage;
|
||||
@@ -93,6 +94,10 @@ builder.Services.AddSingleton<IDelayScheduler, SystemDelayScheduler>();
|
||||
|
||||
builder.Services.AddEntryTraceAnalyzer();
|
||||
builder.Services.AddSingleton<IEntryTraceExecutionService, EntryTraceExecutionService>();
|
||||
|
||||
// BinaryIndex integration for binary vulnerability detection (Sprint: SPRINT_20251226_014_BINIDX)
|
||||
builder.Services.AddBinaryIndexIntegration(builder.Configuration);
|
||||
|
||||
builder.Services.AddSingleton<ReachabilityUnionWriter>();
|
||||
builder.Services.AddSingleton<ReachabilityUnionPublisher>();
|
||||
builder.Services.AddSingleton<IReachabilityUnionPublisherService, ReachabilityUnionPublisherService>();
|
||||
@@ -156,6 +161,7 @@ builder.Services.AddSingleton<NativeAnalyzerExecutor>();
|
||||
builder.Services.AddSingleton<IScanAnalyzerDispatcher, CompositeScanAnalyzerDispatcher>();
|
||||
builder.Services.AddSingleton<IScanStageExecutor, RegistrySecretStageExecutor>();
|
||||
builder.Services.AddSingleton<IScanStageExecutor, AnalyzerStageExecutor>();
|
||||
builder.Services.AddSingleton<IScanStageExecutor, BinaryLookupStageExecutor>();
|
||||
builder.Services.AddSingleton<IScanStageExecutor, EpssEnrichmentStageExecutor>();
|
||||
builder.Services.AddSingleton<IScanStageExecutor, Reachability.ReachabilityBuildStageExecutor>();
|
||||
builder.Services.AddSingleton<IScanStageExecutor, Reachability.ReachabilityPublishStageExecutor>();
|
||||
|
||||
@@ -15,5 +15,8 @@
|
||||
<ProjectReference Include="../../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Auth.Security/StellaOps.Auth.Security.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Scanner.ProofSpine/StellaOps.Scanner.ProofSpine.csproj" />
|
||||
<ProjectReference Include="../../../Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -13,5 +13,8 @@
|
||||
Use SliceDataDto and JsonElement instead of ReachabilitySlice type. -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Evidence\StellaOps.Scanner.Evidence.csproj" />
|
||||
<ProjectReference Include="..\..\..\Attestor\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -20,20 +20,23 @@ public class CecilMethodFingerprinterTests
|
||||
NullLogger<CecilMethodFingerprinter>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Ecosystem_ReturnsNuget()
|
||||
{
|
||||
Assert.Equal("nuget", _fingerprinter.Ecosystem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FingerprintAsync_WithNullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(
|
||||
() => _fingerprinter.FingerprintAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FingerprintAsync_WithNonExistentPath_ReturnsEmptyResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -53,7 +56,8 @@ public class CecilMethodFingerprinterTests
|
||||
Assert.Empty(result.Methods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FingerprintAsync_WithOwnAssembly_FindsMethods()
|
||||
{
|
||||
// Arrange - use the test assembly itself
|
||||
@@ -80,7 +84,8 @@ public class CecilMethodFingerprinterTests
|
||||
Assert.True(result.Methods.Count > 0, "Should find at least some methods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FingerprintAsync_ComputesDeterministicHashes()
|
||||
{
|
||||
// Arrange - fingerprint twice
|
||||
@@ -109,11 +114,13 @@ public class CecilMethodFingerprinterTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FingerprintAsync_WithCancellation_RespectsCancellation()
|
||||
{
|
||||
// Arrange
|
||||
using var cts = new CancellationTokenSource();
|
||||
using StellaOps.TestKit;
|
||||
cts.Cancel();
|
||||
|
||||
var testAssemblyPath = typeof(CecilMethodFingerprinterTests).Assembly.Location;
|
||||
@@ -142,7 +149,8 @@ public class CecilMethodFingerprinterTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FingerprintAsync_MethodKeyFormat_IsValid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -172,7 +180,8 @@ public class CecilMethodFingerprinterTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FingerprintAsync_IncludesSignature()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -8,11 +8,13 @@ using StellaOps.Scanner.VulnSurfaces.CallGraph;
|
||||
using StellaOps.Scanner.VulnSurfaces.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.VulnSurfaces.Tests;
|
||||
|
||||
public class InternalCallGraphTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddMethod_StoresMethod()
|
||||
{
|
||||
// Arrange
|
||||
@@ -38,7 +40,8 @@ public class InternalCallGraphTests
|
||||
Assert.Equal(1, graph.MethodCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddEdge_CreatesForwardAndReverseMapping()
|
||||
{
|
||||
// Arrange
|
||||
@@ -63,7 +66,8 @@ public class InternalCallGraphTests
|
||||
Assert.Equal(1, graph.EdgeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetPublicMethods_ReturnsOnlyPublic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -97,7 +101,8 @@ public class InternalCallGraphTests
|
||||
Assert.Equal("A::Public()", publicMethods[0].MethodKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetCallees_EmptyForUnknownMethod()
|
||||
{
|
||||
// Arrange
|
||||
@@ -114,7 +119,8 @@ public class InternalCallGraphTests
|
||||
Assert.Empty(callees);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetMethod_ReturnsNullForUnknown()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.VulnSurfaces.Fingerprint;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.VulnSurfaces.Tests;
|
||||
|
||||
public class MethodDiffEngineTests
|
||||
@@ -20,14 +21,16 @@ public class MethodDiffEngineTests
|
||||
NullLogger<MethodDiffEngine>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DiffAsync_WithNullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(
|
||||
() => _diffEngine.DiffAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DiffAsync_WithIdenticalFingerprints_ReturnsNoChanges()
|
||||
{
|
||||
// Arrange
|
||||
@@ -68,7 +71,8 @@ public class MethodDiffEngineTests
|
||||
Assert.Equal(0, diff.TotalChanges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DiffAsync_WithModifiedMethod_ReturnsModified()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +116,8 @@ public class MethodDiffEngineTests
|
||||
Assert.Empty(diff.Removed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DiffAsync_WithAddedMethod_ReturnsAdded()
|
||||
{
|
||||
// Arrange
|
||||
@@ -155,7 +160,8 @@ public class MethodDiffEngineTests
|
||||
Assert.Empty(diff.Removed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DiffAsync_WithRemovedMethod_ReturnsRemoved()
|
||||
{
|
||||
// Arrange
|
||||
@@ -198,7 +204,8 @@ public class MethodDiffEngineTests
|
||||
Assert.Equal("Test.Class::RemovedMethod", diff.Removed[0].MethodKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DiffAsync_WithMultipleChanges_ReturnsAllChanges()
|
||||
{
|
||||
// Arrange - simulate a fix that modifies one method, adds one, removes one
|
||||
@@ -247,7 +254,8 @@ public class MethodDiffEngineTests
|
||||
Assert.Equal(3, diff.TotalChanges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DiffAsync_TriggerMethods_AreModifiedOrRemoved()
|
||||
{
|
||||
// This test validates the key insight:
|
||||
@@ -298,7 +306,8 @@ public class MethodDiffEngineTests
|
||||
Assert.Empty(diff.Removed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DiffAsync_WithEmptyFingerprints_ReturnsNoChanges()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -35,7 +35,8 @@ public class NuGetPackageDownloaderTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Ecosystem_ReturnsNuget()
|
||||
{
|
||||
// Arrange
|
||||
@@ -45,7 +46,8 @@ public class NuGetPackageDownloaderTests : IDisposable
|
||||
Assert.Equal("nuget", downloader.Ecosystem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DownloadAsync_WithNullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -56,7 +58,8 @@ public class NuGetPackageDownloaderTests : IDisposable
|
||||
() => downloader.DownloadAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DownloadAsync_WithHttpError_ReturnsFailResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -93,7 +96,8 @@ public class NuGetPackageDownloaderTests : IDisposable
|
||||
Assert.Null(result.ExtractedPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DownloadAsync_WithValidNupkg_ReturnsSuccessResult()
|
||||
{
|
||||
// Arrange - create a mock .nupkg (which is just a zip file)
|
||||
@@ -135,7 +139,8 @@ public class NuGetPackageDownloaderTests : IDisposable
|
||||
Assert.False(result.FromCache);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DownloadAsync_WithCachedPackage_ReturnsCachedResult()
|
||||
{
|
||||
// Arrange - pre-create the cached directory
|
||||
@@ -162,7 +167,8 @@ public class NuGetPackageDownloaderTests : IDisposable
|
||||
Assert.Equal(packageDir, result.ExtractedPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DownloadAsync_WithCacheFalse_BypassesCache()
|
||||
{
|
||||
// Arrange - pre-create the cached directory
|
||||
@@ -210,7 +216,8 @@ public class NuGetPackageDownloaderTests : IDisposable
|
||||
ItExpr.IsAny<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DownloadAsync_UsesCorrectUrl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -250,7 +257,8 @@ public class NuGetPackageDownloaderTests : IDisposable
|
||||
Assert.EndsWith(".nupkg", capturedRequest.RequestUri!.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DownloadAsync_WithCustomRegistry_UsesCustomUrl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -289,7 +297,8 @@ public class NuGetPackageDownloaderTests : IDisposable
|
||||
Assert.StartsWith("https://custom.nuget.feed.example.com/v3", capturedRequest.RequestUri!.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DownloadAsync_WithCancellation_HonorsCancellation()
|
||||
{
|
||||
// Arrange
|
||||
@@ -344,6 +353,7 @@ public class NuGetPackageDownloaderTests : IDisposable
|
||||
// Add a minimal .nuspec file
|
||||
var nuspecEntry = archive.CreateEntry("test.nuspec");
|
||||
using var writer = new StreamWriter(nuspecEntry.Open());
|
||||
using StellaOps.TestKit;
|
||||
writer.Write("""
|
||||
<?xml version="1.0"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
|
||||
@@ -10,6 +10,7 @@ using StellaOps.Scanner.VulnSurfaces.Models;
|
||||
using StellaOps.Scanner.VulnSurfaces.Triggers;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.VulnSurfaces.Tests;
|
||||
|
||||
public class TriggerMethodExtractorTests
|
||||
@@ -21,7 +22,8 @@ public class TriggerMethodExtractorTests
|
||||
_extractor = new TriggerMethodExtractor(NullLogger<TriggerMethodExtractor>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_DirectPath_FindsTrigger()
|
||||
{
|
||||
// Arrange
|
||||
@@ -85,7 +87,8 @@ public class TriggerMethodExtractorTests
|
||||
Assert.False(trigger.IsInterfaceExpansion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_NoPath_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -124,7 +127,8 @@ public class TriggerMethodExtractorTests
|
||||
Assert.Empty(result.Triggers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_MultiplePublicMethods_FindsAllTriggers()
|
||||
{
|
||||
// Arrange
|
||||
@@ -174,7 +178,8 @@ public class TriggerMethodExtractorTests
|
||||
Assert.Contains(result.Triggers, t => t.TriggerMethodKey == "Class::Api2()");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_MaxDepthExceeded_DoesNotFindTrigger()
|
||||
{
|
||||
// Arrange
|
||||
@@ -231,7 +236,8 @@ public class TriggerMethodExtractorTests
|
||||
Assert.Empty(result.Triggers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_VirtualMethod_ReducesConfidence()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -4,10 +4,12 @@ using StellaOps.Scanner.VulnSurfaces.Services;
|
||||
using StellaOps.Scanner.VulnSurfaces.Storage;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.VulnSurfaces.Tests;
|
||||
|
||||
public sealed class VulnSurfaceServiceTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact(DisplayName = "GetAffectedSymbolsAsync returns sinks when surface exists")]
|
||||
public async Task GetAffectedSymbolsAsync_ReturnsSurfaceSinks()
|
||||
{
|
||||
@@ -50,6 +52,7 @@ public sealed class VulnSurfaceServiceTests
|
||||
Assert.Equal(surfaceGuid, repository.LastSurfaceId);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact(DisplayName = "GetAffectedSymbolsAsync falls back to package symbol provider")]
|
||||
public async Task GetAffectedSymbolsAsync_FallsBackToPackageSymbols()
|
||||
{
|
||||
@@ -64,6 +67,7 @@ public sealed class VulnSurfaceServiceTests
|
||||
Assert.Single(result.Symbols);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact(DisplayName = "GetAffectedSymbolsAsync returns heuristic when no data")]
|
||||
public async Task GetAffectedSymbolsAsync_ReturnsHeuristicWhenEmpty()
|
||||
{
|
||||
|
||||
@@ -4,12 +4,14 @@ using System.Text.Json;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Advisory.Tests;
|
||||
|
||||
public sealed class AdvisoryClientTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact(DisplayName = "GetCveSymbolsAsync uses Concelier response and caches results")]
|
||||
public async Task GetCveSymbolsAsync_UsesConcelierAndCaches()
|
||||
{
|
||||
@@ -57,6 +59,7 @@ public sealed class AdvisoryClientTests
|
||||
Assert.NotNull(mapping2);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact(DisplayName = "GetCveSymbolsAsync falls back to bundle store on HTTP failure")]
|
||||
public async Task GetCveSymbolsAsync_FallsBackToBundle()
|
||||
{
|
||||
@@ -71,6 +74,7 @@ public sealed class AdvisoryClientTests
|
||||
});
|
||||
|
||||
using var temp = new TempFile();
|
||||
using StellaOps.TestKit;
|
||||
var bundle = new
|
||||
{
|
||||
items = new[]
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Advisory.Tests;
|
||||
|
||||
public sealed class FileAdvisoryBundleStoreTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact(DisplayName = "FileAdvisoryBundleStore resolves CVE IDs case-insensitively")]
|
||||
public async Task TryGetAsync_ResolvesCaseInsensitive()
|
||||
{
|
||||
|
||||
@@ -2,11 +2,13 @@ using System.IO;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Node;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests;
|
||||
|
||||
public class Phase22SmokeTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Phase22_Fixture_Matches_Golden()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
@@ -5,11 +5,13 @@ using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Node.Internal.Phase22;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Node.Tests;
|
||||
|
||||
public class NodePhase22SampleLoaderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TryLoadAsync_ReadsComponentsFromNdjson()
|
||||
{
|
||||
var root = Path.Combine("Fixtures");
|
||||
|
||||
@@ -4,6 +4,7 @@ using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Ruby;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Ruby.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -18,7 +19,8 @@ public sealed class RubyBenchmarks
|
||||
private const int BenchmarkIterations = 10;
|
||||
private const int MaxAnalysisTimeMs = 1000;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SimpleApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
|
||||
@@ -47,7 +49,8 @@ public sealed class RubyBenchmarks
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Simple app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComplexApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "complex-app");
|
||||
@@ -76,7 +79,8 @@ public sealed class RubyBenchmarks
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Complex app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RailsApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "rails-app");
|
||||
@@ -105,7 +109,8 @@ public sealed class RubyBenchmarks
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Rails app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SinatraApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "sinatra-app");
|
||||
@@ -134,7 +139,8 @@ public sealed class RubyBenchmarks
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Sinatra app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ContainerApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "container-app");
|
||||
@@ -163,7 +169,8 @@ public sealed class RubyBenchmarks
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Container app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LegacyApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "legacy-app");
|
||||
@@ -192,7 +199,8 @@ public sealed class RubyBenchmarks
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Legacy app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CliApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "cli-app");
|
||||
@@ -221,7 +229,8 @@ public sealed class RubyBenchmarks
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"CLI app analysis should complete in <{MaxAnalysisTimeMs}ms including policy scanning (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MultipleRuns_ProduceDeterministicResultsAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace StellaOps.Scanner.Analyzers.Lang.Ruby.Tests;
|
||||
|
||||
public sealed class RubyLanguageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SimpleWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
|
||||
@@ -23,7 +24,8 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzerEmitsObservationPayloadWithSummaryAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
|
||||
@@ -73,7 +75,8 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
Assert.Equal("2.4.22", root.GetProperty("bundledWith").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComplexWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "complex-app");
|
||||
@@ -87,7 +90,8 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CliWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "cli-app");
|
||||
@@ -101,7 +105,8 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RailsWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "rails-app");
|
||||
@@ -115,7 +120,8 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SinatraWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "sinatra-app");
|
||||
@@ -129,7 +135,8 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ContainerWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "container-app");
|
||||
@@ -143,7 +150,8 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ContainerWorkspaceDetectsRubyVersionAndNativeExtensionsAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "container-app");
|
||||
@@ -173,6 +181,7 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
|
||||
Assert.True(store.TryGet(ScanAnalysisKeys.RubyObservationPayload, out AnalyzerObservationPayload payload));
|
||||
using var document = JsonDocument.Parse(payload.Content.ToArray());
|
||||
using StellaOps.TestKit;
|
||||
var root = document.RootElement;
|
||||
var environment = root.GetProperty("environment");
|
||||
|
||||
@@ -185,7 +194,8 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
Assert.True(nativeExtensions.GetArrayLength() >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LegacyWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "legacy-app");
|
||||
@@ -199,7 +209,8 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LegacyWorkspaceDetectsCapabilitiesWithoutBundlerAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "legacy-app");
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
public class ElfDynamicSectionParserTests : NativeTestBase
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesMinimalElfWithNoDynamicSection()
|
||||
{
|
||||
// Minimal ELF64 with no dependencies (static binary scenario)
|
||||
@@ -21,7 +22,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
|
||||
info.Runpath.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesElfWithDtNeeded()
|
||||
{
|
||||
// Build ELF with DT_NEEDED entries using the builder
|
||||
@@ -38,7 +40,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
|
||||
info.Dependencies[2].Soname.Should().Be("libpthread.so.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesElfWithRpathAndRunpath()
|
||||
{
|
||||
// Build ELF with rpath and runpath using the builder
|
||||
@@ -53,7 +56,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
|
||||
info.Runpath.Should().BeEquivalentTo(["$ORIGIN/../lib"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesElfWithInterpreterAndBuildId()
|
||||
{
|
||||
// Build ELF with interpreter and build ID using the builder
|
||||
@@ -67,7 +71,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
|
||||
info.BinaryId.Should().Be("deadbeef0102030405060708090a0b0c");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeduplicatesDtNeededEntries()
|
||||
{
|
||||
// ElfBuilder deduplicates internally, so add "duplicates" via builder
|
||||
@@ -85,7 +90,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
|
||||
info.Dependencies[0].Soname.Should().Be("libc.so.6");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ReturnsFalseForNonElfData()
|
||||
{
|
||||
var buffer = new byte[] { 0x00, 0x01, 0x02, 0x03 };
|
||||
@@ -96,7 +102,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ReturnsFalseForPeFile()
|
||||
{
|
||||
var buffer = new byte[256];
|
||||
@@ -104,12 +111,14 @@ public class ElfDynamicSectionParserTests : NativeTestBase
|
||||
buffer[1] = (byte)'Z';
|
||||
|
||||
using var stream = new MemoryStream(buffer);
|
||||
using StellaOps.TestKit;
|
||||
var result = ElfDynamicSectionParser.TryParse(stream, out var info);
|
||||
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesElfWithVersionNeeds()
|
||||
{
|
||||
// Test that version needs (GLIBC_2.17, etc.) are properly extracted
|
||||
@@ -128,7 +137,8 @@ public class ElfDynamicSectionParserTests : NativeTestBase
|
||||
info.Dependencies[0].VersionNeeds.Should().Contain(v => v.Version == "GLIBC_2.28");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesElfWithWeakVersionNeeds()
|
||||
{
|
||||
// Test that weak version requirements (VER_FLG_WEAK) are properly detected
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
public class HeuristicScannerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Scan_DetectsElfSonamePattern()
|
||||
{
|
||||
// Arrange - binary containing soname strings
|
||||
@@ -26,7 +27,8 @@ public class HeuristicScannerTests
|
||||
result.Edges.Should().OnlyContain(e => e.ReasonCode == HeuristicReasonCodes.StringDlopen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Scan_DetectsWindowsDllPattern()
|
||||
{
|
||||
// Arrange
|
||||
@@ -46,7 +48,8 @@ public class HeuristicScannerTests
|
||||
result.Edges.Should().OnlyContain(e => e.ReasonCode == HeuristicReasonCodes.StringLoadLibrary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Scan_DetectsMachODylibPattern()
|
||||
{
|
||||
// Arrange
|
||||
@@ -66,7 +69,8 @@ public class HeuristicScannerTests
|
||||
result.Edges.Should().Contain(e => e.LibraryName == "@loader_path/libbaz.dylib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Scan_AssignsHighConfidenceToPathLikeStrings()
|
||||
{
|
||||
// Arrange
|
||||
@@ -86,7 +90,8 @@ public class HeuristicScannerTests
|
||||
simpleSoname.Confidence.Should().Be(HeuristicConfidence.Medium);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Scan_DetectsPluginConfigReferences()
|
||||
{
|
||||
// Arrange
|
||||
@@ -106,7 +111,8 @@ public class HeuristicScannerTests
|
||||
result.PluginConfigs.Should().Contain("modules.conf");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Scan_DetectsGoCgoImportDirective()
|
||||
{
|
||||
// Arrange - simulate Go binary with cgo import
|
||||
@@ -126,7 +132,8 @@ public class HeuristicScannerTests
|
||||
e.Confidence == HeuristicConfidence.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Scan_DetectsGoCgoStaticImport()
|
||||
{
|
||||
// Arrange
|
||||
@@ -145,7 +152,8 @@ public class HeuristicScannerTests
|
||||
e.ReasonCode == HeuristicReasonCodes.GoCgoImport);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Scan_DeduplicatesEdgesByLibraryName()
|
||||
{
|
||||
// Arrange - same library mentioned multiple times
|
||||
@@ -164,7 +172,8 @@ public class HeuristicScannerTests
|
||||
result.Edges.Should().ContainSingle(e => e.LibraryName == "libfoo.so");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Scan_IncludesFileOffsetInEdge()
|
||||
{
|
||||
// Arrange
|
||||
@@ -182,7 +191,8 @@ public class HeuristicScannerTests
|
||||
edge.FileOffset.Should().Be(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ScanForDynamicLoading_ReturnsOnlyLibraryEdges()
|
||||
{
|
||||
// Arrange
|
||||
@@ -200,7 +210,8 @@ public class HeuristicScannerTests
|
||||
e.ReasonCode == HeuristicReasonCodes.StringDlopen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ScanForPluginConfigs_ReturnsOnlyConfigReferences()
|
||||
{
|
||||
// Arrange
|
||||
@@ -219,7 +230,8 @@ public class HeuristicScannerTests
|
||||
configs.Should().Contain("plugin.json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Scan_EmptyStream_ReturnsEmptyResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -233,7 +245,8 @@ public class HeuristicScannerTests
|
||||
result.PluginConfigs.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Scan_NoValidStrings_ReturnsEmptyResult()
|
||||
{
|
||||
// Arrange - binary data with no printable strings
|
||||
@@ -247,7 +260,8 @@ public class HeuristicScannerTests
|
||||
result.Edges.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("libfoo.so.1", true)]
|
||||
[InlineData("libbar.so", true)]
|
||||
[InlineData("lib-baz_qux.so.2.3", true)]
|
||||
@@ -261,6 +275,7 @@ public class HeuristicScannerTests
|
||||
|
||||
// Act
|
||||
using var stream = new MemoryStream(data);
|
||||
using StellaOps.TestKit;
|
||||
var result = HeuristicScanner.Scan(stream, NativeFormat.Elf);
|
||||
|
||||
// Assert
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
public class MachOLoadCommandParserTests : NativeTestBase
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesMinimalMachO64LittleEndian()
|
||||
{
|
||||
// Build minimal Mach-O 64-bit little-endian using builder
|
||||
@@ -20,7 +21,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
|
||||
info.Slices[0].CpuType.Should().Be("x86_64");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesMinimalMachO64BigEndian()
|
||||
{
|
||||
// Build minimal Mach-O 64-bit big-endian using builder
|
||||
@@ -37,7 +39,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
|
||||
info.Slices[0].CpuType.Should().Be("x86_64");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesMachOWithDylibs()
|
||||
{
|
||||
// Build Mach-O with dylib dependencies using builder
|
||||
@@ -55,7 +58,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
|
||||
info.Slices[0].Dependencies[1].Path.Should().Be("/usr/lib/libc++.1.dylib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesMachOWithRpath()
|
||||
{
|
||||
// Build Mach-O with rpaths using builder
|
||||
@@ -71,7 +75,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
|
||||
info.Slices[0].Rpaths[1].Should().Be("@loader_path/../lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesMachOWithUuid()
|
||||
{
|
||||
// Build Mach-O with UUID using builder
|
||||
@@ -86,7 +91,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
|
||||
info.Slices[0].Uuid.Should().MatchRegex(@"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesFatBinary()
|
||||
{
|
||||
// Build universal (fat) binary using builder
|
||||
@@ -100,7 +106,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
|
||||
info.Slices[1].CpuType.Should().Be("arm64");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesWeakAndReexportDylibs()
|
||||
{
|
||||
// Build Mach-O with weak and reexport dylibs using builder
|
||||
@@ -115,7 +122,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
|
||||
info.Slices[0].Dependencies.Should().Contain(d => d.ReasonCode == "macho-reexport");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeduplicatesDylibs()
|
||||
{
|
||||
// Build Mach-O with duplicate dylibs - builder or parser should deduplicate
|
||||
@@ -129,7 +137,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
|
||||
info.Slices[0].Dependencies.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ReturnsFalseForNonMachO()
|
||||
{
|
||||
var buffer = new byte[] { (byte)'M', (byte)'Z', 0x00, 0x00 };
|
||||
@@ -140,7 +149,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ReturnsFalseForElf()
|
||||
{
|
||||
var buffer = new byte[] { 0x7F, (byte)'E', (byte)'L', (byte)'F' };
|
||||
@@ -151,7 +161,8 @@ public class MachOLoadCommandParserTests : NativeTestBase
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesVersionNumbers()
|
||||
{
|
||||
// Build Mach-O with versioned dylib using builder
|
||||
@@ -159,6 +170,7 @@ public class MachOLoadCommandParserTests : NativeTestBase
|
||||
.AddDylib("/usr/lib/libfoo.dylib", "1.2.3", "1.0.0")
|
||||
.Build();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var info = ParseMachO(macho);
|
||||
|
||||
info.Slices[0].Dependencies[0].CurrentVersion.Should().Be("1.2.3");
|
||||
|
||||
@@ -391,7 +391,8 @@ public sealed class MachOReaderTests
|
||||
|
||||
#region Magic Detection Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Returns_Null_For_Empty_Stream()
|
||||
{
|
||||
using var stream = new MemoryStream([]);
|
||||
@@ -399,7 +400,8 @@ public sealed class MachOReaderTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Returns_Null_For_Invalid_Magic()
|
||||
{
|
||||
var data = new byte[] { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 };
|
||||
@@ -408,7 +410,8 @@ public sealed class MachOReaderTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Detects_64Bit_LittleEndian_MachO()
|
||||
{
|
||||
var data = BuildMachO64();
|
||||
@@ -421,7 +424,8 @@ public sealed class MachOReaderTests
|
||||
Assert.False(result.Identities[0].IsFatBinary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Detects_32Bit_MachO()
|
||||
{
|
||||
var data = BuildMachO32(cpuType: 7); // x86
|
||||
@@ -437,7 +441,8 @@ public sealed class MachOReaderTests
|
||||
|
||||
#region LC_UUID Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Extracts_LC_UUID()
|
||||
{
|
||||
var uuid = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10 };
|
||||
@@ -450,7 +455,8 @@ public sealed class MachOReaderTests
|
||||
Assert.Equal("0123456789abcdeffedcba9876543210", result.Identities[0].Uuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Returns_Null_Uuid_When_Not_Present()
|
||||
{
|
||||
var data = BuildMachO64(uuid: null);
|
||||
@@ -462,7 +468,8 @@ public sealed class MachOReaderTests
|
||||
Assert.Null(result.Identities[0].Uuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_UUID_Is_Lowercase_Hex_No_Dashes()
|
||||
{
|
||||
var uuid = new byte[] { 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x12, 0x34, 0x56, 0x78, 0x9A };
|
||||
@@ -482,7 +489,8 @@ public sealed class MachOReaderTests
|
||||
|
||||
#region Export Trie Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Extracts_Exports_From_LC_DYLD_INFO_ONLY()
|
||||
{
|
||||
var data = BuildMachO64(exports: new[] { "_main", "_printf" }, exportsViaDyldInfoOnly: true);
|
||||
@@ -494,7 +502,8 @@ public sealed class MachOReaderTests
|
||||
Assert.Equal(new[] { "_main", "_printf" }, result.Identities[0].Exports);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Extracts_Exports_From_LC_DYLD_EXPORTS_TRIE()
|
||||
{
|
||||
var data = BuildMachO64(exports: new[] { "_zeta", "_alpha" }, exportsViaDyldInfoOnly: false);
|
||||
@@ -510,7 +519,8 @@ public sealed class MachOReaderTests
|
||||
|
||||
#region Platform Detection Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(MachOPlatform.MacOS)]
|
||||
[InlineData(MachOPlatform.iOS)]
|
||||
[InlineData(MachOPlatform.TvOS)]
|
||||
@@ -528,7 +538,8 @@ public sealed class MachOReaderTests
|
||||
Assert.Equal(expectedPlatform, result.Identities[0].Platform);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Extracts_MinOs_Version()
|
||||
{
|
||||
var data = BuildMachO64(minOs: 0x000E0500); // 14.5.0
|
||||
@@ -539,7 +550,8 @@ public sealed class MachOReaderTests
|
||||
Assert.Equal("14.5", result.Identities[0].MinOsVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Extracts_SDK_Version()
|
||||
{
|
||||
var data = BuildMachO64(sdk: 0x000F0000); // 15.0.0
|
||||
@@ -550,7 +562,8 @@ public sealed class MachOReaderTests
|
||||
Assert.Equal("15.0", result.Identities[0].SdkVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Version_With_Patch()
|
||||
{
|
||||
var data = BuildMachO64(minOs: 0x000E0501); // 14.5.1
|
||||
@@ -565,7 +578,8 @@ public sealed class MachOReaderTests
|
||||
|
||||
#region Code Signature Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_UnsignedBinary_HasNull_CodeSignature()
|
||||
{
|
||||
var data = BuildMachO64();
|
||||
@@ -577,7 +591,8 @@ public sealed class MachOReaderTests
|
||||
Assert.Null(result.Identities[0].CodeSignature);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_SignedBinary_Extracts_SigningId_TeamId_CdHash_Entitlements_And_HardenedRuntime()
|
||||
{
|
||||
var signingId = "com.stellaops.demo";
|
||||
@@ -615,7 +630,8 @@ public sealed class MachOReaderTests
|
||||
|
||||
#region CPU Type Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(0x00000007, "i386")] // CPU_TYPE_X86
|
||||
[InlineData(0x01000007, "x86_64")] // CPU_TYPE_X86_64
|
||||
[InlineData(0x0000000C, "arm")] // CPU_TYPE_ARM
|
||||
@@ -634,7 +650,8 @@ public sealed class MachOReaderTests
|
||||
|
||||
#region Fat Binary Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Handles_Fat_Binary()
|
||||
{
|
||||
var arm64Slice = BuildMachO64(cpuType: 0x0100000C, uuid: new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 });
|
||||
@@ -655,7 +672,8 @@ public sealed class MachOReaderTests
|
||||
Assert.NotEqual(result.Identities[0].Uuid, result.Identities[1].Uuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseFatBinary_Returns_Multiple_Identities()
|
||||
{
|
||||
var arm64Slice = BuildMachO64(cpuType: 0x0100000C);
|
||||
@@ -672,7 +690,8 @@ public sealed class MachOReaderTests
|
||||
|
||||
#region TryExtractIdentity Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_Returns_True_For_Valid_MachO()
|
||||
{
|
||||
var data = BuildMachO64();
|
||||
@@ -685,7 +704,8 @@ public sealed class MachOReaderTests
|
||||
Assert.Equal("arm64", identity.CpuType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_Returns_False_For_Invalid_Data()
|
||||
{
|
||||
var data = new byte[] { 0x00, 0x00, 0x00, 0x00 };
|
||||
@@ -697,7 +717,8 @@ public sealed class MachOReaderTests
|
||||
Assert.Null(identity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_Returns_First_Slice_For_Fat_Binary()
|
||||
{
|
||||
var arm64Slice = BuildMachO64(cpuType: 0x0100000C);
|
||||
@@ -718,11 +739,13 @@ public sealed class MachOReaderTests
|
||||
|
||||
#region Path and LayerDigest Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Preserves_Path_And_LayerDigest()
|
||||
{
|
||||
var data = BuildMachO64();
|
||||
using var stream = new MemoryStream(data);
|
||||
using StellaOps.TestKit;
|
||||
var result = MachOReader.Parse(stream, "/usr/bin/myapp", "sha256:abc123");
|
||||
|
||||
Assert.NotNull(result);
|
||||
|
||||
@@ -3,6 +3,7 @@ using StellaOps.Scanner.Analyzers.Native;
|
||||
using StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
|
||||
using StellaOps.Scanner.Analyzers.Native.Tests.TestUtilities;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -13,7 +14,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
{
|
||||
#region ELF Parameterized Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(true, false)] // 64-bit, little-endian
|
||||
[InlineData(true, true)] // 64-bit, big-endian
|
||||
public void ElfBuilder_ParsesDependencies_AllFormats(bool is64Bit, bool isBigEndian)
|
||||
@@ -34,7 +36,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
info.Dependencies[1].Soname.Should().Be("libm.so.6");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("GLIBC_2.17", false)]
|
||||
[InlineData("GLIBC_2.28", false)]
|
||||
[InlineData("GLIBC_2.34", true)]
|
||||
@@ -58,7 +61,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
dep.VersionNeeds[0].IsWeak.Should().Be(isWeak);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ElfBuilder_LinuxX64Factory_CreatesValidElf()
|
||||
{
|
||||
// Arrange
|
||||
@@ -82,7 +86,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
|
||||
#region PE Parameterized Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(false)] // PE32 with 4-byte thunks
|
||||
[InlineData(true)] // PE32+ with 8-byte thunks
|
||||
public void PeBuilder_ParsesImports_CorrectBitness(bool is64Bit)
|
||||
@@ -104,7 +109,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
info.Dependencies[0].ImportedFunctions.Should().Contain("LoadLibraryA");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(PeSubsystem.WindowsConsole)]
|
||||
[InlineData(PeSubsystem.WindowsGui)]
|
||||
public void PeBuilder_SetsSubsystem_Correctly(PeSubsystem subsystem)
|
||||
@@ -121,7 +127,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
info.Subsystem.Should().Be(subsystem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PeBuilder_Console64Factory_CreatesValidPe()
|
||||
{
|
||||
// Arrange
|
||||
@@ -140,7 +147,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
info.DelayLoadDependencies.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PeBuilder_WithManifest_CreatesValidPe()
|
||||
{
|
||||
// Arrange
|
||||
@@ -160,7 +168,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
|
||||
#region Mach-O Parameterized Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(MachODylibKind.Load, "macho-loadlib")]
|
||||
[InlineData(MachODylibKind.Weak, "macho-weaklib")]
|
||||
[InlineData(MachODylibKind.Reexport, "macho-reexport")]
|
||||
@@ -181,7 +190,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
info.Slices[0].Dependencies[0].ReasonCode.Should().Be(expectedReason);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(MachOCpuType.X86_64, "x86_64")]
|
||||
[InlineData(MachOCpuType.Arm64, "arm64")]
|
||||
public void MachOBuilder_SetsCpuType_Correctly(MachOCpuType cpuType, string expectedName)
|
||||
@@ -201,7 +211,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
info.Slices[0].CpuType.Should().Be(expectedName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MachOBuilder_MacOSArm64Factory_CreatesValidMachO()
|
||||
{
|
||||
// Arrange
|
||||
@@ -225,7 +236,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
info.Slices[0].Uuid.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MachOBuilder_Universal_CreatesFatBinary()
|
||||
{
|
||||
// Arrange
|
||||
@@ -241,7 +253,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
info.Slices.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MachOBuilder_WithVersion_ParsesVersionNumbers()
|
||||
{
|
||||
// Arrange
|
||||
@@ -261,7 +274,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
|
||||
#region Cross-Format Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AllBuilders_ProduceParseable_Binaries()
|
||||
{
|
||||
// Arrange
|
||||
@@ -275,7 +289,8 @@ public class NativeBuilderParameterizedTests : NativeTestBase
|
||||
TryParseMachO(macho, out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AllBuilders_RejectWrongFormat()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
public class NativeFormatDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectsElf64LittleEndian()
|
||||
{
|
||||
var bytes = new byte[64];
|
||||
@@ -28,7 +29,8 @@ public class NativeFormatDetectorTests
|
||||
Assert.Equal("le", id.Endianness);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectsElfInterpreterAndBuildId()
|
||||
{
|
||||
// Minimal ELF64 with two program headers: PT_INTERP and PT_NOTE (GNU build-id)
|
||||
@@ -93,7 +95,8 @@ public class NativeFormatDetectorTests
|
||||
Assert.Equal("gnu-build-id:0102030405060708090a0b0c0d0e0f10", id.BuildId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectsPe()
|
||||
{
|
||||
var bytes = new byte[256];
|
||||
@@ -116,7 +119,8 @@ public class NativeFormatDetectorTests
|
||||
Assert.Equal("le", id.Endianness);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectsMachO64()
|
||||
{
|
||||
var bytes = new byte[32];
|
||||
@@ -134,7 +138,8 @@ public class NativeFormatDetectorTests
|
||||
Assert.Equal("darwin", id.OperatingSystem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractsMachOUuid()
|
||||
{
|
||||
var buffer = new byte[128];
|
||||
@@ -161,12 +166,14 @@ public class NativeFormatDetectorTests
|
||||
Assert.Equal($"macho-uuid:{Convert.ToHexString(uuid.ToByteArray()).ToLowerInvariant()}", id.Uuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ReturnsUnknownForUnsupported()
|
||||
{
|
||||
var bytes = new byte[] { 0x00, 0x01, 0x02, 0x03 };
|
||||
using var stream = new MemoryStream(bytes);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var detected = NativeFormatDetector.TryDetect(stream, out var id);
|
||||
|
||||
Assert.False(detected);
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
public class NativeObservationSerializerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
@@ -22,7 +23,8 @@ public class NativeObservationSerializerTests
|
||||
parsed.RootElement.GetProperty("$schema").GetString().Should().Be("stellaops.native.observation@1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_OmitsNullProperties()
|
||||
{
|
||||
// Arrange
|
||||
@@ -36,7 +38,8 @@ public class NativeObservationSerializerTests
|
||||
json.Should().NotContain("\"build_id\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SerializePretty_ProducesFormattedJson()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +53,8 @@ public class NativeObservationSerializerTests
|
||||
json.Should().Contain(" ");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Deserialize_RestoresDocument()
|
||||
{
|
||||
// Arrange
|
||||
@@ -68,7 +72,8 @@ public class NativeObservationSerializerTests
|
||||
restored.HeuristicEdges.Should().HaveCount(original.HeuristicEdges.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeSha256_ProducesConsistentHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -84,7 +89,8 @@ public class NativeObservationSerializerTests
|
||||
hash1.Should().MatchRegex("^[a-f0-9]+$");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SerializeToBytes_ProducesUtf8()
|
||||
{
|
||||
// Arrange
|
||||
@@ -99,7 +105,8 @@ public class NativeObservationSerializerTests
|
||||
json.Should().StartWith("{");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_WritesToStream()
|
||||
{
|
||||
// Arrange
|
||||
@@ -116,7 +123,8 @@ public class NativeObservationSerializerTests
|
||||
json.Should().Contain("stellaops.native.observation@1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReadAsync_ReadsFromStream()
|
||||
{
|
||||
// Arrange
|
||||
@@ -124,6 +132,7 @@ public class NativeObservationSerializerTests
|
||||
var json = NativeObservationSerializer.Serialize(original);
|
||||
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json));
|
||||
|
||||
using StellaOps.TestKit;
|
||||
// Act
|
||||
var doc = await NativeObservationSerializer.ReadAsync(stream);
|
||||
|
||||
@@ -132,7 +141,8 @@ public class NativeObservationSerializerTests
|
||||
doc!.Binary.Path.Should().Be(original.Binary.Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Deserialize_EmptyString_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -230,7 +240,8 @@ public class NativeObservationSerializerTests
|
||||
|
||||
public class NativeObservationBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithBinary_CreatesDocument()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -243,7 +254,8 @@ public class NativeObservationBuilderTests
|
||||
doc.Binary.Format.Should().Be("elf");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithoutBinary_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -255,7 +267,8 @@ public class NativeObservationBuilderTests
|
||||
.WithMessage("*Binary*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddEntrypoint_AddsToList()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -272,7 +285,8 @@ public class NativeObservationBuilderTests
|
||||
doc.Entrypoints[0].Conditions.Should().Contain("linux");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddElfDependencies_AddsEdgesAndEnvironment()
|
||||
{
|
||||
// Arrange
|
||||
@@ -302,7 +316,8 @@ public class NativeObservationBuilderTests
|
||||
doc.Environment.Runpath.Should().Contain("/app/lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddPeDependencies_AddsEdgesAndSxs()
|
||||
{
|
||||
// Arrange
|
||||
@@ -339,7 +354,8 @@ public class NativeObservationBuilderTests
|
||||
doc.Environment.SxsDependencies![0].Name.Should().Be("Microsoft.VC90.CRT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddMachODependencies_AddsEdgesAndRpaths()
|
||||
{
|
||||
// Arrange
|
||||
@@ -372,7 +388,8 @@ public class NativeObservationBuilderTests
|
||||
doc.Environment.MachORpaths.Should().Contain("@loader_path/../Frameworks");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddHeuristicResults_AddsEdgesAndPluginConfigs()
|
||||
{
|
||||
// Arrange
|
||||
@@ -396,7 +413,8 @@ public class NativeObservationBuilderTests
|
||||
doc.Environment.PluginConfigs.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddResolution_AddsExplainTrace()
|
||||
{
|
||||
// Arrange
|
||||
@@ -424,7 +442,8 @@ public class NativeObservationBuilderTests
|
||||
doc.Resolution[0].Steps[1].Found.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FullIntegration_BuildsCompleteDocument()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
public class ElfResolverTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithRpath_FindsLibraryInRpathDirectory()
|
||||
{
|
||||
// Arrange
|
||||
@@ -26,7 +28,8 @@ public class ElfResolverTests
|
||||
s.Found == true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithRunpath_IgnoresRpath()
|
||||
{
|
||||
// Arrange - library exists in rpath but not runpath
|
||||
@@ -47,7 +50,8 @@ public class ElfResolverTests
|
||||
result.Steps.Should().Contain(s => s.SearchReason == "runpath");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithLdLibraryPath_SearchesBeforeRunpath()
|
||||
{
|
||||
// Arrange
|
||||
@@ -68,7 +72,8 @@ public class ElfResolverTests
|
||||
result.Steps.First().SearchReason.Should().Be("ld_library_path");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithOriginExpansion_ExpandsOriginVariable()
|
||||
{
|
||||
// Arrange
|
||||
@@ -84,7 +89,8 @@ public class ElfResolverTests
|
||||
result.ResolvedPath.Should().Be("/app/bin/../lib/libfoo.so.1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithOriginBraceSyntax_ExpandsOriginVariable()
|
||||
{
|
||||
// Arrange
|
||||
@@ -100,7 +106,8 @@ public class ElfResolverTests
|
||||
result.ResolvedPath.Should().Be("/app/bin/../lib/libbar.so.2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_NotFound_ReturnsUnresolvedWithSteps()
|
||||
{
|
||||
// Arrange
|
||||
@@ -119,7 +126,8 @@ public class ElfResolverTests
|
||||
result.Steps.Should().Contain(s => s.SearchReason == "default" && !s.Found);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithDefaultPaths_SearchesSystemDirectories()
|
||||
{
|
||||
// Arrange
|
||||
@@ -134,7 +142,8 @@ public class ElfResolverTests
|
||||
result.Steps.Should().Contain(s => s.SearchReason == "default");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_SearchOrder_FollowsCorrectPriority()
|
||||
{
|
||||
// Arrange - library exists in all locations
|
||||
@@ -158,7 +167,8 @@ public class ElfResolverTests
|
||||
|
||||
public class PeResolverTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_InApplicationDirectory_FindsDll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -174,7 +184,8 @@ public class PeResolverTests
|
||||
.Which.SearchReason.Should().Be("application_directory");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_InSystem32_FindsDll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -189,7 +200,8 @@ public class PeResolverTests
|
||||
result.Steps.Should().Contain(s => s.SearchReason == "system_directory");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_InSysWOW64_FindsDll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -203,7 +215,8 @@ public class PeResolverTests
|
||||
result.ResolvedPath.Should().Be("C:/Windows/SysWOW64/wow64.dll");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_InCurrentDirectory_FindsDll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -218,7 +231,8 @@ public class PeResolverTests
|
||||
result.Steps.Should().Contain(s => s.SearchReason == "current_directory" && s.Found);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_InPathEnvironment_FindsDll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -234,7 +248,8 @@ public class PeResolverTests
|
||||
result.Steps.Should().Contain(s => s.SearchReason == "path_environment" && s.Found);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_SafeDllSearchOrder_ApplicationBeforeSystem()
|
||||
{
|
||||
// Arrange - DLL exists in both app dir and system32
|
||||
@@ -252,7 +267,8 @@ public class PeResolverTests
|
||||
result.Steps.First().SearchReason.Should().Be("application_directory");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_NotFound_ReturnsAllSearchedPaths()
|
||||
{
|
||||
// Arrange
|
||||
@@ -272,7 +288,8 @@ public class PeResolverTests
|
||||
result.Steps.Should().Contain(s => s.SearchReason == "path_environment");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithNullApplicationDirectory_SkipsAppDirSearch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -289,7 +306,8 @@ public class PeResolverTests
|
||||
|
||||
public class MachOResolverTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithRpath_ExpandsAndFindsLibrary()
|
||||
{
|
||||
// Arrange
|
||||
@@ -306,7 +324,8 @@ public class MachOResolverTests
|
||||
.Which.SearchReason.Should().Be("rpath");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithMultipleRpaths_SearchesInOrder()
|
||||
{
|
||||
// Arrange
|
||||
@@ -324,7 +343,8 @@ public class MachOResolverTests
|
||||
result.Steps[1].Found.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithLoaderPath_ExpandsPlaceholder()
|
||||
{
|
||||
// Arrange
|
||||
@@ -345,7 +365,8 @@ public class MachOResolverTests
|
||||
.Which.SearchReason.Should().Be("loader_path");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithExecutablePath_ExpandsPlaceholder()
|
||||
{
|
||||
// Arrange
|
||||
@@ -365,7 +386,8 @@ public class MachOResolverTests
|
||||
.Which.SearchReason.Should().Be("executable_path");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_WithRpathContainingLoaderPath_ExpandsBoth()
|
||||
{
|
||||
// Arrange
|
||||
@@ -380,7 +402,8 @@ public class MachOResolverTests
|
||||
result.ResolvedPath.Should().Be("/app/bin/../lib/libfoo.dylib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_AbsolutePath_ChecksDirectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -396,7 +419,8 @@ public class MachOResolverTests
|
||||
.Which.SearchReason.Should().Be("absolute_path");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_RelativePath_SearchesDefaultPaths()
|
||||
{
|
||||
// Arrange
|
||||
@@ -411,7 +435,8 @@ public class MachOResolverTests
|
||||
result.Steps.Should().Contain(s => s.SearchReason == "default_library_path");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_RpathNotFound_FallsBackToDefaultPaths()
|
||||
{
|
||||
// Arrange - library not in rpath but in default path
|
||||
@@ -428,7 +453,8 @@ public class MachOResolverTests
|
||||
result.Steps.Should().Contain(s => s.SearchReason == "default_library_path" && s.Found);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_NotFound_ReturnsAllSearchedPaths()
|
||||
{
|
||||
// Arrange
|
||||
@@ -445,7 +471,8 @@ public class MachOResolverTests
|
||||
result.Steps.Should().OnlyContain(s => !s.Found);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_LoaderPathNotFound_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -463,7 +490,8 @@ public class MachOResolverTests
|
||||
|
||||
public class VirtualFileSystemTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FileExists_WithExistingFile_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -473,7 +501,8 @@ public class VirtualFileSystemTests
|
||||
fs.FileExists("/usr/lib/libc.so.6").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FileExists_WithNonExistingFile_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -483,7 +512,8 @@ public class VirtualFileSystemTests
|
||||
fs.FileExists("/usr/lib/missing.so").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FileExists_IsCaseInsensitive()
|
||||
{
|
||||
// Arrange
|
||||
@@ -493,7 +523,8 @@ public class VirtualFileSystemTests
|
||||
fs.FileExists("/usr/lib/LIBC.SO.6").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DirectoryExists_WithExistingDirectory_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -504,7 +535,8 @@ public class VirtualFileSystemTests
|
||||
fs.DirectoryExists("/usr/lib/x86_64-linux-gnu").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizePath_HandlesBackslashes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -514,7 +546,8 @@ public class VirtualFileSystemTests
|
||||
fs.FileExists("C:\\Windows\\System32\\kernel32.dll").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnumerateFiles_ReturnsFilesInDirectory()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
public class PeImportParserTests : NativeTestBase
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesMinimalPe32()
|
||||
{
|
||||
// Build minimal PE32 using builder
|
||||
@@ -23,7 +24,8 @@ public class PeImportParserTests : NativeTestBase
|
||||
info.Subsystem.Should().Be(PeSubsystem.WindowsConsole);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesMinimalPe32Plus()
|
||||
{
|
||||
// Build minimal PE32+ using builder
|
||||
@@ -35,7 +37,8 @@ public class PeImportParserTests : NativeTestBase
|
||||
info.Machine.Should().Be("x86_64");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesPeWithImports()
|
||||
{
|
||||
// Build PE with imports using builder
|
||||
@@ -52,7 +55,8 @@ public class PeImportParserTests : NativeTestBase
|
||||
info.Dependencies[1].DllName.Should().Be("user32.dll");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeduplicatesImports()
|
||||
{
|
||||
// Build PE with duplicate imports - builder or parser should deduplicate
|
||||
@@ -67,7 +71,8 @@ public class PeImportParserTests : NativeTestBase
|
||||
info.Dependencies[0].DllName.Should().Be("kernel32.dll");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesDelayLoadImports()
|
||||
{
|
||||
// Build PE with delay imports using builder
|
||||
@@ -82,7 +87,8 @@ public class PeImportParserTests : NativeTestBase
|
||||
info.DelayLoadDependencies[0].ReasonCode.Should().Be("pe-delayimport");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesSubsystem()
|
||||
{
|
||||
// Build PE with GUI subsystem using builder
|
||||
@@ -95,7 +101,8 @@ public class PeImportParserTests : NativeTestBase
|
||||
info.Subsystem.Should().Be(PeSubsystem.WindowsGui);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ReturnsFalseForNonPe()
|
||||
{
|
||||
var buffer = new byte[] { 0x7F, (byte)'E', (byte)'L', (byte)'F' };
|
||||
@@ -106,7 +113,8 @@ public class PeImportParserTests : NativeTestBase
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ReturnsFalseForTruncatedPe()
|
||||
{
|
||||
var buffer = new byte[] { (byte)'M', (byte)'Z' };
|
||||
@@ -117,7 +125,8 @@ public class PeImportParserTests : NativeTestBase
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesEmbeddedManifest()
|
||||
{
|
||||
// Build PE with SxS dependency manifest using builder
|
||||
@@ -126,13 +135,15 @@ public class PeImportParserTests : NativeTestBase
|
||||
"6595b64144ccf1df", "*")
|
||||
.Build();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var info = ParsePe(pe);
|
||||
|
||||
info.SxsDependencies.Should().HaveCountGreaterOrEqualTo(1);
|
||||
info.SxsDependencies[0].Name.Should().Be("Microsoft.Windows.Common-Controls");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesPe32PlusWithImportThunks()
|
||||
{
|
||||
// Test that 64-bit PE files correctly parse 8-byte import thunks
|
||||
@@ -150,7 +161,8 @@ public class PeImportParserTests : NativeTestBase
|
||||
info.Dependencies[0].ImportedFunctions.Should().Contain("LoadLibraryA");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsesPeWithEmbeddedResourceManifest()
|
||||
{
|
||||
// Test that manifest is properly extracted from PE resources
|
||||
|
||||
@@ -12,7 +12,8 @@ public class PeReaderTests : NativeTestBase
|
||||
{
|
||||
#region Basic Parsing
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_InvalidData_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -26,7 +27,8 @@ public class PeReaderTests : NativeTestBase
|
||||
identity.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_TooShort_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -39,7 +41,8 @@ public class PeReaderTests : NativeTestBase
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_MissingMzSignature_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -54,7 +57,8 @@ public class PeReaderTests : NativeTestBase
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_ValidMinimalPe64_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -71,7 +75,8 @@ public class PeReaderTests : NativeTestBase
|
||||
identity.Subsystem.Should().Be(PeSubsystem.WindowsConsole);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_ValidMinimalPe32_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -90,7 +95,8 @@ public class PeReaderTests : NativeTestBase
|
||||
identity.Machine.Should().Be("x86");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_GuiSubsystem_ParsesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +118,8 @@ public class PeReaderTests : NativeTestBase
|
||||
|
||||
#region Parse Method
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ValidPeStream_ReturnsPeParseResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -128,13 +135,15 @@ public class PeReaderTests : NativeTestBase
|
||||
result.Identity.Is64Bit.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_InvalidStream_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var invalidData = new byte[] { 0x00, 0x01, 0x02, 0x03 };
|
||||
using var stream = new MemoryStream(invalidData);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
// Act
|
||||
var result = PeReader.Parse(stream, "invalid.exe");
|
||||
|
||||
@@ -142,7 +151,8 @@ public class PeReaderTests : NativeTestBase
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ThrowsOnNullStream()
|
||||
{
|
||||
// Act & Assert
|
||||
@@ -154,7 +164,8 @@ public class PeReaderTests : NativeTestBase
|
||||
|
||||
#region Machine Architecture
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(PeMachine.I386, "x86", false)]
|
||||
[InlineData(PeMachine.Amd64, "x86_64", true)]
|
||||
[InlineData(PeMachine.Arm64, "arm64", true)]
|
||||
@@ -179,7 +190,8 @@ public class PeReaderTests : NativeTestBase
|
||||
|
||||
#region Exports
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_NoExports_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange - standard console app has no exports
|
||||
@@ -198,7 +210,8 @@ public class PeReaderTests : NativeTestBase
|
||||
|
||||
#region Compiler Hints (Rich Header)
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_NoRichHeader_ReturnsEmptyHints()
|
||||
{
|
||||
// Arrange - builder-generated PEs don't have rich header
|
||||
@@ -214,7 +227,8 @@ public class PeReaderTests : NativeTestBase
|
||||
identity.RichHeaderHash.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_RichHeader_ExtractsCompilerHints()
|
||||
{
|
||||
// Arrange
|
||||
@@ -236,7 +250,8 @@ public class PeReaderTests : NativeTestBase
|
||||
|
||||
#region CodeView Debug Info
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_NoDebugDirectory_ReturnsNullCodeView()
|
||||
{
|
||||
// Arrange - builder-generated PEs don't have debug directory
|
||||
@@ -253,7 +268,8 @@ public class PeReaderTests : NativeTestBase
|
||||
identity.PdbPath.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_CodeViewDebugInfo_ExtractsGuidAgeAndPdbPath()
|
||||
{
|
||||
// Arrange
|
||||
@@ -274,7 +290,8 @@ public class PeReaderTests : NativeTestBase
|
||||
|
||||
#region Version Resources
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_NoVersionResource_ReturnsNullVersions()
|
||||
{
|
||||
// Arrange - builder-generated PEs don't have version resources
|
||||
@@ -293,7 +310,8 @@ public class PeReaderTests : NativeTestBase
|
||||
identity.OriginalFilename.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_VersionResource_ExtractsStrings()
|
||||
{
|
||||
// Arrange
|
||||
@@ -316,7 +334,8 @@ public class PeReaderTests : NativeTestBase
|
||||
|
||||
#region Golden Fixtures
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_Exports_ExtractsExportNames()
|
||||
{
|
||||
// Arrange
|
||||
@@ -331,7 +350,8 @@ public class PeReaderTests : NativeTestBase
|
||||
identity!.Exports.Should().ContainSingle().Which.Should().Be("mingw_export");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_MingwFixture_HasNoRichOrCodeView()
|
||||
{
|
||||
// Arrange
|
||||
@@ -354,7 +374,8 @@ public class PeReaderTests : NativeTestBase
|
||||
|
||||
#region Determinism
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_SameInput_ReturnsSameOutput()
|
||||
{
|
||||
// Arrange
|
||||
@@ -368,7 +389,8 @@ public class PeReaderTests : NativeTestBase
|
||||
identity1.Should().BeEquivalentTo(identity2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_DifferentInputs_ReturnsDifferentOutput()
|
||||
{
|
||||
// Arrange
|
||||
@@ -387,7 +409,8 @@ public class PeReaderTests : NativeTestBase
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_InvalidPeOffset_ReturnsFalse()
|
||||
{
|
||||
// Arrange - Create data with MZ signature but invalid PE offset
|
||||
@@ -407,7 +430,8 @@ public class PeReaderTests : NativeTestBase
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_MissingPeSignature_ReturnsFalse()
|
||||
{
|
||||
// Arrange - Create data with MZ but missing PE signature
|
||||
@@ -424,7 +448,8 @@ public class PeReaderTests : NativeTestBase
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryExtractIdentity_InvalidMagic_ReturnsFalse()
|
||||
{
|
||||
// Arrange - Create data with PE signature but invalid magic
|
||||
|
||||
@@ -16,7 +16,8 @@ public sealed class PluginPackagingTests
|
||||
{
|
||||
#region INativeAnalyzerPlugin Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NativeAnalyzerPlugin_Properties_AreConfigured()
|
||||
{
|
||||
var plugin = new NativeAnalyzerPlugin();
|
||||
@@ -26,7 +27,8 @@ public sealed class PluginPackagingTests
|
||||
plugin.Version.Should().Be("1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NativeAnalyzerPlugin_SupportedFormats_ContainsAllFormats()
|
||||
{
|
||||
var plugin = new NativeAnalyzerPlugin();
|
||||
@@ -36,7 +38,8 @@ public sealed class PluginPackagingTests
|
||||
plugin.SupportedFormats.Should().Contain(NativeFormat.MachO);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NativeAnalyzerPlugin_IsAvailable_ReturnsTrue()
|
||||
{
|
||||
var plugin = new NativeAnalyzerPlugin();
|
||||
@@ -47,7 +50,8 @@ public sealed class PluginPackagingTests
|
||||
available.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NativeAnalyzerPlugin_CreateAnalyzer_ReturnsAnalyzer()
|
||||
{
|
||||
var plugin = new NativeAnalyzerPlugin();
|
||||
@@ -65,7 +69,8 @@ public sealed class PluginPackagingTests
|
||||
|
||||
#region NativeAnalyzerPluginCatalog Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PluginCatalog_Constructor_RegistersBuiltInPlugin()
|
||||
{
|
||||
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
|
||||
@@ -75,7 +80,8 @@ public sealed class PluginPackagingTests
|
||||
catalog.Plugins[0].Name.Should().Be("Native Binary Analyzer");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PluginCatalog_Register_AddsPlugin()
|
||||
{
|
||||
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
|
||||
@@ -88,7 +94,8 @@ public sealed class PluginPackagingTests
|
||||
catalog.Plugins.Should().Contain(p => p.Name == "Test Plugin");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PluginCatalog_Register_IgnoresDuplicates()
|
||||
{
|
||||
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
|
||||
@@ -102,7 +109,8 @@ public sealed class PluginPackagingTests
|
||||
catalog.Plugins.Count(p => p.Name == "Test Plugin").Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PluginCatalog_Seal_PreventsModification()
|
||||
{
|
||||
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
|
||||
@@ -115,7 +123,8 @@ public sealed class PluginPackagingTests
|
||||
.WithMessage("*sealed*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PluginCatalog_LoadFromDirectory_DoesNotFailForMissingDirectory()
|
||||
{
|
||||
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
|
||||
@@ -126,7 +135,8 @@ public sealed class PluginPackagingTests
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PluginCatalog_CreateAnalyzers_CreatesFromAvailablePlugins()
|
||||
{
|
||||
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
|
||||
@@ -140,7 +150,8 @@ public sealed class PluginPackagingTests
|
||||
analyzers.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PluginCatalog_CreateAnalyzers_SkipsUnavailablePlugins()
|
||||
{
|
||||
var logger = NullLogger<NativeAnalyzerPluginCatalog>.Instance;
|
||||
@@ -162,7 +173,8 @@ public sealed class PluginPackagingTests
|
||||
|
||||
#region ServiceCollectionExtensions Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddNativeAnalyzer_RegistersServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -173,7 +185,8 @@ public sealed class PluginPackagingTests
|
||||
services.Should().Contain(s => s.ServiceType == typeof(INativeAnalyzer));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddNativeAnalyzer_WithOptions_ConfiguresOptions()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -191,7 +204,8 @@ public sealed class PluginPackagingTests
|
||||
options.Value.DefaultTimeout.Should().Be(TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NativeAnalyzerServiceOptions_DefaultValues()
|
||||
{
|
||||
var options = new NativeAnalyzerServiceOptions();
|
||||
@@ -202,7 +216,8 @@ public sealed class PluginPackagingTests
|
||||
options.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NativeAnalyzerServiceOptions_GetDefaultSearchPathsForFormat_ReturnsCorrectPaths()
|
||||
{
|
||||
var options = new NativeAnalyzerServiceOptions();
|
||||
@@ -218,7 +233,8 @@ public sealed class PluginPackagingTests
|
||||
unknownPaths.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddNativeRuntimeCapture_RegistersAdapter()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -233,7 +249,8 @@ public sealed class PluginPackagingTests
|
||||
|
||||
#region NativeAnalyzerOptions Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NativeAnalyzerOptions_DefaultValues()
|
||||
{
|
||||
var options = new NativeAnalyzerOptions();
|
||||
@@ -249,7 +266,8 @@ public sealed class PluginPackagingTests
|
||||
|
||||
#region INativeAnalyzer Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NativeAnalyzer_AnalyzeAsync_ThrowsForUnknownFormat()
|
||||
{
|
||||
var logger = NullLogger<NativeAnalyzer>.Instance;
|
||||
@@ -264,7 +282,8 @@ public sealed class PluginPackagingTests
|
||||
.WithMessage("*Unknown or unsupported*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NativeAnalyzer_AnalyzeBatchAsync_YieldsResults()
|
||||
{
|
||||
var logger = NullLogger<NativeAnalyzer>.Instance;
|
||||
@@ -294,7 +313,8 @@ public sealed class PluginPackagingTests
|
||||
results[0].Binary.Format.Should().Be("elf");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NativeAnalyzer_AnalyzeAsync_ParsesElfBinary()
|
||||
{
|
||||
var logger = NullLogger<NativeAnalyzer>.Instance;
|
||||
@@ -308,6 +328,7 @@ public sealed class PluginPackagingTests
|
||||
var elfHeader = CreateMinimalElfHeader();
|
||||
using var stream = new MemoryStream(elfHeader);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var result = await analyzer.AnalyzeAsync("/test/binary.so", stream, options);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
public class RuntimeCaptureOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_DefaultOptions_ReturnsNoErrors()
|
||||
{
|
||||
// Arrange
|
||||
@@ -19,7 +20,8 @@ public class RuntimeCaptureOptionsTests
|
||||
errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_InvalidBufferSize_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -32,7 +34,8 @@ public class RuntimeCaptureOptionsTests
|
||||
errors.Should().Contain(e => e.Contains("BufferSize"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_NegativeCaptureDuration_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -45,7 +48,8 @@ public class RuntimeCaptureOptionsTests
|
||||
errors.Should().Contain(e => e.Contains("MaxCaptureDuration"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ExcessiveCaptureDuration_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -58,7 +62,8 @@ public class RuntimeCaptureOptionsTests
|
||||
errors.Should().Contain(e => e.Contains("MaxCaptureDuration"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_SandboxWithoutRoot_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -79,7 +84,8 @@ public class RuntimeCaptureOptionsTests
|
||||
errors.Should().Contain(e => e.Contains("SandboxRoot"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_SandboxWithRoot_ReturnsNoSandboxErrors()
|
||||
{
|
||||
// Arrange
|
||||
@@ -102,7 +108,8 @@ public class RuntimeCaptureOptionsTests
|
||||
|
||||
public class RedactionOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ApplyRedaction_HomePath_IsRedacted()
|
||||
{
|
||||
// Arrange
|
||||
@@ -116,7 +123,8 @@ public class RedactionOptionsTests
|
||||
result.Should().Contain("[REDACTED]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ApplyRedaction_WindowsUserPath_IsRedacted()
|
||||
{
|
||||
// Arrange
|
||||
@@ -130,7 +138,8 @@ public class RedactionOptionsTests
|
||||
result.Should().Contain("[REDACTED]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ApplyRedaction_SystemPath_NotRedacted()
|
||||
{
|
||||
// Arrange
|
||||
@@ -144,7 +153,8 @@ public class RedactionOptionsTests
|
||||
result.Should().Be("/usr/lib/libc.so.6");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ApplyRedaction_DisabledRedaction_NotRedacted()
|
||||
{
|
||||
// Arrange
|
||||
@@ -158,7 +168,8 @@ public class RedactionOptionsTests
|
||||
result.Should().Be("/home/testuser/secret.so");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ApplyRedaction_SshPath_IsRedacted()
|
||||
{
|
||||
// Arrange
|
||||
@@ -172,7 +183,8 @@ public class RedactionOptionsTests
|
||||
result.Should().Contain("[REDACTED]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ApplyRedaction_KeyFile_IsRedacted()
|
||||
{
|
||||
// Arrange
|
||||
@@ -186,7 +198,8 @@ public class RedactionOptionsTests
|
||||
result.Should().Contain("[REDACTED]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_InvalidRegex_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -202,7 +215,8 @@ public class RedactionOptionsTests
|
||||
errors.Should().Contain(e => e.Contains("Invalid redaction regex"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_EmptyReplacement_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -222,7 +236,8 @@ public class RedactionOptionsTests
|
||||
|
||||
public class RuntimeEvidenceAggregatorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Aggregate_EmptySessions_ReturnsEmptyEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -237,7 +252,8 @@ public class RuntimeEvidenceAggregatorTests
|
||||
evidence.RuntimeEdges.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Aggregate_SingleSession_ReturnsCorrectSummary()
|
||||
{
|
||||
// Arrange
|
||||
@@ -293,7 +309,8 @@ public class RuntimeEvidenceAggregatorTests
|
||||
libfoo.CallerModules.Should().Contain("myapp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Aggregate_DuplicateLoads_AggregatesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -316,7 +333,8 @@ public class RuntimeEvidenceAggregatorTests
|
||||
evidence.UniqueLibraries[0].FirstSeen.Should().Be(baseTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Aggregate_FailedLoads_NotIncludedInSummary()
|
||||
{
|
||||
// Arrange
|
||||
@@ -335,7 +353,8 @@ public class RuntimeEvidenceAggregatorTests
|
||||
evidence.RuntimeEdges.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Aggregate_MultipleSessions_MergesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -363,7 +382,8 @@ public class RuntimeEvidenceAggregatorTests
|
||||
|
||||
public class RuntimeCaptureAdapterFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateForCurrentPlatform_ReturnsAdapter()
|
||||
{
|
||||
// Act
|
||||
@@ -381,7 +401,8 @@ public class RuntimeCaptureAdapterFactoryTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetAvailableAdapters_ReturnsAdaptersForCurrentPlatform()
|
||||
{
|
||||
// Act
|
||||
@@ -402,7 +423,8 @@ public class RuntimeCaptureAdapterFactoryTests
|
||||
|
||||
public class SandboxCaptureTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SandboxCapture_WithMockEvents_CapturesEvents()
|
||||
{
|
||||
// Arrange
|
||||
@@ -448,7 +470,8 @@ public class SandboxCaptureTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SandboxCapture_StateTransitions_AreCorrect()
|
||||
{
|
||||
// Arrange
|
||||
@@ -488,7 +511,8 @@ public class SandboxCaptureTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SandboxCapture_CannotStartWhileRunning()
|
||||
{
|
||||
// Arrange
|
||||
@@ -517,6 +541,7 @@ public class SandboxCaptureTests
|
||||
{
|
||||
await adapter.StartCaptureAsync(options);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
// Act & Assert
|
||||
var act = async () => await adapter.StartCaptureAsync(options);
|
||||
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||
@@ -528,7 +553,8 @@ public class SandboxCaptureTests
|
||||
|
||||
public class RuntimeEvidenceModelTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RuntimeLoadEvent_RecordEquality_Works()
|
||||
{
|
||||
// Arrange
|
||||
@@ -542,7 +568,8 @@ public class RuntimeEvidenceModelTests
|
||||
event1.Should().NotBe(event3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RuntimeLoadType_AllTypesHaveReasonCodes()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Homebrew;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.OS.Homebrew.Tests;
|
||||
|
||||
public sealed class HomebrewPackageAnalyzerTests
|
||||
@@ -29,13 +30,15 @@ public sealed class HomebrewPackageAnalyzerTests
|
||||
_logger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AnalyzerId_ReturnsHomebrew()
|
||||
{
|
||||
Assert.Equal("homebrew", _analyzer.AnalyzerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithValidCellar_ReturnsPackages()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +53,8 @@ public sealed class HomebrewPackageAnalyzerTests
|
||||
Assert.True(result.Packages.Count > 0, "Expected at least one package");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_FindsIntelCellarPackages()
|
||||
{
|
||||
// Arrange
|
||||
@@ -67,7 +71,8 @@ public sealed class HomebrewPackageAnalyzerTests
|
||||
Assert.Contains("pkg:brew/homebrew%2Fcore/openssl%403@3.1.0", openssl.PackageUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_FindsAppleSiliconCellarPackages()
|
||||
{
|
||||
// Arrange
|
||||
@@ -83,7 +88,8 @@ public sealed class HomebrewPackageAnalyzerTests
|
||||
Assert.Equal("arm64", jq.Architecture);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_PackageWithRevision_IncludesRevisionInPurl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -99,7 +105,8 @@ public sealed class HomebrewPackageAnalyzerTests
|
||||
Assert.Equal("1", wget.Release);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsDependencies()
|
||||
{
|
||||
// Arrange
|
||||
@@ -115,7 +122,8 @@ public sealed class HomebrewPackageAnalyzerTests
|
||||
Assert.Contains("gettext", wget.Depends);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVendorMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -133,7 +141,8 @@ public sealed class HomebrewPackageAnalyzerTests
|
||||
Assert.Equal("https://openssl.org/", openssl.VendorMetadata["homepage"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_SetsEvidenceSourceToHomebrewCellar()
|
||||
{
|
||||
// Arrange
|
||||
@@ -149,7 +158,8 @@ public sealed class HomebrewPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DiscoversBinFiles()
|
||||
{
|
||||
// Arrange
|
||||
@@ -164,7 +174,8 @@ public sealed class HomebrewPackageAnalyzerTests
|
||||
Assert.Contains(wget.Files, f => f.Path.Contains("wget"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ResultsAreDeterministicallySorted()
|
||||
{
|
||||
// Arrange
|
||||
@@ -182,7 +193,8 @@ public sealed class HomebrewPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_NoCellar_ReturnsEmptyPackages()
|
||||
{
|
||||
// Arrange - use temp directory without Cellar structure
|
||||
@@ -205,7 +217,8 @@ public sealed class HomebrewPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_PopulatesTelemetry()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -8,7 +8,8 @@ public sealed class HomebrewReceiptParserTests
|
||||
{
|
||||
private readonly HomebrewReceiptParser _parser = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ValidReceipt_ReturnsExpectedValues()
|
||||
{
|
||||
// Arrange
|
||||
@@ -51,7 +52,8 @@ public sealed class HomebrewReceiptParserTests
|
||||
Assert.Equal("x86_64", receipt.Architecture);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithRevision_ReturnsCorrectRevision()
|
||||
{
|
||||
// Arrange
|
||||
@@ -77,7 +79,8 @@ public sealed class HomebrewReceiptParserTests
|
||||
Assert.Equal(1, receipt.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_AppleSilicon_ReturnsArm64Architecture()
|
||||
{
|
||||
// Arrange
|
||||
@@ -100,7 +103,8 @@ public sealed class HomebrewReceiptParserTests
|
||||
Assert.Equal("arm64", receipt.Architecture);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithSourceInfo_ExtractsSourceUrlAndChecksum()
|
||||
{
|
||||
// Arrange
|
||||
@@ -126,7 +130,8 @@ public sealed class HomebrewReceiptParserTests
|
||||
Assert.Equal("sha256:abcdef123456", receipt.SourceChecksum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_MultipleDependencies_SortsAlphabetically()
|
||||
{
|
||||
// Arrange
|
||||
@@ -155,7 +160,8 @@ public sealed class HomebrewReceiptParserTests
|
||||
Assert.Equal("zlib", receipt.RuntimeDependencies[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_InvalidJson_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -169,7 +175,8 @@ public sealed class HomebrewReceiptParserTests
|
||||
Assert.Null(receipt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_EmptyJson_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -183,7 +190,8 @@ public sealed class HomebrewReceiptParserTests
|
||||
Assert.Null(receipt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_MissingName_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -202,7 +210,8 @@ public sealed class HomebrewReceiptParserTests
|
||||
Assert.Null(receipt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_TappedFrom_UsesTappedFromOverTap()
|
||||
{
|
||||
// Arrange
|
||||
@@ -224,7 +233,8 @@ public sealed class HomebrewReceiptParserTests
|
||||
Assert.Equal("custom/tap", receipt.Tap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_FallbackVersion_UsesVersionFieldWhenVersionsStableMissing()
|
||||
{
|
||||
// Arrange - older receipt format uses version field directly
|
||||
@@ -245,7 +255,8 @@ public sealed class HomebrewReceiptParserTests
|
||||
Assert.Equal("2.0.0", receipt.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_NormalizesArchitecture_AArch64ToArm64()
|
||||
{
|
||||
// Arrange
|
||||
@@ -259,6 +270,7 @@ public sealed class HomebrewReceiptParserTests
|
||||
""";
|
||||
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||
|
||||
using StellaOps.TestKit;
|
||||
// Act
|
||||
var receipt = _parser.Parse(stream);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Scanner.Analyzers.OS.MacOsBundle;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests;
|
||||
|
||||
public sealed class EntitlementsParserTests
|
||||
@@ -11,7 +12,8 @@ public sealed class EntitlementsParserTests
|
||||
|
||||
private readonly EntitlementsParser _parser = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ValidEntitlements_ReturnsEntitlements()
|
||||
{
|
||||
// Arrange
|
||||
@@ -25,7 +27,8 @@ public sealed class EntitlementsParserTests
|
||||
Assert.True(result.IsSandboxed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_DetectsHighRiskEntitlements()
|
||||
{
|
||||
// Arrange
|
||||
@@ -40,7 +43,8 @@ public sealed class EntitlementsParserTests
|
||||
Assert.Contains("com.apple.security.device.microphone", result.HighRiskEntitlements);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_CategorizeEntitlements()
|
||||
{
|
||||
// Arrange
|
||||
@@ -57,7 +61,8 @@ public sealed class EntitlementsParserTests
|
||||
Assert.Contains("sandbox", result.Categories);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_NonExistentFile_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -70,7 +75,8 @@ public sealed class EntitlementsParserTests
|
||||
Assert.Same(BundleEntitlements.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FindEntitlementsFile_FindsXcentFile()
|
||||
{
|
||||
// Arrange
|
||||
@@ -84,7 +90,8 @@ public sealed class EntitlementsParserTests
|
||||
Assert.EndsWith(".xcent", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FindEntitlementsFile_NoBundlePath_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -94,7 +101,8 @@ public sealed class EntitlementsParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FindEntitlementsFile_NoEntitlements_ReturnsNull()
|
||||
{
|
||||
// Arrange - bundle without entitlements
|
||||
@@ -107,7 +115,8 @@ public sealed class EntitlementsParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HasEntitlement_ReturnsTrueForExistingEntitlement()
|
||||
{
|
||||
// Arrange
|
||||
@@ -119,7 +128,8 @@ public sealed class EntitlementsParserTests
|
||||
Assert.True(result.HasEntitlement("com.apple.security.device.camera"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HasEntitlement_ReturnsFalseForMissingEntitlement()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Scanner.Analyzers.OS.MacOsBundle;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests;
|
||||
|
||||
public sealed class InfoPlistParserTests
|
||||
@@ -11,7 +12,8 @@ public sealed class InfoPlistParserTests
|
||||
|
||||
private readonly InfoPlistParser _parser = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ValidInfoPlist_ReturnsBundleInfo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -29,7 +31,8 @@ public sealed class InfoPlistParserTests
|
||||
Assert.Equal("1.2.3", result.ShortVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ExtractsMinimumSystemVersion()
|
||||
{
|
||||
// Arrange
|
||||
@@ -43,7 +46,8 @@ public sealed class InfoPlistParserTests
|
||||
Assert.Equal("12.0", result.MinimumSystemVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ExtractsExecutable()
|
||||
{
|
||||
// Arrange
|
||||
@@ -57,7 +61,8 @@ public sealed class InfoPlistParserTests
|
||||
Assert.Equal("TestApp", result.Executable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ExtractsSupportedPlatforms()
|
||||
{
|
||||
// Arrange
|
||||
@@ -72,7 +77,8 @@ public sealed class InfoPlistParserTests
|
||||
Assert.Contains("MacOSX", result.SupportedPlatforms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_NonExistentFile_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -85,7 +91,8 @@ public sealed class InfoPlistParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_MissingBundleIdentifier_ReturnsNull()
|
||||
{
|
||||
// Arrange - Create a temp file without CFBundleIdentifier
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.MacOsBundle;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests;
|
||||
|
||||
public sealed class MacOsBundleAnalyzerTests
|
||||
@@ -29,13 +30,15 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
_logger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AnalyzerId_ReturnsMacosBundleIdentifier()
|
||||
{
|
||||
Assert.Equal("macos-bundle", _analyzer.AnalyzerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithValidBundles_ReturnsPackages()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +53,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
Assert.True(result.Packages.Count > 0, "Expected at least one bundle");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_FindsTestApp()
|
||||
{
|
||||
// Arrange
|
||||
@@ -68,7 +72,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
Assert.Equal("Test Application", testApp.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVersionCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -88,7 +93,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
Assert.Equal("123", testApp.Release);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_BuildsCorrectPurl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -105,7 +111,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
Assert.Contains("pkg:generic/macos-app/com.stellaops.testapp@1.2.3", testApp.PackageUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVendorFromBundleId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -122,7 +129,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
Assert.Equal("stellaops", testApp.SourcePackage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_SetsEvidenceSourceToMacOsBundle()
|
||||
{
|
||||
// Arrange
|
||||
@@ -138,7 +146,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVendorMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -159,7 +168,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
Assert.Equal("MacOSX", testApp.VendorMetadata["macos:platforms"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_IncludesCodeResourcesHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -178,7 +188,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
Assert.StartsWith("sha256:", hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DetectsSandboxedApp()
|
||||
{
|
||||
// Arrange
|
||||
@@ -195,7 +206,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
Assert.Equal("true", sandboxedApp.VendorMetadata["macos:sandboxed"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DetectsHighRiskEntitlements()
|
||||
{
|
||||
// Arrange
|
||||
@@ -216,7 +228,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
Assert.Contains("com.apple.security.device.microphone", highRisk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DetectsCapabilityCategories()
|
||||
{
|
||||
// Arrange
|
||||
@@ -239,7 +252,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
Assert.Contains("sandbox", categories);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_IncludesFileEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -264,7 +278,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
Assert.True(infoPlist.IsConfigFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ResultsAreDeterministicallySorted()
|
||||
{
|
||||
// Arrange
|
||||
@@ -282,7 +297,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_NoApplicationsDirectory_ReturnsEmptyPackages()
|
||||
{
|
||||
// Arrange - use temp directory without Applications
|
||||
@@ -305,7 +321,8 @@ public sealed class MacOsBundleAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_PopulatesTelemetry()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Pkgutil;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests;
|
||||
|
||||
public sealed class PkgutilPackageAnalyzerTests
|
||||
@@ -29,13 +30,15 @@ public sealed class PkgutilPackageAnalyzerTests
|
||||
_logger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AnalyzerId_ReturnsPkgutil()
|
||||
{
|
||||
Assert.Equal("pkgutil", _analyzer.AnalyzerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithValidReceipts_ReturnsPackages()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +53,8 @@ public sealed class PkgutilPackageAnalyzerTests
|
||||
Assert.True(result.Packages.Count > 0, "Expected at least one package");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_FindsSafariPackage()
|
||||
{
|
||||
// Arrange
|
||||
@@ -66,7 +70,8 @@ public sealed class PkgutilPackageAnalyzerTests
|
||||
Assert.Contains("pkg:generic/apple/com.apple.pkg.Safari@17.1", safari.PackageUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVendorFromIdentifier()
|
||||
{
|
||||
// Arrange
|
||||
@@ -81,7 +86,8 @@ public sealed class PkgutilPackageAnalyzerTests
|
||||
Assert.Equal("apple", safari.SourcePackage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_SetsEvidenceSourceToPkgutilReceipt()
|
||||
{
|
||||
// Arrange
|
||||
@@ -97,7 +103,8 @@ public sealed class PkgutilPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVendorMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -113,7 +120,8 @@ public sealed class PkgutilPackageAnalyzerTests
|
||||
Assert.Equal("/", safari.VendorMetadata["pkgutil:volume"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ResultsAreDeterministicallySorted()
|
||||
{
|
||||
// Arrange
|
||||
@@ -131,7 +139,8 @@ public sealed class PkgutilPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_NoReceiptsDirectory_ReturnsEmptyPackages()
|
||||
{
|
||||
// Arrange - use temp directory without receipts
|
||||
@@ -154,7 +163,8 @@ public sealed class PkgutilPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_PopulatesTelemetry()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -16,7 +16,8 @@ namespace StellaOps.Scanner.Analyzers.OS.Tests;
|
||||
|
||||
public sealed class OsAnalyzerDeterminismTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ApkAnalyzerMatchesGolden()
|
||||
{
|
||||
using var fixture = FixtureManager.UseFixture("apk", out var rootPath);
|
||||
@@ -28,10 +29,12 @@ public sealed class OsAnalyzerDeterminismTests
|
||||
GoldenAssert.MatchSnapshot(snapshot, FixtureManager.GetGoldenPath("apk.json"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DpkgAnalyzerMatchesGolden()
|
||||
{
|
||||
using var fixture = FixtureManager.UseFixture("dpkg", out var rootPath);
|
||||
using StellaOps.TestKit;
|
||||
var analyzer = new DpkgPackageAnalyzer(NullLogger<DpkgPackageAnalyzer>.Instance);
|
||||
var context = CreateContext(rootPath);
|
||||
|
||||
@@ -40,7 +43,8 @@ public sealed class OsAnalyzerDeterminismTests
|
||||
GoldenAssert.MatchSnapshot(snapshot, FixtureManager.GetGoldenPath("dpkg.json"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RpmAnalyzerMatchesGolden()
|
||||
{
|
||||
var headers = new[]
|
||||
|
||||
@@ -4,11 +4,13 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests;
|
||||
|
||||
public class ChocolateyAnalyzerPluginTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Name_ReturnsCorrectPluginName()
|
||||
{
|
||||
// Arrange
|
||||
@@ -21,7 +23,8 @@ public class ChocolateyAnalyzerPluginTests
|
||||
Assert.Equal("StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey", name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsAvailable_WithValidServiceProvider_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -37,7 +40,8 @@ public class ChocolateyAnalyzerPluginTests
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsAvailable_WithNullServiceProvider_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +54,8 @@ public class ChocolateyAnalyzerPluginTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateAnalyzer_WithValidServiceProvider_ReturnsAnalyzer()
|
||||
{
|
||||
// Arrange
|
||||
@@ -68,7 +73,8 @@ public class ChocolateyAnalyzerPluginTests
|
||||
Assert.Equal("windows-chocolatey", analyzer.AnalyzerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateAnalyzer_WithNullServiceProvider_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -25,13 +25,15 @@ public class ChocolateyPackageAnalyzerTests
|
||||
_logger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AnalyzerId_ReturnsCorrectValue()
|
||||
{
|
||||
Assert.Equal("windows-chocolatey", _analyzer.AnalyzerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithNoChocolateyDirectory_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -55,7 +57,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithEmptyChocolateyLib_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -80,7 +83,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithNuspecFile_ReturnsPackageRecord()
|
||||
{
|
||||
// Arrange
|
||||
@@ -113,7 +117,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithMultiplePackages_ReturnsAllRecords()
|
||||
{
|
||||
// Arrange
|
||||
@@ -160,7 +165,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVendorMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -198,7 +204,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithInstallScript_ComputesHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -231,7 +238,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_FallsBackToDirectoryParsing_WhenNoNuspec()
|
||||
{
|
||||
// Arrange
|
||||
@@ -264,7 +272,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_IncludesFileEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -307,7 +316,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ResultsAreSortedDeterministically()
|
||||
{
|
||||
// Arrange
|
||||
@@ -346,7 +356,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_SkipsHiddenDirectories()
|
||||
{
|
||||
// Arrange
|
||||
@@ -379,7 +390,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_HandlesLowerCaseChocolateyPath()
|
||||
{
|
||||
// Arrange
|
||||
@@ -407,7 +419,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_TruncatesLongDescription()
|
||||
{
|
||||
// Arrange
|
||||
@@ -440,7 +453,8 @@ public class ChocolateyPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithCancellation_ThrowsOperationCanceledException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -452,6 +466,7 @@ public class ChocolateyPackageAnalyzerTests
|
||||
CreateNuspecFile(packageDir, "git", "2.42.0", "Git", "Author", "Git");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
using StellaOps.TestKit;
|
||||
cts.Cancel();
|
||||
|
||||
try
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
using StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests;
|
||||
|
||||
public class NuspecParserTests
|
||||
{
|
||||
private readonly NuspecParser _parser = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithValidNuspec_ReturnsMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -49,7 +51,8 @@ public class NuspecParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithOldNamespace_ReturnsMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -81,7 +84,8 @@ public class NuspecParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithOld2011Namespace_ReturnsMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -113,7 +117,8 @@ public class NuspecParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithNoNamespace_ReturnsMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -145,7 +150,8 @@ public class NuspecParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithMissingId_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -174,7 +180,8 @@ public class NuspecParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithMissingVersion_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -203,7 +210,8 @@ public class NuspecParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithInvalidXml_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -227,7 +235,8 @@ public class NuspecParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithNonExistentFile_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -237,7 +246,8 @@ public class NuspecParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithNullPath_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -247,7 +257,8 @@ public class NuspecParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithEmptyPath_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -257,7 +268,8 @@ public class NuspecParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithWhitespacePath_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -267,7 +279,8 @@ public class NuspecParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ComputesInstallScriptHash_FromToolsDirectory()
|
||||
{
|
||||
// Arrange
|
||||
@@ -302,7 +315,8 @@ public class NuspecParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ComputesInstallScriptHash_FromRootDirectory()
|
||||
{
|
||||
// Arrange
|
||||
@@ -336,7 +350,8 @@ public class NuspecParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_EnumeratesInstalledFiles()
|
||||
{
|
||||
// Arrange
|
||||
@@ -373,7 +388,8 @@ public class NuspecParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("git.2.42.0", "git", "2.42.0")]
|
||||
[InlineData("nodejs.20.10.0", "nodejs", "20.10.0")]
|
||||
[InlineData("7zip.23.01", "7zip", "23.01")]
|
||||
@@ -391,7 +407,8 @@ public class NuspecParserTests
|
||||
Assert.Equal(expectedVersion, result.Value.Version);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests;
|
||||
|
||||
public class MsiDatabaseParserTests
|
||||
{
|
||||
private readonly MsiDatabaseParser _parser = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithValidMsiFile_ExtractsMetadata()
|
||||
{
|
||||
// Arrange - Create a minimal valid OLE compound document
|
||||
@@ -34,7 +36,8 @@ public class MsiDatabaseParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithVersionedFilename_ExtractsVersionFromName()
|
||||
{
|
||||
// Arrange
|
||||
@@ -59,7 +62,8 @@ public class MsiDatabaseParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithSpaceVersionedFilename_ExtractsVersionFromName()
|
||||
{
|
||||
// Arrange
|
||||
@@ -84,7 +88,8 @@ public class MsiDatabaseParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithUnversionedFilename_UsesDefaultVersion()
|
||||
{
|
||||
// Arrange
|
||||
@@ -109,7 +114,8 @@ public class MsiDatabaseParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithNonExistentFile_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -119,7 +125,8 @@ public class MsiDatabaseParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithInvalidMsiFile_ReturnsNull()
|
||||
{
|
||||
// Arrange - Create a file with invalid content
|
||||
@@ -140,7 +147,8 @@ public class MsiDatabaseParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithEmptyPath_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -150,7 +158,8 @@ public class MsiDatabaseParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithNullPath_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -160,7 +169,8 @@ public class MsiDatabaseParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("Product-1.0.msi", "Product", "1.0")]
|
||||
[InlineData("Product-1.0.0.msi", "Product", "1.0.0")]
|
||||
[InlineData("Product-1.0.0.1.msi", "Product", "1.0.0.1")]
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Windows.Msi;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests;
|
||||
|
||||
public class MsiPackageAnalyzerTests
|
||||
@@ -25,13 +26,15 @@ public class MsiPackageAnalyzerTests
|
||||
_logger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AnalyzerId_ReturnsCorrectValue()
|
||||
{
|
||||
Assert.Equal("windows-msi", _analyzer.AnalyzerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithNoMsiDirectory_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -55,7 +58,8 @@ public class MsiPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithMsiFiles_ReturnsPackageRecords()
|
||||
{
|
||||
// Arrange
|
||||
@@ -94,7 +98,8 @@ public class MsiPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithNestedMsiFiles_DiscoversMsisRecursively()
|
||||
{
|
||||
// Arrange
|
||||
@@ -122,7 +127,8 @@ public class MsiPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithUserAppDataCache_ScansMsisInUserDirectories()
|
||||
{
|
||||
// Arrange
|
||||
@@ -149,7 +155,8 @@ public class MsiPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithInvalidMsiFile_SkipsInvalidFile()
|
||||
{
|
||||
// Arrange
|
||||
@@ -180,7 +187,8 @@ public class MsiPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ResultsAreSortedDeterministically()
|
||||
{
|
||||
// Arrange
|
||||
@@ -212,7 +220,8 @@ public class MsiPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithDuplicateMsiFiles_DeduplicatesByPath()
|
||||
{
|
||||
// Arrange
|
||||
@@ -241,7 +250,8 @@ public class MsiPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_SetsCorrectFileEvidence()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests;
|
||||
|
||||
public class WinSxSManifestParserTests
|
||||
{
|
||||
private readonly WinSxSManifestParser _parser = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithValidManifest_ExtractsMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -44,7 +46,8 @@ public class WinSxSManifestParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithAmd64Architecture_ExtractsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -74,7 +77,8 @@ public class WinSxSManifestParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithMultipleFiles_ExtractsAllFiles()
|
||||
{
|
||||
// Arrange
|
||||
@@ -104,7 +108,8 @@ public class WinSxSManifestParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithKbReferenceInFilename_ExtractsKbReference()
|
||||
{
|
||||
// Arrange
|
||||
@@ -130,7 +135,8 @@ public class WinSxSManifestParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithNonExistentFile_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -140,7 +146,8 @@ public class WinSxSManifestParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithInvalidXml_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -160,7 +167,8 @@ public class WinSxSManifestParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithMissingAssemblyIdentity_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -183,7 +191,8 @@ public class WinSxSManifestParserTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WithEmptyPath_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -193,7 +202,8 @@ public class WinSxSManifestParserTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildAssemblyIdentityString_BuildsCorrectFormat()
|
||||
{
|
||||
// Arrange
|
||||
@@ -218,7 +228,8 @@ public class WinSxSManifestParserTests
|
||||
Assert.Equal("microsoft.windows.common-controls_6.0.0.0_x86_6595b64144ccf1df_en-us", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildAssemblyIdentityString_WithNeutralLanguage_OmitsLanguage()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Windows.WinSxS;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests;
|
||||
|
||||
public class WinSxSPackageAnalyzerTests
|
||||
@@ -25,13 +26,15 @@ public class WinSxSPackageAnalyzerTests
|
||||
_logger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AnalyzerId_ReturnsCorrectValue()
|
||||
{
|
||||
Assert.Equal("windows-winsxs", _analyzer.AnalyzerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithNoWinSxSDirectory_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -55,7 +58,8 @@ public class WinSxSPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithManifestFiles_ReturnsPackageRecords()
|
||||
{
|
||||
// Arrange
|
||||
@@ -97,7 +101,8 @@ public class WinSxSPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVendorMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -132,7 +137,8 @@ public class WinSxSPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsPublisherFromAssemblyName()
|
||||
{
|
||||
// Arrange
|
||||
@@ -160,7 +166,8 @@ public class WinSxSPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_IncludesFileEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -201,7 +208,8 @@ public class WinSxSPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_WithInvalidManifest_SkipsAndContinues()
|
||||
{
|
||||
// Arrange
|
||||
@@ -233,7 +241,8 @@ public class WinSxSPackageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ResultsAreSortedDeterministically()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -42,7 +42,8 @@ public sealed class LayerCacheRoundTripTests : IAsyncLifetime
|
||||
_fileCas = new FileContentAddressableStore(_options, NullLogger<FileContentAddressableStore>.Instance, _timeProvider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RoundTrip_Succeeds_And_Respects_Ttl_And_ImportExport()
|
||||
{
|
||||
var layerDigest = "sha256:abcd1234";
|
||||
@@ -109,6 +110,7 @@ public sealed class LayerCacheRoundTripTests : IAsyncLifetime
|
||||
// Compaction removes CAS entry once over threshold.
|
||||
// Force compaction by writing a large entry.
|
||||
using var largeStream = CreateStream(new string('x', 400_000));
|
||||
using StellaOps.TestKit;
|
||||
var largeHash = "sha256:" + new string('e', 64);
|
||||
await _fileCas.PutAsync(new FileCasPutRequest(largeHash, largeStream), CancellationToken.None);
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
|
||||
@@ -2,11 +2,13 @@ using StellaOps.Scanner.CallGraph;
|
||||
using StellaOps.Scanner.CallGraph.Node;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class BenchmarkIntegrationTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("unsafe-eval", true)]
|
||||
[InlineData("guarded-eval", false)]
|
||||
public async Task NodeTraceExtractor_AlignsWithBenchmarkReachability(string caseName, bool expectSinkReachable)
|
||||
|
||||
@@ -8,11 +8,13 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.CallGraph.Binary;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class BinaryCallGraphExtractorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BinaryEntrypointClassifier_ClassifiesMainFunction()
|
||||
{
|
||||
// Arrange
|
||||
@@ -34,7 +36,8 @@ public class BinaryCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.CliCommand, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BinaryEntrypointClassifier_ClassifiesInitArray()
|
||||
{
|
||||
// Arrange
|
||||
@@ -56,7 +59,8 @@ public class BinaryCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.InitFunction, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BinaryEntrypointClassifier_ReturnsNullForInternalFunction()
|
||||
{
|
||||
// Arrange
|
||||
@@ -77,7 +81,8 @@ public class BinaryCallGraphExtractorTests
|
||||
Assert.False(result.HasValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DwarfDebugReader_HandlesNonExistentFile()
|
||||
{
|
||||
// Arrange
|
||||
@@ -89,7 +94,8 @@ public class BinaryCallGraphExtractorTests
|
||||
await reader.ReadAsync("/nonexistent/binary", default));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BinaryRelocation_HasCorrectProperties()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -110,7 +116,8 @@ public class BinaryCallGraphExtractorTests
|
||||
Assert.True(relocation.IsExternal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DwarfFunction_RecordsCorrectInfo()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -135,7 +142,8 @@ public class BinaryCallGraphExtractorTests
|
||||
Assert.True(func.IsExternal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BinarySymbol_TracksVisibility()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
@@ -2,11 +2,13 @@ using StellaOps.Scanner.CallGraph.Binary;
|
||||
using StellaOps.Scanner.CallGraph.Binary.Disassembly;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class BinaryDisassemblyTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void X86Disassembler_Extracts_Call_And_Jmp()
|
||||
{
|
||||
var disassembler = new X86Disassembler();
|
||||
@@ -26,7 +28,8 @@ public class BinaryDisassemblyTests
|
||||
Assert.Equal(0x100CUL, calls[1].TargetAddress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DirectCallExtractor_Maps_Targets_To_Symbols()
|
||||
{
|
||||
var extractor = new DirectCallExtractor();
|
||||
|
||||
@@ -5,11 +5,13 @@ using StellaOps.Scanner.CallGraph.Binary.Disassembly;
|
||||
using StellaOps.Scanner.CallGraph.Binary.Analysis;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class BinaryTextSectionReaderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReadsElfTextSection()
|
||||
{
|
||||
var textBytes = new byte[] { 0x90, 0x90, 0xC3, 0x90 };
|
||||
@@ -32,7 +34,8 @@ public class BinaryTextSectionReaderTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReadsPeTextSection()
|
||||
{
|
||||
var textBytes = new byte[] { 0x90, 0x90, 0xC3, 0x90 };
|
||||
@@ -54,7 +57,8 @@ public class BinaryTextSectionReaderTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReadsMachOTextSection()
|
||||
{
|
||||
var textBytes = new byte[] { 0x1F, 0x20, 0x03, 0xD5 };
|
||||
@@ -76,7 +80,8 @@ public class BinaryTextSectionReaderTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StringScannerExtractsLibraryCandidates()
|
||||
{
|
||||
var textBytes = new byte[] { 0x90, 0x90, 0xC3, 0x90 };
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using StellaOps.Scanner.CallGraph.Caching;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class CircuitBreakerStateTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RecordFailure_TripsOpen_AfterThreshold()
|
||||
{
|
||||
var config = new CircuitBreakerConfig
|
||||
@@ -26,7 +28,8 @@ public class CircuitBreakerStateTests
|
||||
Assert.True(cb.IsOpen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RecordSuccess_ResetsToClosed()
|
||||
{
|
||||
var config = new CircuitBreakerConfig { FailureThreshold = 1, TimeoutSeconds = 60, HalfOpenTimeout = 10 };
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class DotNetCallGraphExtractorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_SimpleProject_ProducesEntrypointAndSink()
|
||||
{
|
||||
await using var temp = await TempDirectory.CreateAsync();
|
||||
@@ -71,11 +72,13 @@ public class DotNetCallGraphExtractorTests
|
||||
Assert.NotEmpty(snapshot.EntrypointIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_IsDeterministic_ForSameInputs()
|
||||
{
|
||||
await using var temp = await TempDirectory.CreateAsync();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var csprojPath = Path.Combine(temp.Path, "App.csproj");
|
||||
await File.WriteAllTextAsync(csprojPath, """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
@@ -9,11 +9,13 @@ using StellaOps.Scanner.CallGraph.Go;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class GoCallGraphExtractorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildFunctionId_CreatesCorrectFormat()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -23,7 +25,8 @@ public class GoCallGraphExtractorTests
|
||||
Assert.Equal("go:github.com/example/pkg.HandleRequest", id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildMethodId_CreatesCorrectFormat()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -33,7 +36,8 @@ public class GoCallGraphExtractorTests
|
||||
Assert.Equal("go:github.com/example/pkg.Server.Start", id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildExternalId_CreatesCorrectFormat()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -43,7 +47,8 @@ public class GoCallGraphExtractorTests
|
||||
Assert.Equal("go:external/fmt.Println", id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ParsesFunctionId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -61,7 +66,8 @@ public class GoCallGraphExtractorTests
|
||||
Assert.False(result.IsExternal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ParsesExternalId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -77,7 +83,8 @@ public class GoCallGraphExtractorTests
|
||||
Assert.True(result.IsExternal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsStdLib_ReturnsTrueForStandardLibrary()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
@@ -86,7 +93,8 @@ public class GoCallGraphExtractorTests
|
||||
Assert.False(GoSymbolIdBuilder.IsStdLib("go:external/github.com/gin-gonic/gin.New"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoSsaResultParser_ParsesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
@@ -120,7 +128,8 @@ public class GoCallGraphExtractorTests
|
||||
Assert.Single(result.Entrypoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoSsaResultParser_ThrowsOnEmptyInput()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
@@ -128,7 +137,8 @@ public class GoCallGraphExtractorTests
|
||||
Assert.Throws<ArgumentException>(() => GoSsaResultParser.Parse(" "));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoEntrypointClassifier_ClassifiesHttpHandler()
|
||||
{
|
||||
// Arrange
|
||||
@@ -164,7 +174,8 @@ public class GoCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.HttpHandler, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoSinkMatcher_MatchesExecCommand()
|
||||
{
|
||||
// Arrange
|
||||
@@ -177,7 +188,8 @@ public class GoCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.CmdExec, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoSinkMatcher_MatchesSqlQuery()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -31,7 +31,8 @@ public class JavaCallGraphExtractorTests
|
||||
|
||||
#region JavaEntrypointClassifier Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaEntrypointClassifier_SpringRequestMapping_DetectedAsHttpHandler()
|
||||
{
|
||||
var classifier = new JavaEntrypointClassifier();
|
||||
@@ -61,7 +62,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.HttpHandler, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaEntrypointClassifier_SpringRestController_PublicMethodDetectedAsHttpHandler()
|
||||
{
|
||||
var classifier = new JavaEntrypointClassifier();
|
||||
@@ -91,7 +93,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.HttpHandler, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaEntrypointClassifier_JaxRsPath_DetectedAsHttpHandler()
|
||||
{
|
||||
var classifier = new JavaEntrypointClassifier();
|
||||
@@ -121,7 +124,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.HttpHandler, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaEntrypointClassifier_SpringScheduled_DetectedAsScheduledJob()
|
||||
{
|
||||
var classifier = new JavaEntrypointClassifier();
|
||||
@@ -151,7 +155,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.ScheduledJob, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaEntrypointClassifier_KafkaListener_DetectedAsMessageHandler()
|
||||
{
|
||||
var classifier = new JavaEntrypointClassifier();
|
||||
@@ -181,7 +186,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.MessageHandler, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaEntrypointClassifier_GrpcService_DetectedAsGrpcMethod()
|
||||
{
|
||||
var classifier = new JavaEntrypointClassifier();
|
||||
@@ -212,7 +218,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.GrpcMethod, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaEntrypointClassifier_MainMethod_DetectedAsCliCommand()
|
||||
{
|
||||
var classifier = new JavaEntrypointClassifier();
|
||||
@@ -242,7 +249,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.CliCommand, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaEntrypointClassifier_PrivateMethod_NotDetectedAsEntrypoint()
|
||||
{
|
||||
var classifier = new JavaEntrypointClassifier();
|
||||
@@ -276,7 +284,8 @@ public class JavaCallGraphExtractorTests
|
||||
|
||||
#region JavaSinkMatcher Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaSinkMatcher_RuntimeExec_DetectedAsCmdExec()
|
||||
{
|
||||
var matcher = new JavaSinkMatcher();
|
||||
@@ -286,7 +295,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.CmdExec, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaSinkMatcher_ProcessBuilderInit_DetectedAsCmdExec()
|
||||
{
|
||||
var matcher = new JavaSinkMatcher();
|
||||
@@ -296,7 +306,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.CmdExec, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaSinkMatcher_StatementExecute_DetectedAsSqlRaw()
|
||||
{
|
||||
var matcher = new JavaSinkMatcher();
|
||||
@@ -306,7 +317,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.SqlRaw, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaSinkMatcher_ObjectInputStream_DetectedAsUnsafeDeser()
|
||||
{
|
||||
var matcher = new JavaSinkMatcher();
|
||||
@@ -316,7 +328,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.UnsafeDeser, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaSinkMatcher_HttpClientExecute_DetectedAsSsrf()
|
||||
{
|
||||
var matcher = new JavaSinkMatcher();
|
||||
@@ -326,7 +339,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.Ssrf, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaSinkMatcher_FileWriter_DetectedAsPathTraversal()
|
||||
{
|
||||
var matcher = new JavaSinkMatcher();
|
||||
@@ -336,7 +350,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.PathTraversal, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaSinkMatcher_UnknownMethod_ReturnsNull()
|
||||
{
|
||||
var matcher = new JavaSinkMatcher();
|
||||
@@ -346,7 +361,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaSinkMatcher_XxeVulnerableParsing_DetectedAsXxe()
|
||||
{
|
||||
var matcher = new JavaSinkMatcher();
|
||||
@@ -356,7 +372,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.XxeInjection, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaSinkMatcher_ScriptEngineEval_DetectedAsCodeInjection()
|
||||
{
|
||||
var matcher = new JavaSinkMatcher();
|
||||
@@ -370,7 +387,8 @@ public class JavaCallGraphExtractorTests
|
||||
|
||||
#region JavaBytecodeAnalyzer Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaBytecodeAnalyzer_ValidClassHeader_Parsed()
|
||||
{
|
||||
var analyzer = new JavaBytecodeAnalyzer(NullLogger<JavaCallGraphExtractor>.Instance);
|
||||
@@ -394,7 +412,8 @@ public class JavaCallGraphExtractorTests
|
||||
// The important thing is it doesn't throw
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaBytecodeAnalyzer_InvalidMagic_ReturnsNull()
|
||||
{
|
||||
var analyzer = new JavaBytecodeAnalyzer(NullLogger<JavaCallGraphExtractor>.Instance);
|
||||
@@ -406,7 +425,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaBytecodeAnalyzer_EmptyArray_ReturnsNull()
|
||||
{
|
||||
var analyzer = new JavaBytecodeAnalyzer(NullLogger<JavaCallGraphExtractor>.Instance);
|
||||
@@ -420,7 +440,8 @@ public class JavaCallGraphExtractorTests
|
||||
|
||||
#region Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_InvalidPath_ThrowsFileNotFound()
|
||||
{
|
||||
var request = new CallGraphExtractionRequest(
|
||||
@@ -432,7 +453,8 @@ public class JavaCallGraphExtractorTests
|
||||
() => _extractor.ExtractAsync(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_WrongLanguage_ThrowsArgumentException()
|
||||
{
|
||||
await using var temp = await TempDirectory.CreateAsync();
|
||||
@@ -446,7 +468,8 @@ public class JavaCallGraphExtractorTests
|
||||
() => _extractor.ExtractAsync(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_EmptyDirectory_ProducesEmptySnapshot()
|
||||
{
|
||||
await using var temp = await TempDirectory.CreateAsync();
|
||||
@@ -468,7 +491,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(_fixedTime, snapshot.ExtractedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extractor_Language_IsJava()
|
||||
{
|
||||
Assert.Equal("java", _extractor.Language);
|
||||
@@ -478,7 +502,8 @@ public class JavaCallGraphExtractorTests
|
||||
|
||||
#region Determinism Verification Tests (JCG-020)
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_SamePath_ProducesSameDigest()
|
||||
{
|
||||
// Arrange: Create a temp directory
|
||||
@@ -497,12 +522,14 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(snapshot1.GraphDigest, snapshot2.GraphDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_DifferentScanId_SameNodesAndEdges()
|
||||
{
|
||||
// Arrange: Create a temp directory
|
||||
await using var temp = await TempDirectory.CreateAsync();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var request1 = new CallGraphExtractionRequest(
|
||||
ScanId: "scan-a",
|
||||
Language: "java",
|
||||
@@ -523,7 +550,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(snapshot1.GraphDigest, snapshot2.GraphDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildNodeId_SameInputs_ProducesIdenticalIds()
|
||||
{
|
||||
// Act: Build node IDs multiple times with same inputs
|
||||
@@ -534,7 +562,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildNodeId_DifferentDescriptors_ProducesDifferentIds()
|
||||
{
|
||||
// Act: Build node IDs with different descriptors (overloaded methods)
|
||||
@@ -545,7 +574,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.NotEqual(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaEntrypointClassifier_SameInput_AlwaysSameResult()
|
||||
{
|
||||
var classifier = new JavaEntrypointClassifier();
|
||||
@@ -581,7 +611,8 @@ public class JavaCallGraphExtractorTests
|
||||
Assert.Equal(result2, result3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaSinkMatcher_SameInput_AlwaysSameResult()
|
||||
{
|
||||
var matcher = new JavaSinkMatcher();
|
||||
|
||||
@@ -35,7 +35,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
|
||||
#region Entrypoint Classifier Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointClassifier_ExpressHandler_ReturnsHttpHandler()
|
||||
{
|
||||
var classifier = new JsEntrypointClassifier();
|
||||
@@ -56,7 +57,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(EntrypointType.HttpHandler, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointClassifier_FastifyRoute_ReturnsHttpHandler()
|
||||
{
|
||||
var classifier = new JsEntrypointClassifier();
|
||||
@@ -79,7 +81,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(EntrypointType.HttpHandler, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointClassifier_LambdaHandler_ReturnsLambda()
|
||||
{
|
||||
var classifier = new JsEntrypointClassifier();
|
||||
@@ -99,7 +102,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(EntrypointType.Lambda, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointClassifier_AzureFunction_WithHandler_ReturnsLambda()
|
||||
{
|
||||
var classifier = new JsEntrypointClassifier();
|
||||
@@ -119,7 +123,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(EntrypointType.Lambda, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointClassifier_CliWithRunName_ReturnsCliCommand()
|
||||
{
|
||||
var classifier = new JsEntrypointClassifier();
|
||||
@@ -139,7 +144,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(EntrypointType.CliCommand, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointClassifier_UnknownSocket_ReturnsNull()
|
||||
{
|
||||
var classifier = new JsEntrypointClassifier();
|
||||
@@ -160,7 +166,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointClassifier_NestHandler_ReturnsMessageHandler()
|
||||
{
|
||||
var classifier = new JsEntrypointClassifier();
|
||||
@@ -181,7 +188,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(EntrypointType.MessageHandler, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointClassifier_UnknownCron_ReturnsNull()
|
||||
{
|
||||
var classifier = new JsEntrypointClassifier();
|
||||
@@ -202,7 +210,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointClassifier_GraphQL_ReturnsHttpHandler()
|
||||
{
|
||||
var classifier = new JsEntrypointClassifier();
|
||||
@@ -223,7 +232,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(EntrypointType.HttpHandler, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointClassifier_NoMatch_ReturnsNull()
|
||||
{
|
||||
var classifier = new JsEntrypointClassifier();
|
||||
@@ -247,7 +257,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
|
||||
#region Sink Matcher Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_ChildProcessExec_ReturnsCmdExec()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -257,7 +268,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.CmdExec, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_ChildProcessSpawn_ReturnsCmdExec()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -267,7 +279,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.CmdExec, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_ChildProcessFork_ReturnsCmdExec()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -277,7 +290,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.CmdExec, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_MysqlQuery_ReturnsSqlRaw()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -287,7 +301,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.SqlRaw, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_PgQuery_ReturnsSqlRaw()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -297,7 +312,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.SqlRaw, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_KnexRaw_ReturnsSqlRaw()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -307,7 +323,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.SqlRaw, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_FsReadFile_ReturnsPathTraversal()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -317,7 +334,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.PathTraversal, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_FsWriteFile_ReturnsPathTraversal()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -327,7 +345,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.PathTraversal, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_AxiosGet_ReturnsSsrf()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -337,7 +356,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.Ssrf, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_HttpRequest_ReturnsSsrf()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -347,7 +367,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.Ssrf, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_JsYamlLoad_ReturnsUnsafeDeser()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -357,7 +378,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.UnsafeDeser, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_Eval_ReturnsCodeInjection()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -367,7 +389,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.CodeInjection, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_VmRunInContext_ReturnsCodeInjection()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -377,7 +400,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.CodeInjection, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_EjsRender_ReturnsTemplateInjection()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -387,7 +411,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(SinkCategory.TemplateInjection, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_NoMatch_ReturnsNull()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
@@ -401,7 +426,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
|
||||
#region Extractor Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extractor_Language_IsJavascript()
|
||||
{
|
||||
Assert.Equal("javascript", _extractor.Language);
|
||||
@@ -456,6 +482,7 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
{
|
||||
await using var temp = await TempDirectory.CreateAsync();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var packageJson = """
|
||||
{
|
||||
"name": "test-app",
|
||||
@@ -475,7 +502,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(snapshot1.GraphDigest, snapshot2.GraphDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointClassifier_SameInput_AlwaysSameResult()
|
||||
{
|
||||
var classifier = new JsEntrypointClassifier();
|
||||
@@ -499,7 +527,8 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
Assert.Equal(result2, result3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsSinkMatcher_SameInput_AlwaysSameResult()
|
||||
{
|
||||
var matcher = new JsSinkMatcher();
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
using StellaOps.Scanner.CallGraph.Node;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class NodeCallGraphExtractorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BabelResultParser_ParsesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
@@ -56,7 +58,8 @@ public class NodeCallGraphExtractorTests
|
||||
Assert.Single(result.Entrypoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BabelResultParser_ParsesNodeWithPosition()
|
||||
{
|
||||
// Arrange
|
||||
@@ -92,7 +95,8 @@ public class NodeCallGraphExtractorTests
|
||||
Assert.Equal(5, node.Position.Column);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BabelResultParser_ParsesEdgeWithSite()
|
||||
{
|
||||
// Arrange
|
||||
@@ -127,7 +131,8 @@ public class NodeCallGraphExtractorTests
|
||||
Assert.Equal(25, edge.Site.Line);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BabelResultParser_ThrowsOnEmptyInput()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
@@ -135,7 +140,8 @@ public class NodeCallGraphExtractorTests
|
||||
Assert.Throws<ArgumentNullException>(() => BabelResultParser.Parse(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BabelResultParser_ParsesNdjson()
|
||||
{
|
||||
// Arrange
|
||||
@@ -152,7 +158,8 @@ public class NodeCallGraphExtractorTests
|
||||
Assert.Equal("app", result.Module);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JsEntrypointInfo_HasCorrectProperties()
|
||||
{
|
||||
// Arrange
|
||||
@@ -184,7 +191,8 @@ public class NodeCallGraphExtractorTests
|
||||
Assert.Equal("GET", ep.Method);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BabelResultParser_ParsesSinks()
|
||||
{
|
||||
// Arrange
|
||||
@@ -229,7 +237,8 @@ public class NodeCallGraphExtractorTests
|
||||
Assert.Equal(42, sink.Site.Line);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BabelResultParser_ParsesMultipleSinkCategories()
|
||||
{
|
||||
// Arrange
|
||||
@@ -269,7 +278,8 @@ public class NodeCallGraphExtractorTests
|
||||
Assert.Contains(result.Sinks, s => s.Category == "file_write");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BabelResultParser_ParsesEmptySinks()
|
||||
{
|
||||
// Arrange
|
||||
@@ -290,7 +300,8 @@ public class NodeCallGraphExtractorTests
|
||||
Assert.Empty(result.Sinks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BabelResultParser_ParsesMissingSinks()
|
||||
{
|
||||
// Arrange - sinks field omitted entirely
|
||||
@@ -310,7 +321,8 @@ public class NodeCallGraphExtractorTests
|
||||
Assert.Empty(result.Sinks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BabelResultParser_ParsesSinkWithoutSite()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -9,11 +9,13 @@ using StellaOps.Scanner.CallGraph.Python;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class PythonCallGraphExtractorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PythonEntrypointClassifier_ClassifiesFlaskRoute()
|
||||
{
|
||||
// Arrange
|
||||
@@ -40,7 +42,8 @@ public class PythonCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.HttpHandler, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PythonEntrypointClassifier_ClassifiesFastApiRoute()
|
||||
{
|
||||
// Arrange
|
||||
@@ -67,7 +70,8 @@ public class PythonCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.HttpHandler, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PythonEntrypointClassifier_ClassifiesCeleryTask()
|
||||
{
|
||||
// Arrange
|
||||
@@ -94,7 +98,8 @@ public class PythonCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.BackgroundJob, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PythonEntrypointClassifier_ClassifiesClickCommand()
|
||||
{
|
||||
// Arrange
|
||||
@@ -121,7 +126,8 @@ public class PythonCallGraphExtractorTests
|
||||
Assert.Equal(EntrypointType.CliCommand, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PythonSinkMatcher_MatchesSubprocessCall()
|
||||
{
|
||||
// Arrange
|
||||
@@ -134,7 +140,8 @@ public class PythonCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.CmdExec, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PythonSinkMatcher_MatchesEval()
|
||||
{
|
||||
// Arrange
|
||||
@@ -147,7 +154,8 @@ public class PythonCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.CodeInjection, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PythonSinkMatcher_MatchesPickleLoads()
|
||||
{
|
||||
// Arrange
|
||||
@@ -160,7 +168,8 @@ public class PythonCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.UnsafeDeser, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PythonSinkMatcher_MatchesSqlAlchemyExecute()
|
||||
{
|
||||
// Arrange
|
||||
@@ -173,7 +182,8 @@ public class PythonCallGraphExtractorTests
|
||||
Assert.Equal(SinkCategory.SqlRaw, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PythonSinkMatcher_ReturnsNullForSafeFunction()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.CallGraph;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -10,7 +11,8 @@ namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
/// </summary>
|
||||
public class ReachabilityAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Analyze_WhenSinkReachable_ReturnsShortestPath()
|
||||
{
|
||||
var entry = CallGraphNodeIds.Compute("dotnet:test:entry");
|
||||
@@ -46,7 +48,8 @@ public class ReachabilityAnalyzerTests
|
||||
Assert.Equal(new[] { entry, mid, sink }, result.Paths[0].NodeIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Analyze_WhenNoEntrypoints_ReturnsEmpty()
|
||||
{
|
||||
var snapshot = new CallGraphSnapshot(
|
||||
@@ -71,7 +74,8 @@ public class ReachabilityAnalyzerTests
|
||||
/// <summary>
|
||||
/// WIT-007A: Verify deterministic path ordering (SinkId ASC, EntrypointId ASC, PathLength ASC).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Analyze_PathsAreDeterministicallyOrdered_BySinkIdThenEntrypointIdThenLength()
|
||||
{
|
||||
// Arrange: create graph with multiple entrypoints and sinks
|
||||
@@ -121,7 +125,8 @@ public class ReachabilityAnalyzerTests
|
||||
/// <summary>
|
||||
/// WIT-007A: Verify that multiple runs produce identical results (determinism).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Analyze_ProducesIdenticalResults_OnMultipleRuns()
|
||||
{
|
||||
var entry = "entry:test";
|
||||
@@ -163,7 +168,8 @@ public class ReachabilityAnalyzerTests
|
||||
/// <summary>
|
||||
/// WIT-007A: Verify MaxTotalPaths limit is enforced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Analyze_WithOptions_RespectsMaxTotalPathsLimit()
|
||||
{
|
||||
// Arrange: create graph with 5 sinks reachable from 1 entrypoint
|
||||
@@ -206,7 +212,8 @@ public class ReachabilityAnalyzerTests
|
||||
/// <summary>
|
||||
/// WIT-007A: Verify MaxDepth limit is enforced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Analyze_WithOptions_RespectsMaxDepthLimit()
|
||||
{
|
||||
// Arrange: create a chain of 10 nodes
|
||||
@@ -250,7 +257,8 @@ public class ReachabilityAnalyzerTests
|
||||
/// <summary>
|
||||
/// WIT-007A: Verify node IDs in paths are ordered from entrypoint to sink.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Analyze_PathNodeIds_AreOrderedFromEntrypointToSink()
|
||||
{
|
||||
var entry = "entry:start";
|
||||
@@ -297,7 +305,8 @@ public class ReachabilityAnalyzerTests
|
||||
/// <summary>
|
||||
/// WIT-007B: Verify ExplicitSinks option allows targeting specific sinks not in snapshot.SinkIds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Analyze_WithExplicitSinks_FindsPathsToSpecifiedSinksOnly()
|
||||
{
|
||||
// Arrange: graph with 3 reachable nodes, only 1 is in snapshot.SinkIds
|
||||
@@ -347,7 +356,8 @@ public class ReachabilityAnalyzerTests
|
||||
/// <summary>
|
||||
/// WIT-007B: Verify ExplicitSinks with empty array falls back to snapshot sinks.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Analyze_WithEmptyExplicitSinks_UsesSnapshotSinks()
|
||||
{
|
||||
var entry = "entry:start";
|
||||
|
||||
@@ -6,6 +6,7 @@ using StellaOps.Scanner.CallGraph;
|
||||
using StellaOps.Scanner.CallGraph.Caching;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class ValkeyCallGraphCacheServiceTests : IAsyncLifetime
|
||||
@@ -76,7 +77,8 @@ public class ValkeyCallGraphCacheServiceTests : IAsyncLifetime
|
||||
await _cache.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SetThenGet_CallGraph_RoundTrips()
|
||||
{
|
||||
var nodeId = CallGraphNodeIds.Compute("dotnet:test:entry");
|
||||
@@ -99,7 +101,8 @@ public class ValkeyCallGraphCacheServiceTests : IAsyncLifetime
|
||||
Assert.Equal(snapshot.GraphDigest, loaded.GraphDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SetThenGet_ReachabilityResult_RoundTrips()
|
||||
{
|
||||
var result = new ReachabilityAnalysisResult(
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace StellaOps.Scanner.Core.Tests;
|
||||
|
||||
public class ReachabilityGraphBuilderUnionTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ConvertsBuilderToUnionGraphAndWritesNdjson()
|
||||
{
|
||||
var builder = new ReachabilityGraphBuilder()
|
||||
@@ -18,6 +19,7 @@ public class ReachabilityGraphBuilderUnionTests
|
||||
var writer = new ReachabilityUnionWriter();
|
||||
|
||||
using var temp = new TempDir();
|
||||
using StellaOps.TestKit;
|
||||
var result = await writer.WriteAsync(graph, temp.Path, "analysis-graph-1");
|
||||
|
||||
Assert.Equal(2, result.Nodes.RecordCount);
|
||||
|
||||
@@ -8,7 +8,8 @@ namespace StellaOps.Scanner.Core.Tests;
|
||||
|
||||
public class ReachabilityUnionPublisherTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishesZipToCas()
|
||||
{
|
||||
var graph = new ReachabilityUnionGraph(
|
||||
@@ -17,6 +18,7 @@ public class ReachabilityUnionPublisherTests
|
||||
|
||||
var cas = new FakeFileContentAddressableStore();
|
||||
using var temp = new TempDir();
|
||||
using StellaOps.TestKit;
|
||||
var publisher = new ReachabilityUnionPublisher(new ReachabilityUnionWriter());
|
||||
|
||||
var result = await publisher.PublishAsync(graph, cas, temp.Path, "analysis-pub-1");
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace StellaOps.Scanner.Core.Tests;
|
||||
|
||||
public class ReachabilityUnionWriterTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WritesDeterministicFilesAndHashes()
|
||||
{
|
||||
var writer = new ReachabilityUnionWriter();
|
||||
@@ -62,6 +63,7 @@ public class ReachabilityUnionWriterTests
|
||||
.EnumerateArray()
|
||||
.Select(file => (Path: file.GetProperty("path").GetString(), Sha256: file.GetProperty("sha256").GetString()))
|
||||
.ToList();
|
||||
using StellaOps.TestKit;
|
||||
}
|
||||
|
||||
Assert.Contains(files, file => file.Path == result.Nodes.Path && file.Sha256 == result.Nodes.Sha256);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Core.Tests;
|
||||
|
||||
public class ScanManifestTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHash_SameManifest_ProducesSameHash()
|
||||
{
|
||||
var manifest1 = CreateSampleManifest();
|
||||
@@ -18,7 +20,8 @@ public class ScanManifestTests
|
||||
Assert.StartsWith("sha256:", hash1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHash_DifferentSeed_ProducesDifferentHash()
|
||||
{
|
||||
var seed1 = new byte[32];
|
||||
@@ -32,7 +35,8 @@ public class ScanManifestTests
|
||||
Assert.NotEqual(manifest1.ComputeHash(), manifest2.ComputeHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHash_DifferentArtifactDigest_ProducesDifferentHash()
|
||||
{
|
||||
var manifest1 = CreateSampleManifest(artifactDigest: "sha256:abc123");
|
||||
@@ -41,7 +45,8 @@ public class ScanManifestTests
|
||||
Assert.NotEqual(manifest1.ComputeHash(), manifest2.ComputeHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHash_HashIsLowercaseHex()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
@@ -52,7 +57,8 @@ public class ScanManifestTests
|
||||
Assert.Matches(@"^[0-9a-f]{64}$", hexPart);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialization_RoundTrip_PreservesAllFields()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
@@ -71,7 +77,8 @@ public class ScanManifestTests
|
||||
Assert.Equal(manifest.Seed, deserialized.Seed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialization_JsonPropertyNames_AreCamelCase()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
@@ -84,7 +91,8 @@ public class ScanManifestTests
|
||||
Assert.Contains("\"concelierSnapshotHash\":", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalJson_ProducesDeterministicOutput()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
@@ -95,7 +103,8 @@ public class ScanManifestTests
|
||||
Assert.Equal(json1, json2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Builder_CreatesValidManifest()
|
||||
{
|
||||
var seed = new byte[32];
|
||||
@@ -123,7 +132,8 @@ public class ScanManifestTests
|
||||
Assert.Equal("10", manifest.Knobs["maxDepth"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Builder_WithKnobs_MergesMultipleKnobs()
|
||||
{
|
||||
var manifest = ScanManifest.CreateBuilder("scan-001", "sha256:abc123")
|
||||
@@ -140,7 +150,8 @@ public class ScanManifestTests
|
||||
Assert.Equal("value4", manifest.Knobs["key4"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Builder_SeedMustBe32Bytes()
|
||||
{
|
||||
var builder = ScanManifest.CreateBuilder("scan-001", "sha256:abc123");
|
||||
@@ -149,7 +160,8 @@ public class ScanManifestTests
|
||||
Assert.Contains("32 bytes", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Record_WithExpression_CreatesModifiedCopy()
|
||||
{
|
||||
var original = CreateSampleManifest();
|
||||
@@ -160,7 +172,8 @@ public class ScanManifestTests
|
||||
Assert.Equal(original.ScanId, modified.ScanId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToJson_Indented_FormatsOutput()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
@@ -170,7 +183,8 @@ public class ScanManifestTests
|
||||
Assert.Contains(" ", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToJson_NotIndented_CompactOutput()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
@@ -179,7 +193,8 @@ public class ScanManifestTests
|
||||
Assert.DoesNotContain("\n", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void KnobsCollection_IsImmutable()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace StellaOps.Scanner.Diff.Tests;
|
||||
|
||||
public sealed class ComponentDifferTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compute_CapturesAddedRemovedAndChangedComponents()
|
||||
{
|
||||
var oldFragments = new[]
|
||||
@@ -173,7 +174,8 @@ public sealed class ComponentDifferTests
|
||||
Assert.False(removedJson.TryGetProperty("introducingLayer", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compute_UsageView_FiltersComponents()
|
||||
{
|
||||
var oldFragments = new[]
|
||||
@@ -218,7 +220,8 @@ public sealed class ComponentDifferTests
|
||||
Assert.False(parsed.RootElement.TryGetProperty("newImageDigest", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compute_MetadataChange_WhenEvidenceDiffers()
|
||||
{
|
||||
var oldFragments = new[]
|
||||
@@ -277,7 +280,8 @@ public sealed class ComponentDifferTests
|
||||
Assert.Equal(1, change.OldComponent!.Evidence.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compute_MetadataChange_WhenBuildIdDiffers()
|
||||
{
|
||||
var oldFragments = new[]
|
||||
@@ -333,6 +337,7 @@ public sealed class ComponentDifferTests
|
||||
|
||||
var json = DiffJsonSerializer.Serialize(document);
|
||||
using var parsed = JsonDocument.Parse(json);
|
||||
using StellaOps.TestKit;
|
||||
var changeJson = parsed.RootElement
|
||||
.GetProperty("layers")[0]
|
||||
.GetProperty("changes")[0];
|
||||
|
||||
@@ -5,13 +5,15 @@ using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Emit.Lineage;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Emit.Lineage.Tests;
|
||||
|
||||
public class RebuildProofTests
|
||||
{
|
||||
#region RebuildProof Model Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RebuildProof_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var proof = new RebuildProof
|
||||
@@ -31,7 +33,8 @@ public class RebuildProofTests
|
||||
proof.PolicyHash.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RebuildProof_WithFeedSnapshots_TracksAllFeeds()
|
||||
{
|
||||
var feeds = ImmutableArray.Create(
|
||||
@@ -69,7 +72,8 @@ public class RebuildProofTests
|
||||
proof.FeedSnapshots[1].EntryCount.Should().Be(15000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RebuildProof_WithAnalyzerVersions_TracksAllAnalyzers()
|
||||
{
|
||||
var analyzers = ImmutableArray.Create(
|
||||
@@ -103,7 +107,8 @@ public class RebuildProofTests
|
||||
proof.AnalyzerVersions[0].AnalyzerId.Should().Be("npm-analyzer");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RebuildProof_OptionalDsseSignature_IsNullByDefault()
|
||||
{
|
||||
var proof = new RebuildProof
|
||||
@@ -121,7 +126,8 @@ public class RebuildProofTests
|
||||
proof.ProofHash.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RebuildProof_WithSignature_StoresSignature()
|
||||
{
|
||||
var proof = new RebuildProof
|
||||
@@ -145,7 +151,8 @@ public class RebuildProofTests
|
||||
|
||||
#region FeedSnapshot Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FeedSnapshot_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var snapshot = new FeedSnapshot
|
||||
@@ -161,7 +168,8 @@ public class RebuildProofTests
|
||||
snapshot.SnapshotHash.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FeedSnapshot_OptionalProperties_AreNullByDefault()
|
||||
{
|
||||
var snapshot = new FeedSnapshot
|
||||
@@ -180,7 +188,8 @@ public class RebuildProofTests
|
||||
|
||||
#region AnalyzerVersion Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AnalyzerVersion_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var analyzer = new AnalyzerVersion
|
||||
@@ -195,7 +204,8 @@ public class RebuildProofTests
|
||||
analyzer.Version.Should().Be("2.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AnalyzerVersion_OptionalHashes_AreNullByDefault()
|
||||
{
|
||||
var analyzer = new AnalyzerVersion
|
||||
@@ -213,7 +223,8 @@ public class RebuildProofTests
|
||||
|
||||
#region RebuildVerification Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RebuildVerification_SuccessfulRebuild_HasMatchingHash()
|
||||
{
|
||||
var proof = new RebuildProof
|
||||
@@ -242,7 +253,8 @@ public class RebuildProofTests
|
||||
verification.ErrorMessage.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RebuildVerification_FailedRebuild_HasErrorMessage()
|
||||
{
|
||||
var proof = new RebuildProof
|
||||
@@ -269,7 +281,8 @@ public class RebuildProofTests
|
||||
verification.RebuiltSbomId.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RebuildVerification_MismatchRebuild_HasDifferences()
|
||||
{
|
||||
var proof = new RebuildProof
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Emit.Lineage;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Emit.Lineage.Tests;
|
||||
|
||||
public class SbomDiffEngineTests
|
||||
@@ -25,7 +26,8 @@ public class SbomDiffEngineTests
|
||||
|
||||
#region Basic Diff Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDiff_IdenticalComponents_ReturnsNoDelta()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -46,7 +48,8 @@ public class SbomDiffEngineTests
|
||||
diff.Summary.Unchanged.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDiff_AddedComponent_DetectsAddition()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -67,7 +70,8 @@ public class SbomDiffEngineTests
|
||||
diff.Summary.Added.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDiff_RemovedComponent_DetectsRemoval()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -89,7 +93,8 @@ public class SbomDiffEngineTests
|
||||
diff.Summary.IsBreaking.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDiff_VersionUpgrade_DetectsVersionChange()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -107,7 +112,8 @@ public class SbomDiffEngineTests
|
||||
diff.Summary.IsBreaking.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDiff_VersionDowngrade_MarksAsBreaking()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -121,7 +127,8 @@ public class SbomDiffEngineTests
|
||||
diff.Summary.IsBreaking.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDiff_LicenseChange_DetectsLicenseChange()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -141,7 +148,8 @@ public class SbomDiffEngineTests
|
||||
|
||||
#region Complex Diff Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDiff_MultipleChanges_TracksAll()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -170,7 +178,8 @@ public class SbomDiffEngineTests
|
||||
diff.Summary.IsBreaking.Should().BeTrue(); // Due to removal
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDiff_EmptyFrom_AllAdditions()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -190,7 +199,8 @@ public class SbomDiffEngineTests
|
||||
diff.Summary.Unchanged.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDiff_EmptyTo_AllRemovals()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -214,7 +224,8 @@ public class SbomDiffEngineTests
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDiff_SameInputs_ProducesSameOutput()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -239,7 +250,8 @@ public class SbomDiffEngineTests
|
||||
diff1.Deltas.Should().HaveCount(diff2.Deltas.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDiff_DeltasAreSorted()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -267,7 +279,8 @@ public class SbomDiffEngineTests
|
||||
|
||||
#region CreatePointer Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreatePointer_SumsCorrectly()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -294,7 +307,8 @@ public class SbomDiffEngineTests
|
||||
pointer.DiffHash.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreatePointer_DiffHashIsDeterministic()
|
||||
{
|
||||
var fromId = SbomId.New();
|
||||
@@ -316,7 +330,8 @@ public class SbomDiffEngineTests
|
||||
|
||||
#region Summary Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiffSummary_TotalComponents_CalculatesCorrectly()
|
||||
{
|
||||
var summary = new DiffSummary
|
||||
|
||||
@@ -5,13 +5,15 @@ using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Emit.Lineage;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Emit.Lineage.Tests;
|
||||
|
||||
public class SbomLineageTests
|
||||
{
|
||||
#region SbomId Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SbomId_New_CreatesUniqueId()
|
||||
{
|
||||
var id1 = SbomId.New();
|
||||
@@ -20,7 +22,8 @@ public class SbomLineageTests
|
||||
id1.Should().NotBe(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SbomId_Parse_RoundTrips()
|
||||
{
|
||||
var original = SbomId.New();
|
||||
@@ -29,7 +32,8 @@ public class SbomLineageTests
|
||||
parsed.Should().Be(original);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SbomId_ToString_ReturnsGuidString()
|
||||
{
|
||||
var id = SbomId.New();
|
||||
@@ -42,7 +46,8 @@ public class SbomLineageTests
|
||||
|
||||
#region SbomLineage Model Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SbomLineage_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var lineage = new SbomLineage
|
||||
@@ -58,7 +63,8 @@ public class SbomLineageTests
|
||||
lineage.ContentHash.Should().Be("sha256:def456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SbomLineage_WithParent_TracksLineage()
|
||||
{
|
||||
var parentId = SbomId.New();
|
||||
@@ -78,7 +84,8 @@ public class SbomLineageTests
|
||||
child.Ancestors.Should().Contain(parentId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SbomLineage_WithDiffPointer_TracksChanges()
|
||||
{
|
||||
var diff = new SbomDiffPointer
|
||||
@@ -103,7 +110,8 @@ public class SbomLineageTests
|
||||
lineage.DiffFromParent!.TotalChanges.Should().Be(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SbomLineage_RootLineage_HasNoParent()
|
||||
{
|
||||
var root = new SbomLineage
|
||||
@@ -123,7 +131,8 @@ public class SbomLineageTests
|
||||
|
||||
#region SbomDiffPointer Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SbomDiffPointer_TotalChanges_SumsAllCategories()
|
||||
{
|
||||
var pointer = new SbomDiffPointer
|
||||
@@ -137,7 +146,8 @@ public class SbomLineageTests
|
||||
pointer.TotalChanges.Should().Be(23);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SbomDiffPointer_EmptyDiff_HasZeroChanges()
|
||||
{
|
||||
var pointer = new SbomDiffPointer
|
||||
|
||||
@@ -20,7 +20,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
_output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_CollapsesBundleExecWrapper()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -55,7 +56,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Contains(result.Edges, edge => edge.Relationship == "wrapper" && edge.FromNodeId == bundleNode.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_CollapsesDockerPhpEntrypointWrapper()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -89,7 +91,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal("docker-php-entrypoint", wrapperNode.Metadata?["wrapper.name"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_CollapsesNpmExecWrapper()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -136,7 +139,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal("npm", npmNode.Metadata?["wrapper.name"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_CollapsesYarnNodeWrapper()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -175,7 +179,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal("yarn node", yarnNode.Metadata?["wrapper.name"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_CollapsesPipenvRunWrapper()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -214,7 +219,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal("pipenv run", pipenvNode.Metadata?["wrapper.name"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_CollapsesPoetryRunWrapper()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -263,7 +269,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
return new EntryTraceAnalyzer(options, new EntryTraceMetrics(), NullLogger<EntryTraceAnalyzer>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_FollowsShellIncludeAndPythonModule()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -323,7 +330,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Contains(result.Edges, edge => edge.Relationship == "python-module" && edge.Metadata is { } metadata && metadata.TryGetValue("module", out var module) && module == "app.main");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_RecordsDiagnosticsForMissingInclude()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -353,7 +361,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal(EntryTraceUnknownReason.MissingFile, result.Diagnostics[0].Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_IsDeterministic()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -386,7 +395,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
second.Edges.Select(e => (e.FromNodeId, e.ToNodeId, e.Relationship)).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_HandlesCmdShellScript()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -413,7 +423,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Contains(result.Diagnostics, diagnostic => diagnostic.Reason == EntryTraceUnknownReason.UnsupportedSyntax);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ClassifiesGoBinaryWithPlan()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -454,7 +465,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal("/", plan.WorkingDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ExtractsJarManifestEvidence()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -497,7 +509,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal(terminal.Confidence, plan.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_UsesHistoryCandidateWhenEntrypointMissing()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -533,7 +546,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Contains("/app/server.js", terminal.Arguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_DiscoversSupervisorCommand()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -569,7 +583,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Contains("app:app", terminal.Arguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_DiscoversEntrypointScriptCandidate()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -605,7 +620,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Contains("/srv/service.py", terminal.Arguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_DiscoversServiceRunScript()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -654,7 +670,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal(EntryTraceTerminalType.Native, terminal.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_PropagatesUserSwitchWrapper()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -690,7 +707,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal("app", edge.Metadata?["user"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_PropagatesEnvWrapperIntoPlan()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -725,7 +743,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal("true", edge.Metadata?["guarded"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_AccumulatesWorkingDirectoryFromShellCd()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -756,7 +775,8 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal("/service", terminal.WorkingDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_HandlesInitShimAndGuardsEdge()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
@@ -808,6 +828,7 @@ public sealed class EntryTraceAnalyzerTests
|
||||
{
|
||||
var manifest = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var writer = new StreamWriter(manifest.Open(), Encoding.UTF8);
|
||||
using StellaOps.TestKit;
|
||||
writer.WriteLine("Manifest-Version: 1.0");
|
||||
writer.WriteLine($"Main-Class: {mainClass}");
|
||||
writer.Flush();
|
||||
|
||||
@@ -4,11 +4,13 @@ using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.EntryTrace.Tests;
|
||||
|
||||
public sealed class EntryTraceImageContextFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Create_UsesEnvironmentAndEntrypointFromConfig()
|
||||
{
|
||||
var json = """
|
||||
@@ -52,7 +54,8 @@ public sealed class EntryTraceImageContextFactoryTests
|
||||
Assert.Equal("/custom/bin:/usr/bin", string.Join(":", imageContext.Context.Path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Create_FallsBackToDefaultPathWhenMissing()
|
||||
{
|
||||
var json = """
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace StellaOps.Scanner.EntryTrace.Tests;
|
||||
|
||||
public sealed class EntryTraceNdjsonWriterTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_ProducesDeterministicNdjsonLines()
|
||||
{
|
||||
var (graph, metadata) = CreateSampleGraph();
|
||||
@@ -58,7 +59,8 @@ public sealed class EntryTraceNdjsonWriterTests
|
||||
Assert.Equal("gosu", capabilityJson.GetProperty("name").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_ProducesStableSha256Hash()
|
||||
{
|
||||
var (graph, metadata) = CreateSampleGraph();
|
||||
@@ -147,6 +149,7 @@ public sealed class EntryTraceNdjsonWriterTests
|
||||
Assert.EndsWith("\n", ndjsonLine, StringComparison.Ordinal);
|
||||
var json = ndjsonLine.TrimEnd('\n');
|
||||
using var document = JsonDocument.Parse(json);
|
||||
using StellaOps.TestKit;
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ public sealed class LayeredRootFileSystemTests : IDisposable
|
||||
Directory.CreateDirectory(_tempRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromDirectories_HandlesWhiteoutsAndResolution()
|
||||
{
|
||||
var layer1 = CreateLayerDirectory("layer1");
|
||||
@@ -65,7 +66,8 @@ public sealed class LayeredRootFileSystemTests : IDisposable
|
||||
Assert.DoesNotContain(optEntries, entry => entry.Path.EndsWith("setup.sh", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryReadBytes_ReturnsLimitedPreview()
|
||||
{
|
||||
var layer = CreateLayerDirectory("layer-bytes");
|
||||
@@ -84,7 +86,8 @@ public sealed class LayeredRootFileSystemTests : IDisposable
|
||||
Assert.Equal("abcd", Encoding.UTF8.GetString(preview.Span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromArchives_ResolvesSymlinkAndWhiteout()
|
||||
{
|
||||
var layer1Path = Path.Combine(_tempRoot, "layer1.tar");
|
||||
@@ -135,7 +138,8 @@ public sealed class LayeredRootFileSystemTests : IDisposable
|
||||
Assert.False(fs.TryReadAllText("/opt/old.sh", out _, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromArchives_ResolvesHardLinkContent()
|
||||
{
|
||||
var baseLayer = Path.Combine(_tempRoot, "base.tar");
|
||||
@@ -185,6 +189,7 @@ public sealed class LayeredRootFileSystemTests : IDisposable
|
||||
{
|
||||
using var stream = File.Create(path);
|
||||
using var writer = new TarWriter(stream, leaveOpen: false);
|
||||
using StellaOps.TestKit;
|
||||
writerAction(writer);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using StellaOps.Scanner.EntryTrace.Parsing;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.EntryTrace.Tests;
|
||||
|
||||
public sealed class ShellParserTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ProducesDeterministicNodes()
|
||||
{
|
||||
const string script = """
|
||||
|
||||
@@ -9,11 +9,13 @@ using FluentAssertions;
|
||||
using StellaOps.Scanner.Evidence.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Evidence.Tests;
|
||||
|
||||
public sealed class FuncProofBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithBinaryIdentity_SetsFileProperties()
|
||||
{
|
||||
// Arrange
|
||||
@@ -33,7 +35,8 @@ public sealed class FuncProofBuilderTests
|
||||
proof.SchemaVersion.Should().Be(FuncProofConstants.SchemaVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithSection_AddsSectionToProof()
|
||||
{
|
||||
// Arrange
|
||||
@@ -56,7 +59,8 @@ public sealed class FuncProofBuilderTests
|
||||
proof.Sections![0].Hash.Should().Be(sectionHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithMultipleSections_AddsAllSections()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -74,7 +78,8 @@ public sealed class FuncProofBuilderTests
|
||||
proof.Sections![2].Name.Should().Be(".data");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithFunction_AddsFunctionToProof()
|
||||
{
|
||||
// Arrange
|
||||
@@ -99,7 +104,8 @@ public sealed class FuncProofBuilderTests
|
||||
proof.Functions![0].Hash.Should().Be(functionHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithFunctionCallers_SetsCallersOnFunction()
|
||||
{
|
||||
// Arrange
|
||||
@@ -115,7 +121,8 @@ public sealed class FuncProofBuilderTests
|
||||
proof.Functions![0].Callers.Should().BeEquivalentTo(callers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithTrace_AddsTraceToProof()
|
||||
{
|
||||
// Arrange
|
||||
@@ -135,7 +142,8 @@ public sealed class FuncProofBuilderTests
|
||||
proof.Traces![0].Truncated.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithTruncatedTrace_SetsTruncatedFlag()
|
||||
{
|
||||
// Arrange
|
||||
@@ -151,7 +159,8 @@ public sealed class FuncProofBuilderTests
|
||||
proof.Traces![0].Truncated.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithMetadata_SetsMetadataProperties()
|
||||
{
|
||||
// Arrange
|
||||
@@ -172,7 +181,8 @@ public sealed class FuncProofBuilderTests
|
||||
proof.Metadata.CreatedAt.Should().Be(timestamp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_GeneratesProofId()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -186,7 +196,8 @@ public sealed class FuncProofBuilderTests
|
||||
proof.ProofId.Should().HaveLength(64); // SHA-256 hex
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_SameInput_GeneratesSameProofId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -205,7 +216,8 @@ public sealed class FuncProofBuilderTests
|
||||
proof1.ProofId.Should().Be(proof2.ProofId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_DifferentInput_GeneratesDifferentProofId()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -223,7 +235,8 @@ public sealed class FuncProofBuilderTests
|
||||
proof1.ProofId.Should().NotBe(proof2.ProofId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeSymbolDigest_DeterministicForSameInput()
|
||||
{
|
||||
// Arrange
|
||||
@@ -238,7 +251,8 @@ public sealed class FuncProofBuilderTests
|
||||
digest1.Should().Be(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeSymbolDigest_DifferentForDifferentOffset()
|
||||
{
|
||||
// Arrange
|
||||
@@ -252,7 +266,8 @@ public sealed class FuncProofBuilderTests
|
||||
digest1.Should().NotBe(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeFunctionHash_DeterministicForSameInput()
|
||||
{
|
||||
// Arrange
|
||||
@@ -266,7 +281,8 @@ public sealed class FuncProofBuilderTests
|
||||
hash1.Should().Be(hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeFunctionHash_DifferentForDifferentInput()
|
||||
{
|
||||
// Arrange
|
||||
@@ -281,7 +297,8 @@ public sealed class FuncProofBuilderTests
|
||||
hash1.Should().NotBe(hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeProofId_DeterministicForSameProof()
|
||||
{
|
||||
// Arrange
|
||||
@@ -298,7 +315,8 @@ public sealed class FuncProofBuilderTests
|
||||
id1.Should().Be(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_FunctionOrdering_IsDeterministic()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
@@ -15,6 +15,7 @@ using StellaOps.Scanner.Evidence.Models;
|
||||
using StellaOps.Scanner.ProofSpine;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Evidence.Tests;
|
||||
|
||||
public sealed class FuncProofDsseServiceTests
|
||||
@@ -34,7 +35,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
_logger = NullLogger<FuncProofDsseService>.Instance;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAsync_WithValidProof_ReturnsSignedEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -64,7 +66,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
result.EnvelopeJson.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAsync_WithNullProofId_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -81,7 +84,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => service.SignAsync(proof));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAsync_CallsSigningServiceWithCorrectPayloadType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -109,7 +113,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
capturedPayloadType.Should().Be(FuncProofConstants.MediaType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithValidEnvelope_ReturnsSuccessResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -136,7 +141,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
result.FuncProof.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithWrongPayloadType_ReturnsInvalidResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -155,7 +161,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
result.FailureReason.Should().Contain("Invalid payload type");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithFailedSignature_ReturnsInvalidResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -180,7 +187,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
result.FailureReason.Should().Be("dsse_sig_mismatch");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractPayload_WithValidEnvelope_ReturnsFuncProof()
|
||||
{
|
||||
// Arrange
|
||||
@@ -205,7 +213,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
extracted.BuildId.Should().Be(proof.BuildId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractPayload_WithInvalidBase64_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -223,7 +232,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
extracted.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractPayload_WithInvalidJson_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -241,7 +251,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
extracted.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToUnsignedEnvelope_CreatesValidEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -257,7 +268,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
envelope.Payload.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseEnvelope_WithValidJson_ReturnsEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -278,7 +290,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
parsed!.PayloadType.Should().Be("test-type");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseEnvelope_WithInvalidJson_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
@@ -288,7 +301,8 @@ public sealed class FuncProofDsseServiceTests
|
||||
parsed.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseEnvelope_WithEmptyString_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
|
||||
@@ -14,6 +14,7 @@ using StellaOps.Scanner.Evidence;
|
||||
using StellaOps.Scanner.Evidence.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Evidence.Tests;
|
||||
|
||||
public sealed class SbomFuncProofLinkerTests
|
||||
@@ -25,7 +26,8 @@ public sealed class SbomFuncProofLinkerTests
|
||||
_linker = new SbomFuncProofLinker(NullLogger<SbomFuncProofLinker>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_AddsEvidenceToComponent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -58,7 +60,8 @@ public sealed class SbomFuncProofLinkerTests
|
||||
frames!.Count.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_AddsExternalReference()
|
||||
{
|
||||
// Arrange
|
||||
@@ -86,7 +89,8 @@ public sealed class SbomFuncProofLinkerTests
|
||||
evidenceRef["url"]!.GetValue<string>().Should().Contain("oci://");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_ThrowsForNonCycloneDx()
|
||||
{
|
||||
// Arrange
|
||||
@@ -111,7 +115,8 @@ public sealed class SbomFuncProofLinkerTests
|
||||
.WithMessage("*CycloneDX*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_ThrowsForMissingComponent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -130,7 +135,8 @@ public sealed class SbomFuncProofLinkerTests
|
||||
.WithMessage("*not found*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractFuncProofReferences_ReturnsEmptyForNoEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -143,7 +149,8 @@ public sealed class SbomFuncProofLinkerTests
|
||||
refs.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractFuncProofReferences_FindsLinkedEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -167,7 +174,8 @@ public sealed class SbomFuncProofLinkerTests
|
||||
refs[0].FunctionCount.Should().Be(funcProof.Functions.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateEvidenceRef_PopulatesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -189,7 +197,8 @@ public sealed class SbomFuncProofLinkerTests
|
||||
evidenceRef.TraceCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_IncludesProofProperties()
|
||||
{
|
||||
// Arrange
|
||||
@@ -224,7 +233,8 @@ public sealed class SbomFuncProofLinkerTests
|
||||
proofIdProperty!["value"]!.GetValue<string>().Should().Be(funcProof.ProofId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_MergesWithExistingEvidence()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -6,6 +6,7 @@ using StellaOps.Scanner.Explainability.Assumptions;
|
||||
using StellaOps.Scanner.Explainability.Confidence;
|
||||
using StellaOps.Scanner.Explainability.Falsifiability;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Explainability.Tests;
|
||||
|
||||
public class RiskReportTests
|
||||
@@ -17,7 +18,8 @@ public class RiskReportTests
|
||||
_generator = new RiskReportGenerator(new EvidenceDensityScorer());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generate_MinimalInput_CreatesReport()
|
||||
{
|
||||
var input = new RiskReportInput
|
||||
@@ -39,7 +41,8 @@ public class RiskReportTests
|
||||
result.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generate_WithSeverity_IncludesInExplanation()
|
||||
{
|
||||
var input = new RiskReportInput
|
||||
@@ -56,7 +59,8 @@ public class RiskReportTests
|
||||
result.Explanation.Should().Contain("CRITICAL");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generate_WithFixedVersion_RecommendsUpdate()
|
||||
{
|
||||
var input = new RiskReportInput
|
||||
@@ -74,7 +78,8 @@ public class RiskReportTests
|
||||
a.Action.Contains("Update") && a.Action.Contains("1.0.1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generate_WithoutFixedVersion_RecommendsMonitoring()
|
||||
{
|
||||
var input = new RiskReportInput
|
||||
@@ -91,7 +96,8 @@ public class RiskReportTests
|
||||
a.Action.Contains("Monitor") || a.Action.Contains("compensating"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generate_WithEvidenceFactors_CalculatesConfidence()
|
||||
{
|
||||
var input = new RiskReportInput
|
||||
@@ -113,7 +119,8 @@ public class RiskReportTests
|
||||
result.ConfidenceScore!.Score.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generate_WithAssumptions_IncludesInReport()
|
||||
{
|
||||
var assumptions = new AssumptionSet
|
||||
@@ -147,7 +154,8 @@ public class RiskReportTests
|
||||
result.DetailedNarrative.Should().Contain("Assumptions");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generate_WithFalsifiability_IncludesInReport()
|
||||
{
|
||||
var falsifiability = new FalsifiabilityCriteria
|
||||
@@ -183,7 +191,8 @@ public class RiskReportTests
|
||||
result.Explanation.Should().Contain("falsified");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generate_WithUnvalidatedAssumptions_RecommendsValidation()
|
||||
{
|
||||
var assumptions = new AssumptionSet
|
||||
@@ -212,7 +221,8 @@ public class RiskReportTests
|
||||
a.Action.Contains("Validate") || a.Action.Contains("assumption"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generate_WithPartiallyEvaluatedFalsifiability_RecommendsCompletion()
|
||||
{
|
||||
var falsifiability = new FalsifiabilityCriteria
|
||||
@@ -242,7 +252,8 @@ public class RiskReportTests
|
||||
a.Action.Contains("falsifiability") || a.Action.Contains("evaluation"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RecommendedAction_HasRequiredProperties()
|
||||
{
|
||||
var action = new RecommendedAction(
|
||||
@@ -257,7 +268,8 @@ public class RiskReportTests
|
||||
action.Effort.Should().Be(EffortLevel.Low);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(EffortLevel.Low)]
|
||||
[InlineData(EffortLevel.Medium)]
|
||||
[InlineData(EffortLevel.High)]
|
||||
|
||||
@@ -41,7 +41,8 @@ public class PoEPipelineTests : IDisposable
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScanWithVulnerability_GeneratesPoE_StoresInCas()
|
||||
{
|
||||
// Arrange
|
||||
@@ -98,7 +99,8 @@ public class PoEPipelineTests : IDisposable
|
||||
Assert.Equal(dsseBytes, artifact.DsseBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScanWithUnreachableVuln_DoesNotGeneratePoE()
|
||||
{
|
||||
// Arrange
|
||||
@@ -124,7 +126,8 @@ public class PoEPipelineTests : IDisposable
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PoEGeneration_ProducesDeterministicHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -141,7 +144,8 @@ public class PoEPipelineTests : IDisposable
|
||||
Assert.StartsWith("blake3:", hash1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PoEStorage_PersistsToCas_RetrievesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -201,6 +205,7 @@ public class PoEPipelineTests : IDisposable
|
||||
{
|
||||
// Using SHA256 as BLAKE3 placeholder
|
||||
using var sha = SHA256.Create();
|
||||
using StellaOps.TestKit;
|
||||
var hashBytes = sha.ComputeHash(data);
|
||||
var hashHex = Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||
return $"blake3:{hashHex}";
|
||||
|
||||
@@ -20,7 +20,8 @@ public sealed class PostgresProofSpineRepositoryTests
|
||||
public PostgresProofSpineRepositoryTests(ScannerProofSpinePostgresFixture fixture)
|
||||
=> _fixture = fixture;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_ThenGetByIdAsync_RoundTripsSpine()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
@@ -37,6 +38,7 @@ public sealed class PostgresProofSpineRepositoryTests
|
||||
};
|
||||
|
||||
await using var dataSource = new ScannerDataSource(Options.Create(options), NullLogger<ScannerDataSource>.Instance);
|
||||
using StellaOps.TestKit;
|
||||
var repository = new PostgresProofSpineRepository(
|
||||
dataSource,
|
||||
NullLogger<PostgresProofSpineRepository>.Instance,
|
||||
|
||||
@@ -5,11 +5,13 @@ using StellaOps.Scanner.ProofSpine;
|
||||
using StellaOps.Scanner.ProofSpine.Options;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.ProofSpine.Tests;
|
||||
|
||||
public sealed class ProofSpineBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildAsync_SameInputs_ProducesSameIds()
|
||||
{
|
||||
var options = Options.Create(new ProofSpineDsseSigningOptions
|
||||
@@ -62,7 +64,8 @@ public sealed class ProofSpineBuilderTests
|
||||
Assert.Equal(spine1.Segments[1].SegmentId, spine2.Segments[1].SegmentId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_DetectsTampering()
|
||||
{
|
||||
var options = Options.Create(new ProofSpineDsseSigningOptions
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Queue;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Queue.Tests;
|
||||
|
||||
public sealed class QueueLeaseIntegrationTests
|
||||
@@ -22,7 +23,8 @@ public sealed class QueueLeaseIntegrationTests
|
||||
DefaultLeaseDuration = TimeSpan.FromSeconds(5)
|
||||
};
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Enqueue_ShouldDeduplicate_ByIdempotencyKey()
|
||||
{
|
||||
var clock = new FakeTimeProvider();
|
||||
@@ -41,7 +43,8 @@ public sealed class QueueLeaseIntegrationTests
|
||||
second.Deduplicated.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Lease_ShouldExposeTraceId_FromQueuedMessage()
|
||||
{
|
||||
var clock = new FakeTimeProvider();
|
||||
@@ -60,7 +63,8 @@ public sealed class QueueLeaseIntegrationTests
|
||||
lease!.TraceId.Should().Be("trace-123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Lease_Acknowledge_ShouldRemoveFromQueue()
|
||||
{
|
||||
var clock = new FakeTimeProvider();
|
||||
@@ -78,7 +82,8 @@ public sealed class QueueLeaseIntegrationTests
|
||||
afterAck.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Release_WithRetry_ShouldDeadLetterAfterMaxAttempts()
|
||||
{
|
||||
var clock = new FakeTimeProvider();
|
||||
@@ -98,7 +103,8 @@ public sealed class QueueLeaseIntegrationTests
|
||||
queue.DeadLetters.Should().ContainSingle(dead => dead.JobId == "job-retry");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Retry_ShouldIncreaseAttemptOnNextLease()
|
||||
{
|
||||
var clock = new FakeTimeProvider();
|
||||
|
||||
@@ -5,6 +5,7 @@ using FluentAssertions;
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
using StellaOps.Scanner.Reachability.Stack;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Stack.Tests;
|
||||
|
||||
public class ReachabilityStackEvaluatorTests
|
||||
@@ -42,7 +43,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
|
||||
#region Verdict Truth Table Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeriveVerdict_AllThreeConfirmReachable_ReturnsExploitable()
|
||||
{
|
||||
// L1=Reachable, L2=Resolved, L3=NotGated -> Exploitable
|
||||
@@ -55,7 +57,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
verdict.Should().Be(ReachabilityVerdict.Exploitable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeriveVerdict_L1L2ConfirmL3Unknown_ReturnsLikelyExploitable()
|
||||
{
|
||||
// L1=Reachable, L2=Resolved, L3=Unknown -> LikelyExploitable
|
||||
@@ -68,7 +71,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
verdict.Should().Be(ReachabilityVerdict.LikelyExploitable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeriveVerdict_L1L2ConfirmL3Conditional_ReturnsLikelyExploitable()
|
||||
{
|
||||
// L1=Reachable, L2=Resolved, L3=Conditional -> LikelyExploitable
|
||||
@@ -81,7 +85,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
verdict.Should().Be(ReachabilityVerdict.LikelyExploitable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeriveVerdict_L1ReachableL2NotResolved_ReturnsUnreachable()
|
||||
{
|
||||
// L1=Reachable, L2=NotResolved (confirmed) -> Unreachable
|
||||
@@ -94,7 +99,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
verdict.Should().Be(ReachabilityVerdict.Unreachable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeriveVerdict_L1NotReachable_ReturnsUnreachable()
|
||||
{
|
||||
// L1=NotReachable (confirmed) -> Unreachable
|
||||
@@ -107,7 +113,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
verdict.Should().Be(ReachabilityVerdict.Unreachable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeriveVerdict_L3Blocked_ReturnsUnreachable()
|
||||
{
|
||||
// L1=Reachable, L2=Resolved, L3=Blocked (confirmed) -> Unreachable
|
||||
@@ -120,7 +127,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
verdict.Should().Be(ReachabilityVerdict.Unreachable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeriveVerdict_L1ReachableL2LowConfidence_ReturnsPossiblyExploitable()
|
||||
{
|
||||
// L1=Reachable, L2=Unknown (low confidence) -> PossiblyExploitable
|
||||
@@ -133,7 +141,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
verdict.Should().Be(ReachabilityVerdict.PossiblyExploitable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeriveVerdict_L1LowConfidenceNoData_ReturnsUnknown()
|
||||
{
|
||||
// L1=Unknown (low confidence, no paths) -> Unknown
|
||||
@@ -155,7 +164,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
|
||||
#region Evaluate Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Evaluate_CreatesCompleteStack()
|
||||
{
|
||||
var symbol = CreateTestSymbol();
|
||||
@@ -176,7 +186,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
stack.Explanation.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Evaluate_ExploitableVerdict_ExplanationContainsAllThreeLayers()
|
||||
{
|
||||
var symbol = CreateTestSymbol();
|
||||
@@ -192,7 +203,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
stack.Explanation.Should().Contain("exploitable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Evaluate_UnreachableVerdict_ExplanationMentionsBlocking()
|
||||
{
|
||||
var symbol = CreateTestSymbol();
|
||||
@@ -210,7 +222,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
|
||||
#region Model Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VulnerableSymbol_StoresAllProperties()
|
||||
{
|
||||
var symbol = new VulnerableSymbol(
|
||||
@@ -228,7 +241,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
symbol.Type.Should().Be(SymbolType.Function);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(SymbolType.Function)]
|
||||
[InlineData(SymbolType.Method)]
|
||||
[InlineData(SymbolType.JavaMethod)]
|
||||
@@ -242,7 +256,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
symbol.Type.Should().Be(type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(ReachabilityVerdict.Exploitable)]
|
||||
[InlineData(ReachabilityVerdict.LikelyExploitable)]
|
||||
[InlineData(ReachabilityVerdict.PossiblyExploitable)]
|
||||
@@ -254,7 +269,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
Enum.IsDefined(typeof(ReachabilityVerdict), verdict).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(GatingOutcome.NotGated)]
|
||||
[InlineData(GatingOutcome.Blocked)]
|
||||
[InlineData(GatingOutcome.Conditional)]
|
||||
@@ -265,7 +281,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
layer3.Outcome.Should().Be(outcome);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GatingCondition_StoresAllProperties()
|
||||
{
|
||||
var condition = new GatingCondition(
|
||||
@@ -284,7 +301,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
condition.Status.Should().Be(GatingStatus.Disabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(GatingType.FeatureFlag)]
|
||||
[InlineData(GatingType.EnvironmentVariable)]
|
||||
[InlineData(GatingType.ConfigurationValue)]
|
||||
@@ -299,7 +317,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
condition.Type.Should().Be(type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CallPath_WithSites_StoresCorrectly()
|
||||
{
|
||||
var entrypoint = new Entrypoint("Main", EntrypointType.Main, "Program.cs", "Application entry");
|
||||
@@ -324,7 +343,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
path.HasConditionals.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SymbolResolution_StoresDetails()
|
||||
{
|
||||
var resolution = new SymbolResolution(
|
||||
@@ -341,7 +361,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
resolution.Method.Should().Be(ResolutionMethod.DirectLink);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(ResolutionMethod.DirectLink)]
|
||||
[InlineData(ResolutionMethod.DynamicLoad)]
|
||||
[InlineData(ResolutionMethod.DelayLoad)]
|
||||
@@ -353,7 +374,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
resolution.Method.Should().Be(method);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoaderRule_StoresProperties()
|
||||
{
|
||||
var rule = new LoaderRule(
|
||||
@@ -371,7 +393,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
|
||||
#region Edge Case Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeriveVerdict_L3BlockedButLowConfidence_DoesNotBlock()
|
||||
{
|
||||
// L3 blocked but low confidence should not definitively block
|
||||
@@ -385,7 +408,8 @@ public class ReachabilityStackEvaluatorTests
|
||||
verdict.Should().Be(ReachabilityVerdict.Exploitable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeriveVerdict_AllLayersHighConfidence_ExploitableIsDefinitive()
|
||||
{
|
||||
var layer1 = CreateLayer1(isReachable: true, ConfidenceLevel.Verified);
|
||||
|
||||
@@ -38,7 +38,8 @@ public class AttestingRichGraphWriterTests : IAsyncLifetime
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteWithAttestationAsync_WhenEnabled_ProducesAttestationFile()
|
||||
{
|
||||
// Arrange
|
||||
@@ -80,7 +81,8 @@ public class AttestingRichGraphWriterTests : IAsyncLifetime
|
||||
Assert.NotEmpty(result.WitnessResult.StatementHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteWithAttestationAsync_WhenDisabled_NoAttestationFile()
|
||||
{
|
||||
// Arrange
|
||||
@@ -118,7 +120,8 @@ public class AttestingRichGraphWriterTests : IAsyncLifetime
|
||||
Assert.Null(result.WitnessResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteWithAttestationAsync_AttestationContainsValidDsse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -159,7 +162,8 @@ public class AttestingRichGraphWriterTests : IAsyncLifetime
|
||||
Assert.Contains("payload", dsseJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteWithAttestationAsync_GraphHashIsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -269,6 +273,7 @@ public class AttestingRichGraphWriterTests : IAsyncLifetime
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var buffer = new MemoryStream();
|
||||
using StellaOps.TestKit;
|
||||
await stream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
return System.Security.Cryptography.SHA256.HashData(buffer.ToArray());
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class BinaryReachabilityLifterTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EmitsSymbolAndCodeIdForBinary()
|
||||
{
|
||||
using var temp = new TempDir();
|
||||
@@ -48,7 +49,8 @@ public class BinaryReachabilityLifterTests
|
||||
Assert.Equal(expectedCodeId, richNode.CodeId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EmitsEntryPointForElfWithNonZeroEntryAddress()
|
||||
{
|
||||
using var temp = new TempDir();
|
||||
@@ -84,7 +86,8 @@ public class BinaryReachabilityLifterTests
|
||||
Assert.NotNull(entryEdge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EmitsPurlForLibrary()
|
||||
{
|
||||
using var temp = new TempDir();
|
||||
@@ -110,7 +113,8 @@ public class BinaryReachabilityLifterTests
|
||||
Assert.Equal("pkg:generic/libssl@3", node.Attributes["purl"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DoesNotEmitEntryPointForElfWithZeroEntry()
|
||||
{
|
||||
using var temp = new TempDir();
|
||||
@@ -135,7 +139,8 @@ public class BinaryReachabilityLifterTests
|
||||
Assert.DoesNotContain(graph.Nodes, n => n.Kind == "entry_point");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EmitsUnknownsForElfUndefinedDynsymSymbols()
|
||||
{
|
||||
using var temp = new TempDir();
|
||||
@@ -168,7 +173,8 @@ public class BinaryReachabilityLifterTests
|
||||
e.To == unknownNode.SymbolId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RichGraphIncludesPurlAndSymbolDigestForElfDependencies()
|
||||
{
|
||||
using var temp = new TempDir();
|
||||
@@ -196,7 +202,8 @@ public class BinaryReachabilityLifterTests
|
||||
Assert.StartsWith("sha256:", edge.SymbolDigest, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RichGraphIncludesPurlAndSymbolDigestForPeImports()
|
||||
{
|
||||
using var temp = new TempDir();
|
||||
@@ -374,6 +381,7 @@ public class BinaryReachabilityLifterTests
|
||||
using var ms = new MemoryStream();
|
||||
using var writer = new BinaryWriter(ms);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var stringTable = new StringBuilder();
|
||||
stringTable.Append('\0');
|
||||
var stringOffsets = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
|
||||
@@ -3,11 +3,13 @@ using System.Linq;
|
||||
using StellaOps.Scanner.Reachability.Ordering;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public sealed class DeterministicGraphOrdererTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_IsDeterministic_AcrossInputOrdering()
|
||||
{
|
||||
var orderer = new DeterministicGraphOrderer();
|
||||
@@ -33,7 +35,8 @@ public sealed class DeterministicGraphOrdererTests
|
||||
canonical2.Edges.Select(e => (e.SourceIndex, e.TargetIndex, e.EdgeType)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TopologicalLexicographic_UsesLexicographicTiebreakers()
|
||||
{
|
||||
var orderer = new DeterministicGraphOrderer();
|
||||
@@ -47,7 +50,8 @@ public sealed class DeterministicGraphOrdererTests
|
||||
Assert.Equal(new[] { "A", "B", "C" }, order);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TopologicalLexicographic_HandlesCyclesByAppendingRemainder()
|
||||
{
|
||||
var orderer = new DeterministicGraphOrderer();
|
||||
@@ -61,7 +65,8 @@ public sealed class DeterministicGraphOrdererTests
|
||||
Assert.Equal(new[] { "C", "A", "B" }, order);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BreadthFirstLexicographic_TraversesFromAnchors()
|
||||
{
|
||||
var orderer = new DeterministicGraphOrderer();
|
||||
@@ -75,7 +80,8 @@ public sealed class DeterministicGraphOrdererTests
|
||||
Assert.Equal(new[] { "A", "B", "C", "D" }, order);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DepthFirstLexicographic_TraversesFromAnchors()
|
||||
{
|
||||
var orderer = new DeterministicGraphOrderer();
|
||||
@@ -89,7 +95,8 @@ public sealed class DeterministicGraphOrdererTests
|
||||
Assert.Equal(new[] { "A", "B", "D", "C" }, order);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void OrderEdges_SortsByNodeOrderThenKind()
|
||||
{
|
||||
var orderer = new DeterministicGraphOrderer();
|
||||
|
||||
@@ -5,13 +5,15 @@ using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class EdgeBundleTests
|
||||
{
|
||||
private const string TestGraphHash = "blake3:abc123def456";
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EdgeBundle_Canonical_SortsEdgesDeterministically()
|
||||
{
|
||||
// Arrange - create bundle with unsorted edges
|
||||
@@ -37,7 +39,8 @@ public class EdgeBundleTests
|
||||
Assert.Equal("func_a", canonical.Edges[2].To);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EdgeBundle_ComputeContentHash_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -59,7 +62,8 @@ public class EdgeBundleTests
|
||||
Assert.StartsWith("sha256:", hash1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EdgeBundle_ComputeContentHash_DiffersWithDifferentEdges()
|
||||
{
|
||||
// Arrange
|
||||
@@ -83,7 +87,8 @@ public class EdgeBundleTests
|
||||
Assert.NotEqual(hash1, hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EdgeBundleBuilder_EnforcesMaxEdgeLimit()
|
||||
{
|
||||
// Arrange
|
||||
@@ -100,7 +105,8 @@ public class EdgeBundleTests
|
||||
builder.AddEdge(new BundledEdge("func_overflow", "func_target", "call", EdgeReason.RuntimeHit, false, 0.9, null, null, null)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EdgeBundleBuilder_Build_CreatesDeterministicBundleId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -119,7 +125,8 @@ public class EdgeBundleTests
|
||||
Assert.StartsWith("bundle:", bundle1.BundleId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BundledEdge_Trimmed_NormalizesValues()
|
||||
{
|
||||
// Arrange
|
||||
@@ -147,7 +154,8 @@ public class EdgeBundleTests
|
||||
Assert.Equal("cas://evidence/123", trimmed.Evidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BundledEdge_Trimmed_HandlesNullableFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -179,7 +187,8 @@ public class EdgeBundleExtractorTests
|
||||
return new RichGraph(nodes, edges.ToList(), new List<RichGraphRoot>(), new RichGraphAnalyzer("test", "1.0", null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractContestedBundle_ReturnsLowConfidenceEdges()
|
||||
{
|
||||
// Arrange
|
||||
@@ -201,7 +210,8 @@ public class EdgeBundleExtractorTests
|
||||
Assert.All(bundle.Edges, e => Assert.Equal(EdgeReason.LowConfidence, e.Reason));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractContestedBundle_ReturnsNullWhenNoLowConfidenceEdges()
|
||||
{
|
||||
// Arrange
|
||||
@@ -219,7 +229,8 @@ public class EdgeBundleExtractorTests
|
||||
Assert.Null(bundle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractThirdPartyBundle_ReturnsEdgesWithPurl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -242,7 +253,8 @@ public class EdgeBundleExtractorTests
|
||||
Assert.All(bundle.Edges, e => Assert.Equal(EdgeReason.ThirdPartyCall, e.Reason));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractRevokedBundle_ReturnsEdgesToRevokedTargets()
|
||||
{
|
||||
// Arrange
|
||||
@@ -269,7 +281,8 @@ public class EdgeBundleExtractorTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractRuntimeHitsBundle_ReturnsProvidedEdges()
|
||||
{
|
||||
// Arrange
|
||||
@@ -289,7 +302,8 @@ public class EdgeBundleExtractorTests
|
||||
Assert.All(bundle.Edges, e => Assert.Equal(EdgeReason.RuntimeHit, e.Reason));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractRuntimeHitsBundle_ReturnsNullForEmptyList()
|
||||
{
|
||||
// Act
|
||||
@@ -299,7 +313,8 @@ public class EdgeBundleExtractorTests
|
||||
Assert.Null(bundle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractInitArrayBundle_ReturnsEdgesFromInitRoots()
|
||||
{
|
||||
// Arrange
|
||||
@@ -334,7 +349,8 @@ public class EdgeBundlePublisherTests
|
||||
{
|
||||
private const string TestGraphHash = "blake3:abc123def456";
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_StoresBundleAndDsseInCas()
|
||||
{
|
||||
// Arrange
|
||||
@@ -365,7 +381,8 @@ public class EdgeBundlePublisherTests
|
||||
Assert.EndsWith(".dsse", result.DsseCasUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_DsseContainsValidPayload()
|
||||
{
|
||||
// Arrange
|
||||
@@ -397,7 +414,8 @@ public class EdgeBundlePublisherTests
|
||||
Assert.Single(signatures.EnumerateArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_BundleJsonContainsAllFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -436,7 +454,8 @@ public class EdgeBundlePublisherTests
|
||||
Assert.Equal("pkg:npm/test@1.0.0", edge.GetProperty("purl").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_CasPathFollowsContract()
|
||||
{
|
||||
// Arrange
|
||||
@@ -459,7 +478,8 @@ public class EdgeBundlePublisherTests
|
||||
Assert.EndsWith(".dsse", result.DsseCasUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_ProducesDeterministicResults()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Scanner.Reachability.Gates;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -9,7 +10,8 @@ namespace StellaOps.Scanner.Reachability.Tests;
|
||||
/// </summary>
|
||||
public sealed class GateDetectionTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GateDetectionResult_Empty_HasNoGates()
|
||||
{
|
||||
Assert.False(GateDetectionResult.Empty.HasGates);
|
||||
@@ -17,7 +19,8 @@ public sealed class GateDetectionTests
|
||||
Assert.Null(GateDetectionResult.Empty.PrimaryGate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GateDetectionResult_WithGates_HasPrimaryGate()
|
||||
{
|
||||
var gates = new[]
|
||||
@@ -33,7 +36,8 @@ public sealed class GateDetectionTests
|
||||
Assert.Equal(GateType.FeatureFlag, result.PrimaryGate?.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GateMultiplierConfig_Default_HasExpectedValues()
|
||||
{
|
||||
var config = GateMultiplierConfig.Default;
|
||||
@@ -45,7 +49,8 @@ public sealed class GateDetectionTests
|
||||
Assert.Equal(500, config.MinimumMultiplierBps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_NoDetectors_ReturnsEmpty()
|
||||
{
|
||||
var detector = new CompositeGateDetector([]);
|
||||
@@ -57,7 +62,8 @@ public sealed class GateDetectionTests
|
||||
Assert.Equal(10000, result.CombinedMultiplierBps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_EmptyCallPath_ReturnsEmpty()
|
||||
{
|
||||
var detector = new CompositeGateDetector([new MockAuthDetector()]);
|
||||
@@ -68,7 +74,8 @@ public sealed class GateDetectionTests
|
||||
Assert.False(result.HasGates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_SingleGate_AppliesMultiplier()
|
||||
{
|
||||
var authDetector = new MockAuthDetector(
|
||||
@@ -83,7 +90,8 @@ public sealed class GateDetectionTests
|
||||
Assert.Equal(3000, result.CombinedMultiplierBps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_MultipleGateTypes_MultipliesMultipliers()
|
||||
{
|
||||
var authDetector = new MockAuthDetector(
|
||||
@@ -101,7 +109,8 @@ public sealed class GateDetectionTests
|
||||
Assert.Equal(600, result.CombinedMultiplierBps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_DuplicateGates_Deduplicates()
|
||||
{
|
||||
var authDetector1 = new MockAuthDetector(
|
||||
@@ -118,7 +127,8 @@ public sealed class GateDetectionTests
|
||||
Assert.Equal(0.9, result.Gates[0].Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_AllGateTypes_AppliesMinimumFloor()
|
||||
{
|
||||
var detectors = new IGateDetector[]
|
||||
@@ -138,7 +148,8 @@ public sealed class GateDetectionTests
|
||||
Assert.Equal(500, result.CombinedMultiplierBps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_DetectorException_ContinuesWithOthers()
|
||||
{
|
||||
var failingDetector = new FailingGateDetector();
|
||||
|
||||
@@ -9,6 +9,7 @@ using StellaOps.Scanner.Reachability.Boundary;
|
||||
using StellaOps.Scanner.Reachability.Gates;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class GatewayBoundaryExtractorTests
|
||||
@@ -23,13 +24,15 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region Priority and CanHandle
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Priority_Returns250_HigherThanK8sExtractor()
|
||||
{
|
||||
Assert.Equal(250, _extractor.Priority);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("gateway", true)]
|
||||
[InlineData("kong", true)]
|
||||
[InlineData("Kong", true)]
|
||||
@@ -45,7 +48,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal(expected, _extractor.CanHandle(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_WithKongAnnotations_ReturnsTrue()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty with
|
||||
@@ -59,7 +63,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.True(_extractor.CanHandle(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_WithIstioAnnotations_ReturnsTrue()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty with
|
||||
@@ -73,7 +78,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.True(_extractor.CanHandle(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_WithTraefikAnnotations_ReturnsTrue()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty with
|
||||
@@ -87,7 +93,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.True(_extractor.CanHandle(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_WithEmptyAnnotations_ReturnsFalse()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty;
|
||||
@@ -98,7 +105,8 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region Gateway Type Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithKongSource_ReturnsKongGatewaySource()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -113,7 +121,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("gateway:kong", result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithEnvoySource_ReturnsEnvoyGatewaySource()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "envoy", null);
|
||||
@@ -128,7 +137,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("gateway:envoy", result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithIstioAnnotations_ReturnsEnvoyGatewaySource()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "gateway", null);
|
||||
@@ -147,7 +157,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("gateway:envoy", result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithApiGatewaySource_ReturnsAwsApigwSource()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "apigateway", null);
|
||||
@@ -166,7 +177,8 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region Exposure Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_DefaultGateway_ReturnsPublicExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -184,7 +196,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.True(result.Exposure.BehindProxy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithInternalFlag_ReturnsInternalExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -205,7 +218,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.False(result.Exposure.InternetFacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithIstioMesh_ReturnsInternalExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "envoy", null);
|
||||
@@ -226,7 +240,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.False(result.Exposure.InternetFacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithAwsPrivateEndpoint_ReturnsInternalExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "apigateway", null);
|
||||
@@ -251,7 +266,8 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region Surface Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithKongPath_ReturnsSurfaceWithPath()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -272,7 +288,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("api", result.Surface.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithKongHost_ReturnsSurfaceWithHost()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -292,7 +309,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("api.example.com", result.Surface.Host);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithGrpcAnnotation_ReturnsGrpcProtocol()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -312,7 +330,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("grpc", result.Surface.Protocol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithWebsocketAnnotation_ReturnsWssProtocol()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -332,7 +351,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("wss", result.Surface.Protocol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_DefaultProtocol_ReturnsHttps()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -353,7 +373,8 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region Kong Auth Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithKongJwtPlugin_ReturnsJwtAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -374,7 +395,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("jwt", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithKongKeyAuth_ReturnsApiKeyAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -395,7 +417,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("api_key", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithKongAcl_ReturnsRoles()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -422,7 +445,8 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region Envoy/Istio Auth Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithIstioJwt_ReturnsJwtAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "envoy", null);
|
||||
@@ -443,7 +467,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("jwt", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithIstioMtls_ReturnsMtlsAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "envoy", null);
|
||||
@@ -464,7 +489,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("mtls", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithEnvoyOidc_ReturnsOAuth2Auth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "envoy", null);
|
||||
@@ -490,7 +516,8 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region AWS API Gateway Auth Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithCognitoAuthorizer_ReturnsOAuth2Auth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "apigateway", null);
|
||||
@@ -512,7 +539,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("cognito", result.Auth.Provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithApiKeyRequired_ReturnsApiKeyAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "apigateway", null);
|
||||
@@ -533,7 +561,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("api_key", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithLambdaAuthorizer_ReturnsCustomAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "apigateway", null);
|
||||
@@ -555,7 +584,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("lambda", result.Auth.Provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithIamAuthorizer_ReturnsIamAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "apigateway", null);
|
||||
@@ -581,7 +611,8 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region Traefik Auth Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithTraefikBasicAuth_ReturnsBasicAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "traefik", null);
|
||||
@@ -602,7 +633,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("basic", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithTraefikForwardAuth_ReturnsCustomAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "traefik", null);
|
||||
@@ -628,7 +660,8 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region Controls Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithRateLimit_ReturnsRateLimitControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -648,7 +681,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "rate_limit");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithIpRestriction_ReturnsIpAllowlistControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -668,7 +702,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "ip_allowlist");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithCors_ReturnsCorsControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -688,7 +723,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "cors");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithWaf_ReturnsWafControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -708,7 +744,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "waf");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithRequestValidation_ReturnsInputValidationControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -728,7 +765,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "input_validation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithMultipleControls_ReturnsAllControls()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -750,7 +788,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal(3, result.Controls.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNoControls_ReturnsNullControls()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -769,7 +808,8 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region Confidence and Metadata
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_BaseConfidence_Returns0Point75()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "gateway", null);
|
||||
@@ -784,7 +824,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal(0.75, result.Confidence, precision: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithKnownGateway_IncreasesConfidence()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -799,7 +840,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal(0.85, result.Confidence, precision: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithAuthAndRouteInfo_MaximizesConfidence()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -819,7 +861,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal(0.95, result.Confidence, precision: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_ReturnsNetworkKind()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -834,7 +877,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Equal("network", result.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_BuildsEvidenceRef_WithGatewayType()
|
||||
{
|
||||
var root = new RichGraphRoot("root-123", "kong", null);
|
||||
@@ -855,7 +899,8 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region ExtractAsync
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_ReturnsSameResultAsExtract()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
@@ -882,14 +927,16 @@ public class GatewayBoundaryExtractorTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNullRoot_ThrowsArgumentNullException()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty with { Source = "kong" };
|
||||
Assert.Throws<ArgumentNullException>(() => _extractor.Extract(null!, null, context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WhenCannotHandle_ReturnsNull()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "static", null);
|
||||
@@ -900,7 +947,8 @@ public class GatewayBoundaryExtractorTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNoAuth_ReturnsNullAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "kong", null);
|
||||
|
||||
@@ -9,6 +9,7 @@ using StellaOps.Scanner.Reachability.Boundary;
|
||||
using StellaOps.Scanner.Reachability.Gates;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class IacBoundaryExtractorTests
|
||||
@@ -23,13 +24,15 @@ public class IacBoundaryExtractorTests
|
||||
|
||||
#region Priority and CanHandle
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Priority_Returns150_BetweenBaseAndK8s()
|
||||
{
|
||||
Assert.Equal(150, _extractor.Priority);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("terraform", true)]
|
||||
[InlineData("Terraform", true)]
|
||||
[InlineData("cloudformation", true)]
|
||||
@@ -46,7 +49,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal(expected, _extractor.CanHandle(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_WithTerraformAnnotations_ReturnsTrue()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty with
|
||||
@@ -60,7 +64,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.True(_extractor.CanHandle(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_WithCloudFormationAnnotations_ReturnsTrue()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty with
|
||||
@@ -74,7 +79,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.True(_extractor.CanHandle(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_WithHelmAnnotations_ReturnsTrue()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty with
|
||||
@@ -88,7 +94,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.True(_extractor.CanHandle(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_WithEmptyAnnotations_ReturnsFalse()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty;
|
||||
@@ -99,7 +106,8 @@ public class IacBoundaryExtractorTests
|
||||
|
||||
#region IaC Type Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithTerraformSource_ReturnsTerraformIacSource()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -114,7 +122,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("iac:terraform", result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithCloudFormationSource_ReturnsCloudFormationIacSource()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "cloudformation", null);
|
||||
@@ -129,7 +138,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("iac:cloudformation", result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithCfnSource_ReturnsCloudFormationIacSource()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "cfn", null);
|
||||
@@ -144,7 +154,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("iac:cloudformation", result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithPulumiSource_ReturnsPulumiIacSource()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "pulumi", null);
|
||||
@@ -159,7 +170,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("iac:pulumi", result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithHelmSource_ReturnsHelmIacSource()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "helm", null);
|
||||
@@ -178,7 +190,8 @@ public class IacBoundaryExtractorTests
|
||||
|
||||
#region Terraform Exposure Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithTerraformPublicSecurityGroup_ReturnsPublicExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -199,7 +212,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.True(result.Exposure.InternetFacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithTerraformInternetFacingAlb_ReturnsPublicExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -220,7 +234,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.True(result.Exposure.InternetFacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithTerraformPublicIp_ReturnsPublicExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -241,7 +256,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.True(result.Exposure.InternetFacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithTerraformPrivateResource_ReturnsInternalExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -266,7 +282,8 @@ public class IacBoundaryExtractorTests
|
||||
|
||||
#region CloudFormation Exposure Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithCloudFormationPublicSecurityGroup_ReturnsPublicExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "cloudformation", null);
|
||||
@@ -287,7 +304,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.True(result.Exposure.InternetFacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithCloudFormationInternetFacingElb_ReturnsPublicExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "cloudformation", null);
|
||||
@@ -308,7 +326,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.True(result.Exposure.InternetFacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithCloudFormationApiGateway_ReturnsPublicExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "cloudformation", null);
|
||||
@@ -333,7 +352,8 @@ public class IacBoundaryExtractorTests
|
||||
|
||||
#region Helm Exposure Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithHelmIngressEnabled_ReturnsPublicExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "helm", null);
|
||||
@@ -354,7 +374,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.True(result.Exposure.InternetFacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithHelmLoadBalancerService_ReturnsPublicExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "helm", null);
|
||||
@@ -375,7 +396,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.True(result.Exposure.InternetFacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithHelmClusterIpService_ReturnsPrivateExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "helm", null);
|
||||
@@ -400,7 +422,8 @@ public class IacBoundaryExtractorTests
|
||||
|
||||
#region Auth Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithIamAuth_ReturnsIamAuthType()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -422,7 +445,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("aws-iam", result.Auth.Provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithCognitoAuth_ReturnsOAuth2AuthType()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "cloudformation", null);
|
||||
@@ -444,7 +468,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("cognito", result.Auth.Provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithAzureAdAuth_ReturnsOAuth2AuthType()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -466,7 +491,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("azure-ad", result.Auth.Provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithMtlsAuth_ReturnsMtlsAuthType()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -487,7 +513,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("mtls", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNoAuth_ReturnsNullAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -506,7 +533,8 @@ public class IacBoundaryExtractorTests
|
||||
|
||||
#region Controls Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithSecurityGroup_ReturnsSecurityGroupControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -526,7 +554,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "security_group");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithWaf_ReturnsWafControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -546,7 +575,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "waf");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithVpc_ReturnsNetworkIsolationControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -566,7 +596,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "network_isolation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNacl_ReturnsNetworkAclControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -586,7 +617,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "network_acl");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithDdosProtection_ReturnsDdosControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -606,7 +638,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "ddos_protection");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithTls_ReturnsEncryptionControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -626,7 +659,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "encryption_in_transit");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithPrivateEndpoint_ReturnsPrivateEndpointControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -646,7 +680,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "private_endpoint");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithMultipleControls_ReturnsAllControls()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -668,7 +703,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal(3, result.Controls.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNoControls_ReturnsNullControls()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -687,7 +723,8 @@ public class IacBoundaryExtractorTests
|
||||
|
||||
#region Surface Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithHelmIngressPath_ReturnsSurfaceWithPath()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "helm", null);
|
||||
@@ -707,7 +744,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("/api/v1", result.Surface.Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithHelmIngressHost_ReturnsSurfaceWithHost()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "helm", null);
|
||||
@@ -727,7 +765,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("api.example.com", result.Surface.Host);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_DefaultSurfaceType_ReturnsInfrastructure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -743,7 +782,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("infrastructure", result.Surface.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_DefaultProtocol_ReturnsHttps()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -763,7 +803,8 @@ public class IacBoundaryExtractorTests
|
||||
|
||||
#region Confidence and Metadata
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_BaseConfidence_Returns0Point6()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "iac", null);
|
||||
@@ -778,7 +819,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal(0.6, result.Confidence, precision: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithKnownIacType_IncreasesConfidence()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -793,7 +835,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal(0.7, result.Confidence, precision: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithSecurityResources_IncreasesConfidence()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -812,7 +855,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal(0.8, result.Confidence, precision: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_MaxConfidence_CapsAt0Point85()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -833,7 +877,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.True(result.Confidence <= 0.85);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_ReturnsNetworkKind()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -848,7 +893,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Equal("network", result.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_BuildsEvidenceRef_WithIacType()
|
||||
{
|
||||
var root = new RichGraphRoot("root-123", "terraform", null);
|
||||
@@ -869,7 +915,8 @@ public class IacBoundaryExtractorTests
|
||||
|
||||
#region ExtractAsync
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_ReturnsSameResultAsExtract()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
@@ -896,14 +943,16 @@ public class IacBoundaryExtractorTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNullRoot_ThrowsArgumentNullException()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty with { Source = "terraform" };
|
||||
Assert.Throws<ArgumentNullException>(() => _extractor.Extract(null!, null, context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WhenCannotHandle_ReturnsNull()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -914,7 +963,8 @@ public class IacBoundaryExtractorTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithLoadBalancer_SetsBehindProxyTrue()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "terraform", null);
|
||||
|
||||
@@ -9,6 +9,7 @@ using StellaOps.Scanner.Reachability.Boundary;
|
||||
using StellaOps.Scanner.Reachability.Gates;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class K8sBoundaryExtractorTests
|
||||
@@ -23,13 +24,15 @@ public class K8sBoundaryExtractorTests
|
||||
|
||||
#region Priority and CanHandle
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Priority_Returns200_HigherThanRichGraphExtractor()
|
||||
{
|
||||
Assert.Equal(200, _extractor.Priority);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("k8s", true)]
|
||||
[InlineData("K8S", true)]
|
||||
[InlineData("kubernetes", true)]
|
||||
@@ -42,7 +45,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal(expected, _extractor.CanHandle(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_WithK8sAnnotations_ReturnsTrue()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty with
|
||||
@@ -56,7 +60,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.True(_extractor.CanHandle(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_WithIngressAnnotation_ReturnsTrue()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty with
|
||||
@@ -70,7 +75,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.True(_extractor.CanHandle(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_WithEmptyAnnotations_ReturnsFalse()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty;
|
||||
@@ -81,7 +87,8 @@ public class K8sBoundaryExtractorTests
|
||||
|
||||
#region Extract - Exposure Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithInternetFacing_ReturnsPublicExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -99,7 +106,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.True(result.Exposure.InternetFacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithIngressClass_ReturnsInternetFacing()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -120,7 +128,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.True(result.Exposure.BehindProxy);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("LoadBalancer", "public", true)]
|
||||
[InlineData("NodePort", "internal", false)]
|
||||
[InlineData("ClusterIP", "private", false)]
|
||||
@@ -145,7 +154,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal(expectedInternetFacing, result.Exposure.InternetFacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithExternalPorts_ReturnsInternalLevel()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -162,7 +172,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("internal", result.Exposure.Level);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithDmzZone_ReturnsInternalLevel()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -184,7 +195,8 @@ public class K8sBoundaryExtractorTests
|
||||
|
||||
#region Extract - Surface Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithServicePath_ReturnsSurfaceWithPath()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -204,7 +216,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("/api/v1", result.Surface.Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithRewriteTarget_ReturnsSurfaceWithPath()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -224,7 +237,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("/backend", result.Surface.Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNamespace_ReturnsSurfaceWithNamespacePath()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -241,7 +255,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("/production", result.Surface.Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithTlsAnnotation_ReturnsHttpsProtocol()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -261,7 +276,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("https", result.Surface.Protocol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithGrpcAnnotation_ReturnsGrpcProtocol()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -281,7 +297,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("grpc", result.Surface.Protocol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithPortBinding_ReturnsSurfaceWithPort()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -298,7 +315,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal(8080, result.Surface.Port);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithIngressHost_ReturnsSurfaceWithHost()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -322,7 +340,8 @@ public class K8sBoundaryExtractorTests
|
||||
|
||||
#region Extract - Auth Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithBasicAuth_ReturnsBasicAuthType()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -343,7 +362,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("basic", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithOAuth_ReturnsOAuth2Type()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -364,7 +384,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("oauth2", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithMtls_ReturnsMtlsType()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -385,7 +406,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("mtls", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithExplicitAuthType_ReturnsSpecifiedType()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -406,7 +428,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("jwt", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithAuthRoles_ReturnsRolesList()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -431,7 +454,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Contains("viewer", result.Auth.Roles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNoAuth_ReturnsNullAuth()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -450,7 +474,8 @@ public class K8sBoundaryExtractorTests
|
||||
|
||||
#region Extract - Controls Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNetworkPolicy_ReturnsNetworkPolicyControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -475,7 +500,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("high", control.Effectiveness);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithRateLimit_ReturnsRateLimitControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -498,7 +524,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("medium", control.Effectiveness);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithIpAllowlist_ReturnsIpAllowlistControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -521,7 +548,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("high", control.Effectiveness);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithWaf_ReturnsWafControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -544,7 +572,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("high", control.Effectiveness);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithMultipleControls_ReturnsAllControls()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -569,7 +598,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Contains(result.Controls, c => c.Type == "waf");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNoControls_ReturnsNullControls()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -588,7 +618,8 @@ public class K8sBoundaryExtractorTests
|
||||
|
||||
#region Extract - Confidence and Metadata
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_BaseConfidence_Returns0Point7()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -603,7 +634,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal(0.7, result.Confidence, precision: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithIngressAnnotation_IncreasesConfidence()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -622,7 +654,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal(0.85, result.Confidence, precision: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithServiceType_IncreasesConfidence()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -641,7 +674,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal(0.8, result.Confidence, precision: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_MaxConfidence_CapsAt0Point95()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -661,7 +695,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.True(result.Confidence <= 0.95);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_ReturnsK8sSource()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -676,7 +711,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("k8s", result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_BuildsEvidenceRef_WithNamespaceAndEnvironment()
|
||||
{
|
||||
var root = new RichGraphRoot("root-123", "k8s", null);
|
||||
@@ -693,7 +729,8 @@ public class K8sBoundaryExtractorTests
|
||||
Assert.Equal("k8s/production/env-456/root-123", result.EvidenceRef);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_ReturnsNetworkKind()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -712,7 +749,8 @@ public class K8sBoundaryExtractorTests
|
||||
|
||||
#region ExtractAsync
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_ReturnsSameResultAsExtract()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "k8s", null);
|
||||
@@ -740,14 +778,16 @@ public class K8sBoundaryExtractorTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNullRoot_ThrowsArgumentNullException()
|
||||
{
|
||||
var context = BoundaryExtractionContext.Empty with { Source = "k8s" };
|
||||
Assert.Throws<ArgumentNullException>(() => _extractor.Extract(null!, null, context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WhenCannotHandle_ReturnsNull()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "static", null);
|
||||
|
||||
@@ -9,6 +9,7 @@ using StellaOps.Scanner.Reachability.Explanation;
|
||||
using StellaOps.Scanner.Reachability.Gates;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class PathExplanationServiceTests
|
||||
@@ -23,7 +24,8 @@ public class PathExplanationServiceTests
|
||||
_renderer = new PathRenderer();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExplainAsync_WithSimplePath_ReturnsExplainedPath()
|
||||
{
|
||||
// Arrange
|
||||
@@ -38,7 +40,8 @@ public class PathExplanationServiceTests
|
||||
Assert.True(result.TotalCount >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExplainAsync_WithSinkFilter_FiltersResults()
|
||||
{
|
||||
// Arrange
|
||||
@@ -56,7 +59,8 @@ public class PathExplanationServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExplainAsync_WithGatesFilter_FiltersPathsWithGates()
|
||||
{
|
||||
// Arrange
|
||||
@@ -74,7 +78,8 @@ public class PathExplanationServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExplainAsync_WithMaxPathLength_LimitsPathLength()
|
||||
{
|
||||
// Arrange
|
||||
@@ -92,7 +97,8 @@ public class PathExplanationServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExplainAsync_WithMaxPaths_LimitsResults()
|
||||
{
|
||||
// Arrange
|
||||
@@ -111,7 +117,8 @@ public class PathExplanationServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Renderer_Text_ProducesExpectedFormat()
|
||||
{
|
||||
// Arrange
|
||||
@@ -125,7 +132,8 @@ public class PathExplanationServiceTests
|
||||
Assert.Contains("SINK:", text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Renderer_Markdown_ProducesExpectedFormat()
|
||||
{
|
||||
// Arrange
|
||||
@@ -140,7 +148,8 @@ public class PathExplanationServiceTests
|
||||
Assert.Contains(path.EntrypointSymbol, markdown);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Renderer_Json_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
@@ -156,7 +165,8 @@ public class PathExplanationServiceTests
|
||||
Assert.Contains("entrypoint_id", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Renderer_WithGates_IncludesGateInfo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -170,7 +180,8 @@ public class PathExplanationServiceTests
|
||||
Assert.Contains("multiplier", text.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExplainPathAsync_WithValidId_ReturnsPath()
|
||||
{
|
||||
// Arrange
|
||||
@@ -184,7 +195,8 @@ public class PathExplanationServiceTests
|
||||
Assert.True(result is null || result.PathId is not null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GateMultiplier_Calculation_IsCorrect()
|
||||
{
|
||||
// Arrange - path with auth gate
|
||||
@@ -194,7 +206,8 @@ public class PathExplanationServiceTests
|
||||
Assert.True(pathWithAuth.GateMultiplierBps < 10000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PathWithoutGates_HasFullMultiplier()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -3,6 +3,7 @@ using StellaOps.Scanner.Reachability.Gates;
|
||||
using StellaOps.Scanner.Reachability.Witnesses;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class PathWitnessBuilderTests
|
||||
@@ -16,7 +17,8 @@ public class PathWitnessBuilderTests
|
||||
_timeProvider = TimeProvider.System;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildAsync_ReturnsNull_WhenNoPathExists()
|
||||
{
|
||||
// Arrange
|
||||
@@ -46,7 +48,8 @@ public class PathWitnessBuilderTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildAsync_ReturnsWitness_WhenPathExists()
|
||||
{
|
||||
// Arrange
|
||||
@@ -82,7 +85,8 @@ public class PathWitnessBuilderTests
|
||||
Assert.NotEmpty(result.Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildAsync_GeneratesContentAddressedWitnessId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -117,7 +121,8 @@ public class PathWitnessBuilderTests
|
||||
Assert.Equal(result1.WitnessId, result2.WitnessId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildAsync_PopulatesArtifactInfo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -149,7 +154,8 @@ public class PathWitnessBuilderTests
|
||||
Assert.Equal("pkg:npm/lodash@4.17.21", result.Artifact.ComponentPurl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildAsync_PopulatesEvidenceInfo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -186,7 +192,8 @@ public class PathWitnessBuilderTests
|
||||
Assert.Equal("build:xyz789", result.Evidence.BuildId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildAsync_FindsShortestPath()
|
||||
{
|
||||
// Arrange - graph with multiple paths
|
||||
@@ -222,7 +229,8 @@ public class PathWitnessBuilderTests
|
||||
Assert.Equal("sym:end", result.Path[2].SymbolId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildAllAsync_YieldsMultipleWitnesses_WhenMultipleRootsReachSink()
|
||||
{
|
||||
// Arrange
|
||||
@@ -256,7 +264,8 @@ public class PathWitnessBuilderTests
|
||||
Assert.Contains(witnesses, w => w.Entrypoint.SymbolId == "sym:root2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildAllAsync_RespectsMaxWitnesses()
|
||||
{
|
||||
// Arrange
|
||||
@@ -390,7 +399,8 @@ public class PathWitnessBuilderTests
|
||||
/// <summary>
|
||||
/// WIT-008: Test that BuildFromAnalyzerAsync generates witnesses from pre-computed paths.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildFromAnalyzerAsync_GeneratesWitnessesFromPaths()
|
||||
{
|
||||
// Arrange
|
||||
@@ -447,7 +457,8 @@ public class PathWitnessBuilderTests
|
||||
/// <summary>
|
||||
/// WIT-008: Test that BuildFromAnalyzerAsync yields empty when no paths provided.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildFromAnalyzerAsync_YieldsEmpty_WhenNoPaths()
|
||||
{
|
||||
// Arrange
|
||||
@@ -480,7 +491,8 @@ public class PathWitnessBuilderTests
|
||||
/// <summary>
|
||||
/// WIT-008: Test that missing node metadata is handled gracefully.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildFromAnalyzerAsync_HandlesMissingNodeMetadata()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -12,6 +12,7 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Reachability.Cache;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public sealed class PrReachabilityGateTests
|
||||
@@ -26,7 +27,8 @@ public sealed class PrReachabilityGateTests
|
||||
_gate = new PrReachabilityGate(optionsMonitor, NullLogger<PrReachabilityGate>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvaluateFlips_NoFlips_ReturnsPass()
|
||||
{
|
||||
// Arrange
|
||||
@@ -42,7 +44,8 @@ public sealed class PrReachabilityGateTests
|
||||
result.Decision.MitigatedCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvaluateFlips_NewReachable_ReturnsBlock()
|
||||
{
|
||||
// Arrange
|
||||
@@ -71,7 +74,8 @@ public sealed class PrReachabilityGateTests
|
||||
result.Decision.BlockingFlips.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvaluateFlips_OnlyMitigated_ReturnsPass()
|
||||
{
|
||||
// Arrange
|
||||
@@ -99,7 +103,8 @@ public sealed class PrReachabilityGateTests
|
||||
result.Decision.MitigatedCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvaluateFlips_GateDisabled_AlwaysPasses()
|
||||
{
|
||||
// Arrange
|
||||
@@ -126,7 +131,8 @@ public sealed class PrReachabilityGateTests
|
||||
result.Reason.Should().Be("PR gate is disabled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvaluateFlips_LowConfidence_Excluded()
|
||||
{
|
||||
// Arrange
|
||||
@@ -155,7 +161,8 @@ public sealed class PrReachabilityGateTests
|
||||
result.Decision.BlockingFlips.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvaluateFlips_MaxNewReachableThreshold_AllowsUnderThreshold()
|
||||
{
|
||||
// Arrange
|
||||
@@ -189,7 +196,8 @@ public sealed class PrReachabilityGateTests
|
||||
result.Passed.Should().BeTrue(); // 2 == threshold, so should pass
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvaluateFlips_MaxNewReachableThreshold_BlocksOverThreshold()
|
||||
{
|
||||
// Arrange
|
||||
@@ -223,7 +231,8 @@ public sealed class PrReachabilityGateTests
|
||||
result.Passed.Should().BeFalse(); // 2 > 1, so should block
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvaluateFlips_Annotations_GeneratedForBlockingFlips()
|
||||
{
|
||||
// Arrange
|
||||
@@ -257,7 +266,8 @@ public sealed class PrReachabilityGateTests
|
||||
result.Annotations[0].StartLine.Should().Be(42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvaluateFlips_AnnotationsDisabled_NoAnnotations()
|
||||
{
|
||||
// Arrange
|
||||
@@ -283,7 +293,8 @@ public sealed class PrReachabilityGateTests
|
||||
result.Annotations.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvaluateFlips_SummaryMarkdown_Generated()
|
||||
{
|
||||
// Arrange
|
||||
@@ -321,7 +332,8 @@ public sealed class PrReachabilityGateTests
|
||||
result.SummaryMarkdown.Should().Contain("Mitigated paths");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Evaluate_NullStateFlips_ReturnsPass()
|
||||
{
|
||||
// Arrange
|
||||
@@ -344,7 +356,8 @@ public sealed class PrReachabilityGateTests
|
||||
gateResult.Reason.Should().Be("No state flip detection performed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Evaluate_WithStateFlips_DelegatesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Reachability.Cache;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public sealed class GraphDeltaComputerTests
|
||||
@@ -24,7 +25,8 @@ public sealed class GraphDeltaComputerTests
|
||||
_computer = new GraphDeltaComputer(NullLogger<GraphDeltaComputer>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeDeltaAsync_SameHash_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -38,7 +40,8 @@ public sealed class GraphDeltaComputerTests
|
||||
delta.HasChanges.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeDeltaAsync_AddedNode_ReturnsCorrectDelta()
|
||||
{
|
||||
// Arrange
|
||||
@@ -56,7 +59,8 @@ public sealed class GraphDeltaComputerTests
|
||||
delta.AffectedMethodKeys.Should().Contain("C");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeDeltaAsync_RemovedNode_ReturnsCorrectDelta()
|
||||
{
|
||||
// Arrange
|
||||
@@ -73,7 +77,8 @@ public sealed class GraphDeltaComputerTests
|
||||
delta.RemovedEdges.Should().ContainSingle(e => e.CallerKey == "B" && e.CalleeKey == "C");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeDeltaAsync_EdgeChange_DetectsAffectedMethods()
|
||||
{
|
||||
// Arrange
|
||||
@@ -116,7 +121,8 @@ public sealed class ImpactSetCalculatorTests
|
||||
_calculator = new ImpactSetCalculator(NullLogger<ImpactSetCalculator>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CalculateImpactAsync_NoDelta_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -132,7 +138,8 @@ public sealed class ImpactSetCalculatorTests
|
||||
impact.SavingsRatio.Should().Be(1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CalculateImpactAsync_ChangeInPath_IdentifiesAffectedEntry()
|
||||
{
|
||||
// Arrange
|
||||
@@ -157,7 +164,8 @@ public sealed class ImpactSetCalculatorTests
|
||||
impact.AffectedEntryPoints.Should().Contain("Entry");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CalculateImpactAsync_ManyAffected_TriggersFullRecompute()
|
||||
{
|
||||
// Arrange - More than 30% affected
|
||||
@@ -205,7 +213,8 @@ public sealed class StateFlipDetectorTests
|
||||
_detector = new StateFlipDetector(NullLogger<StateFlipDetector>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DetectFlipsAsync_NoChanges_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -228,7 +237,8 @@ public sealed class StateFlipDetectorTests
|
||||
result.MitigatedCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DetectFlipsAsync_BecameReachable_ReturnsNewRisk()
|
||||
{
|
||||
// Arrange
|
||||
@@ -254,7 +264,8 @@ public sealed class StateFlipDetectorTests
|
||||
result.ShouldBlockPr.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DetectFlipsAsync_BecameUnreachable_ReturnsMitigated()
|
||||
{
|
||||
// Arrange
|
||||
@@ -280,7 +291,8 @@ public sealed class StateFlipDetectorTests
|
||||
result.ShouldBlockPr.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DetectFlipsAsync_NewReachablePair_ReturnsNewRisk()
|
||||
{
|
||||
// Arrange
|
||||
@@ -300,7 +312,8 @@ public sealed class StateFlipDetectorTests
|
||||
result.ShouldBlockPr.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DetectFlipsAsync_RemovedReachablePair_ReturnsMitigated()
|
||||
{
|
||||
// Arrange
|
||||
@@ -320,7 +333,8 @@ public sealed class StateFlipDetectorTests
|
||||
result.ShouldBlockPr.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DetectFlipsAsync_NetChange_CalculatesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -3,11 +3,13 @@ using StellaOps.Scanner.Reachability.Gates;
|
||||
using StellaOps.Scanner.Reachability.Subgraph;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public sealed class ReachabilitySubgraphExtractorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_BuildsSubgraphWithEntrypointAndVulnerable()
|
||||
{
|
||||
var graph = CreateGraph();
|
||||
@@ -27,7 +29,8 @@ public sealed class ReachabilitySubgraphExtractorTests
|
||||
Assert.Contains(subgraph.Nodes, n => n.Id == "call" && n.Type == ReachabilitySubgraphNodeType.Call);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_MapsGateMetadata()
|
||||
{
|
||||
var graph = CreateGraph(withGate: true);
|
||||
@@ -46,7 +49,8 @@ public sealed class ReachabilitySubgraphExtractorTests
|
||||
Assert.Equal("auth.check", gatedEdge.Gate.GuardSymbol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithNoTargets_ReturnsEmptySubgraph()
|
||||
{
|
||||
var graph = CreateGraph();
|
||||
|
||||
@@ -6,11 +6,13 @@ using StellaOps.Scanner.Reachability.Attestation;
|
||||
using StellaOps.Scanner.Reachability.Subgraph;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public sealed class ReachabilitySubgraphPublisherTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_BuildsDigestAndStoresInCas()
|
||||
{
|
||||
var subgraph = new ReachabilitySubgraph
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class ReachabilityUnionPublisherTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishesZipToCas()
|
||||
{
|
||||
var graph = new ReachabilityUnionGraph(
|
||||
@@ -14,6 +15,7 @@ public class ReachabilityUnionPublisherTests
|
||||
Edges: new ReachabilityUnionEdge[0]);
|
||||
|
||||
using var temp = new TempDir();
|
||||
using StellaOps.TestKit;
|
||||
var cas = new FakeFileContentAddressableStore();
|
||||
var publisher = new ReachabilityUnionPublisher(new ReachabilityUnionWriter());
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class ReachabilityUnionWriterTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WritesDeterministicNdjson()
|
||||
{
|
||||
var writer = new ReachabilityUnionWriter();
|
||||
@@ -39,7 +40,8 @@ public class ReachabilityUnionWriterTests
|
||||
Assert.Contains(nodeLines, l => l.Contains("sym:dotnet:A"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WritesNodePurlAndSymbolDigest()
|
||||
{
|
||||
var writer = new ReachabilityUnionWriter();
|
||||
@@ -68,7 +70,8 @@ public class ReachabilityUnionWriterTests
|
||||
Assert.Contains("\"symbol_digest\":\"sha256:abc123\"", nodeLines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WritesEdgePurlAndSymbolDigest()
|
||||
{
|
||||
var writer = new ReachabilityUnionWriter();
|
||||
@@ -100,7 +103,8 @@ public class ReachabilityUnionWriterTests
|
||||
Assert.Contains("\"symbol_digest\":\"sha256:def456\"", edgeLines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WritesEdgeCandidates()
|
||||
{
|
||||
var writer = new ReachabilityUnionWriter();
|
||||
@@ -139,7 +143,8 @@ public class ReachabilityUnionWriterTests
|
||||
Assert.Contains("\"score\":0.8", edgeLines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WritesSymbolMetadataAndCodeBlockHash()
|
||||
{
|
||||
var writer = new ReachabilityUnionWriter();
|
||||
@@ -166,12 +171,14 @@ public class ReachabilityUnionWriterTests
|
||||
Assert.Contains("\"symbol\":{\"mangled\":\"_Z15ssl3_read_bytes\",\"demangled\":\"ssl3_read_bytes\",\"source\":\"DWARF\",\"confidence\":0.98}", nodeLines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OmitsPurlAndSymbolDigestWhenNull()
|
||||
{
|
||||
var writer = new ReachabilityUnionWriter();
|
||||
using var temp = new TempDir();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var graph = new ReachabilityUnionGraph(
|
||||
Nodes: new[]
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Reachability.Attestation;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -25,7 +26,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
|
||||
#region BuildStatement Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildStatement_CreatesValidStatement()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -41,7 +43,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
Assert.Single(statement.Subject);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildStatement_SetsSubjectCorrectly()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -56,7 +59,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
Assert.Equal("imageabc123", subject.Digest["sha256"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildStatement_ExtractsPredicateCorrectly()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -79,7 +83,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
Assert.Equal("abc123def456", predicate.SourceCommit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildStatement_CountsNodesAndEdges()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -95,7 +100,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
Assert.Equal(2, predicate.EdgeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildStatement_CountsEntrypoints()
|
||||
{
|
||||
var graph = CreateTestGraphWithRoots();
|
||||
@@ -110,7 +116,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
Assert.Equal(2, predicate.EntrypointCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildStatement_UsesProvidedTimestamp()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -125,7 +132,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
Assert.Equal(_timeProvider.GetUtcNow(), predicate.GeneratedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildStatement_ExtractsAnalyzerVersion()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -144,7 +152,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
|
||||
#region SerializeStatement Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SerializeStatement_ProducesValidJson()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -161,7 +170,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
Assert.Contains("\"predicateType\":\"https://stella.ops/reachabilityWitness/v1\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SerializeStatement_IsDeterministic()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -180,7 +190,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
|
||||
#region ComputeStatementHash Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeStatementHash_ReturnsBlake3Hash()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -196,7 +207,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
Assert.Equal(64 + 7, hash.Length); // "blake3:" + 64 hex chars
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeStatementHash_IsDeterministic()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -216,14 +228,16 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildStatement_ThrowsForNullGraph()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
_builder.BuildStatement(null!, "blake3:abc", "sha256:def"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildStatement_ThrowsForEmptyGraphHash()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -231,7 +245,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
_builder.BuildStatement(graph, "", "sha256:def"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildStatement_ThrowsForEmptySubjectDigest()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
@@ -239,7 +254,8 @@ public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
_builder.BuildStatement(graph, "blake3:abc", ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildStatement_HandlesEmptyGraph()
|
||||
{
|
||||
var graph = new RichGraph(
|
||||
|
||||
@@ -8,11 +8,13 @@ using StellaOps.Scanner.ProofSpine.Options;
|
||||
using StellaOps.Scanner.Reachability.Attestation;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public sealed class ReachabilityWitnessPublisherIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_WhenStoreInCasEnabled_StoresGraphAndEnvelopeInCas()
|
||||
{
|
||||
var options = Options.Create(new ReachabilityWitnessOptions
|
||||
@@ -45,7 +47,8 @@ public sealed class ReachabilityWitnessPublisherIntegrationTests
|
||||
Assert.NotEmpty(result.DsseEnvelopeBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_WhenRekorEnabled_SubmitsDsseEnvelope()
|
||||
{
|
||||
var rekor = new CapturingRekorClient();
|
||||
@@ -95,7 +98,8 @@ public sealed class ReachabilityWitnessPublisherIntegrationTests
|
||||
Assert.Equal("rekor-uuid-1234", result.RekorLogId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_WhenAirGapped_SkipsRekorSubmission()
|
||||
{
|
||||
var rekor = new CapturingRekorClient();
|
||||
|
||||
@@ -9,6 +9,7 @@ using StellaOps.Scanner.Reachability.Boundary;
|
||||
using StellaOps.Scanner.Reachability.Gates;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class RichGraphBoundaryExtractorTests
|
||||
@@ -21,7 +22,8 @@ public class RichGraphBoundaryExtractorTests
|
||||
NullLogger<RichGraphBoundaryExtractor>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_HttpRoot_ReturnsBoundaryWithApiSurface()
|
||||
{
|
||||
var root = new RichGraphRoot("root-http", "runtime", null);
|
||||
@@ -47,7 +49,8 @@ public class RichGraphBoundaryExtractorTests
|
||||
Assert.Equal("https", result.Surface.Protocol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_GrpcRoot_ReturnsBoundaryWithGrpcProtocol()
|
||||
{
|
||||
var root = new RichGraphRoot("root-grpc", "runtime", null);
|
||||
@@ -71,7 +74,8 @@ public class RichGraphBoundaryExtractorTests
|
||||
Assert.Equal("grpc", result.Surface.Protocol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_CliRoot_ReturnsProcessBoundary()
|
||||
{
|
||||
var root = new RichGraphRoot("root-cli", "runtime", null);
|
||||
@@ -96,7 +100,8 @@ public class RichGraphBoundaryExtractorTests
|
||||
Assert.Equal("cli", result.Surface.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_LibraryPhase_ReturnsLibraryBoundary()
|
||||
{
|
||||
var root = new RichGraphRoot("root-lib", "library", null);
|
||||
@@ -121,7 +126,8 @@ public class RichGraphBoundaryExtractorTests
|
||||
Assert.Equal("library", result.Surface.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithAuthGate_SetsAuthRequired()
|
||||
{
|
||||
var root = new RichGraphRoot("root-auth", "runtime", null);
|
||||
@@ -158,7 +164,8 @@ public class RichGraphBoundaryExtractorTests
|
||||
Assert.Equal("jwt", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithAdminGate_SetsAdminRole()
|
||||
{
|
||||
var root = new RichGraphRoot("root-admin", "runtime", null);
|
||||
@@ -196,7 +203,8 @@ public class RichGraphBoundaryExtractorTests
|
||||
Assert.Contains("admin", result.Auth.Roles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithFeatureFlagGate_AddsControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-ff", "runtime", null);
|
||||
@@ -234,7 +242,8 @@ public class RichGraphBoundaryExtractorTests
|
||||
Assert.True(result.Controls[0].Active);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_WithInternetFacingContext_SetsExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-public", "runtime", null);
|
||||
@@ -265,7 +274,8 @@ public class RichGraphBoundaryExtractorTests
|
||||
Assert.Equal("public", result.Exposure.Level);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_InternalService_SetsInternalExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-internal", "runtime", null);
|
||||
@@ -290,7 +300,8 @@ public class RichGraphBoundaryExtractorTests
|
||||
Assert.Equal("internal", result.Exposure.Level);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_SetsConfidenceBasedOnContext()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "runtime", null);
|
||||
@@ -334,7 +345,8 @@ public class RichGraphBoundaryExtractorTests
|
||||
Assert.True(richResult.Confidence > emptyResult.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Extract_IsDeterministic()
|
||||
{
|
||||
var root = new RichGraphRoot("root-det", "runtime", null);
|
||||
@@ -374,20 +386,23 @@ public class RichGraphBoundaryExtractorTests
|
||||
Assert.Equal(result1.Confidence, result2.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanHandle_AlwaysReturnsTrue()
|
||||
{
|
||||
Assert.True(_extractor.CanHandle(BoundaryExtractionContext.Empty));
|
||||
Assert.True(_extractor.CanHandle(BoundaryExtractionContext.ForEnvironment("test")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Priority_ReturnsBaseValue()
|
||||
{
|
||||
Assert.Equal(100, _extractor.Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractAsync_ReturnsResult()
|
||||
{
|
||||
var root = new RichGraphRoot("root-async", "runtime", null);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user