save progress

This commit is contained in:
StellaOps Bot
2025-12-18 09:10:36 +02:00
parent b4235c134c
commit 28823a8960
169 changed files with 11995 additions and 449 deletions

View File

@@ -4,8 +4,6 @@
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Reachability.Benchmarks;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests.Benchmarks;
@@ -124,10 +122,10 @@ public sealed class CorpusRunnerIntegrationTests
// Arrange
var results = new List<SampleResult>
{
new("gt-0001", expected: true, actual: true, tier: "executed", durationMs: 10),
new("gt-0002", expected: true, actual: true, tier: "executed", durationMs: 15),
new("gt-0011", expected: false, actual: false, tier: "imported", durationMs: 5),
new("gt-0012", expected: false, actual: true, tier: "executed", durationMs: 8), // False positive
new("gt-0001", true, true, "executed", 10),
new("gt-0002", true, true, "executed", 15),
new("gt-0011", false, false, "imported", 5),
new("gt-0012", false, true, "executed", 8), // False positive
};
// Act

View File

@@ -0,0 +1,230 @@
using StellaOps.Scanner.Reachability.Gates;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests;
/// <summary>
/// Unit tests for gate detection and multiplier calculation.
/// SPRINT_3405_0001_0001 - Tasks #13, #14, #15
/// </summary>
public sealed class GateDetectionTests
{
[Fact]
public void GateDetectionResult_Empty_HasNoGates()
{
Assert.False(GateDetectionResult.Empty.HasGates);
Assert.Empty(GateDetectionResult.Empty.Gates);
Assert.Null(GateDetectionResult.Empty.PrimaryGate);
}
[Fact]
public void GateDetectionResult_WithGates_HasPrimaryGate()
{
var gates = new[]
{
CreateGate(GateType.AuthRequired, 0.7),
CreateGate(GateType.FeatureFlag, 0.9),
};
var result = new GateDetectionResult { Gates = gates };
Assert.True(result.HasGates);
Assert.Equal(2, result.Gates.Count);
Assert.Equal(GateType.FeatureFlag, result.PrimaryGate?.Type);
}
[Fact]
public void GateMultiplierConfig_Default_HasExpectedValues()
{
var config = GateMultiplierConfig.Default;
Assert.Equal(3000, config.AuthRequiredMultiplierBps);
Assert.Equal(2000, config.FeatureFlagMultiplierBps);
Assert.Equal(1500, config.AdminOnlyMultiplierBps);
Assert.Equal(5000, config.NonDefaultConfigMultiplierBps);
Assert.Equal(500, config.MinimumMultiplierBps);
}
[Fact]
public async Task CompositeGateDetector_NoDetectors_ReturnsEmpty()
{
var detector = new CompositeGateDetector([]);
var context = CreateContext(["main", "vulnerable_function"]);
var result = await detector.DetectAllAsync(context);
Assert.False(result.HasGates);
Assert.Equal(10000, result.CombinedMultiplierBps);
}
[Fact]
public async Task CompositeGateDetector_EmptyCallPath_ReturnsEmpty()
{
var detector = new CompositeGateDetector([new MockAuthDetector()]);
var context = CreateContext([]);
var result = await detector.DetectAllAsync(context);
Assert.False(result.HasGates);
}
[Fact]
public async Task CompositeGateDetector_SingleGate_AppliesMultiplier()
{
var authDetector = new MockAuthDetector(
CreateGate(GateType.AuthRequired, 0.95));
var detector = new CompositeGateDetector([authDetector]);
var context = CreateContext(["main", "auth_check", "vulnerable"]);
var result = await detector.DetectAllAsync(context);
Assert.True(result.HasGates);
Assert.Single(result.Gates);
Assert.Equal(3000, result.CombinedMultiplierBps);
}
[Fact]
public async Task CompositeGateDetector_MultipleGateTypes_MultipliesMultipliers()
{
var authDetector = new MockAuthDetector(
CreateGate(GateType.AuthRequired, 0.9));
var featureDetector = new MockFeatureFlagDetector(
CreateGate(GateType.FeatureFlag, 0.8));
var detector = new CompositeGateDetector([authDetector, featureDetector]);
var context = CreateContext(["main", "auth_check", "feature_check", "vulnerable"]);
var result = await detector.DetectAllAsync(context);
Assert.True(result.HasGates);
Assert.Equal(2, result.Gates.Count);
Assert.Equal(600, result.CombinedMultiplierBps);
}
[Fact]
public async Task CompositeGateDetector_DuplicateGates_Deduplicates()
{
var authDetector1 = new MockAuthDetector(
CreateGate(GateType.AuthRequired, 0.9, "checkAuth"));
var authDetector2 = new MockAuthDetector(
CreateGate(GateType.AuthRequired, 0.7, "checkAuth"));
var detector = new CompositeGateDetector([authDetector1, authDetector2]);
var context = CreateContext(["main", "checkAuth", "vulnerable"]);
var result = await detector.DetectAllAsync(context);
Assert.Single(result.Gates);
Assert.Equal(0.9, result.Gates[0].Confidence);
}
[Fact]
public async Task CompositeGateDetector_AllGateTypes_AppliesMinimumFloor()
{
var detectors = new IGateDetector[]
{
new MockAuthDetector(CreateGate(GateType.AuthRequired, 0.9)),
new MockFeatureFlagDetector(CreateGate(GateType.FeatureFlag, 0.9)),
new MockAdminDetector(CreateGate(GateType.AdminOnly, 0.9)),
new MockConfigDetector(CreateGate(GateType.NonDefaultConfig, 0.9)),
};
var detector = new CompositeGateDetector(detectors);
var context = CreateContext(["main", "auth", "feature", "admin", "config", "vulnerable"]);
var result = await detector.DetectAllAsync(context);
Assert.Equal(4, result.Gates.Count);
Assert.Equal(500, result.CombinedMultiplierBps);
}
[Fact]
public async Task CompositeGateDetector_DetectorException_ContinuesWithOthers()
{
var failingDetector = new FailingGateDetector();
var authDetector = new MockAuthDetector(
CreateGate(GateType.AuthRequired, 0.9));
var detector = new CompositeGateDetector([failingDetector, authDetector]);
var context = CreateContext(["main", "vulnerable"]);
var result = await detector.DetectAllAsync(context);
Assert.Single(result.Gates);
Assert.Equal(GateType.AuthRequired, result.Gates[0].Type);
}
private static DetectedGate CreateGate(GateType type, double confidence, string symbol = "guard_symbol")
{
return new DetectedGate
{
Type = type,
Detail = $"{type} gate detected",
GuardSymbol = symbol,
Confidence = confidence,
DetectionMethod = "mock",
};
}
private static CallPathContext CreateContext(string[] callPath)
{
return new CallPathContext
{
CallPath = callPath,
Language = "csharp",
};
}
private sealed class MockAuthDetector : IGateDetector
{
private readonly DetectedGate[] _gates;
public GateType GateType => GateType.AuthRequired;
public MockAuthDetector(params DetectedGate[] gates) => _gates = gates;
public Task<IReadOnlyList<DetectedGate>> DetectAsync(CallPathContext context, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<DetectedGate>>(_gates);
}
private sealed class MockFeatureFlagDetector : IGateDetector
{
private readonly DetectedGate[] _gates;
public GateType GateType => GateType.FeatureFlag;
public MockFeatureFlagDetector(params DetectedGate[] gates) => _gates = gates;
public Task<IReadOnlyList<DetectedGate>> DetectAsync(CallPathContext context, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<DetectedGate>>(_gates);
}
private sealed class MockAdminDetector : IGateDetector
{
private readonly DetectedGate[] _gates;
public GateType GateType => GateType.AdminOnly;
public MockAdminDetector(params DetectedGate[] gates) => _gates = gates;
public Task<IReadOnlyList<DetectedGate>> DetectAsync(CallPathContext context, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<DetectedGate>>(_gates);
}
private sealed class MockConfigDetector : IGateDetector
{
private readonly DetectedGate[] _gates;
public GateType GateType => GateType.NonDefaultConfig;
public MockConfigDetector(params DetectedGate[] gates) => _gates = gates;
public Task<IReadOnlyList<DetectedGate>> DetectAsync(CallPathContext context, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<DetectedGate>>(_gates);
}
private sealed class FailingGateDetector : IGateDetector
{
public GateType GateType => GateType.AuthRequired;
public Task<IReadOnlyList<DetectedGate>> DetectAsync(CallPathContext context, CancellationToken ct)
=> throw new InvalidOperationException("Simulated detector failure");
}
}

View File

@@ -0,0 +1,57 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Reachability;
using StellaOps.Scanner.Reachability.Gates;
using GateDetectors = StellaOps.Scanner.Reachability.Gates.Detectors;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests;
public sealed class RichGraphGateAnnotatorTests
{
[Fact]
public async Task AnnotateAsync_AddsAuthGateAndMultiplier()
{
var union = new ReachabilityUnionGraph(
Nodes: new[]
{
new ReachabilityUnionNode("sym:dotnet:A", "dotnet", "method", "A"),
new ReachabilityUnionNode(
"sym:dotnet:B",
"dotnet",
"method",
"B",
Attributes: new Dictionary<string, string> { ["annotations"] = "[Authorize]" })
},
Edges: new[]
{
new ReachabilityUnionEdge("sym:dotnet:A", "sym:dotnet:B", "call", "high")
});
var graph = RichGraphBuilder.FromUnion(union, "test-analyzer", "1.0.0");
var annotator = new RichGraphGateAnnotator(
detectors: new GateDetectors.IGateDetector[] { new GateDetectors.AuthGateDetector() },
codeProvider: new NullCodeContentProvider(),
multiplierCalculator: new GateMultiplierCalculator(),
logger: NullLogger<RichGraphGateAnnotator>.Instance);
var annotated = await annotator.AnnotateAsync(graph);
Assert.Single(annotated.Edges);
var edge = annotated.Edges[0];
Assert.NotNull(edge.Gates);
Assert.Single(edge.Gates);
Assert.Equal(GateType.AuthRequired, edge.Gates[0].Type);
Assert.Equal(3000, edge.GateMultiplierBps);
}
private sealed class NullCodeContentProvider : GateDetectors.ICodeContentProvider
{
public Task<string?> GetContentAsync(string filePath, CancellationToken ct = default)
=> Task.FromResult<string?>(null);
public Task<IReadOnlyList<string>?> GetLinesAsync(string filePath, int startLine, int endLine, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<string>?>(null);
}
}

View File

@@ -2,6 +2,7 @@ using System.IO;
using System.Threading.Tasks;
using StellaOps.Cryptography;
using StellaOps.Scanner.Reachability;
using StellaOps.Scanner.Reachability.Gates;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests;
@@ -63,4 +64,48 @@ public class RichGraphWriterTests
Assert.Contains("\"code_block_hash\":\"sha256:blockhash\"", json);
Assert.Contains("\"symbol\":{\"mangled\":\"_Zssl_read\",\"demangled\":\"ssl_read\",\"source\":\"DWARF\",\"confidence\":0.9}", json);
}
[Fact]
public async Task WritesGatesOnEdgesWhenPresent()
{
var writer = new RichGraphWriter(CryptoHashFactory.CreateDefault());
using var temp = new TempDir();
var union = new ReachabilityUnionGraph(
Nodes: new[]
{
new ReachabilityUnionNode("sym:dotnet:B", "dotnet", "method", "B"),
new ReachabilityUnionNode("sym:dotnet:A", "dotnet", "method", "A")
},
Edges: new[]
{
new ReachabilityUnionEdge("sym:dotnet:A", "sym:dotnet:B", "call", "high")
});
var rich = RichGraphBuilder.FromUnion(union, "test-analyzer", "1.0.0");
var gate = new DetectedGate
{
Type = GateType.AuthRequired,
Detail = "Auth required: ASP.NET Core Authorize attribute",
GuardSymbol = "sym:dotnet:B",
Confidence = 0.95,
DetectionMethod = "annotation:\\[Authorize\\]"
};
rich = rich with
{
Edges = new[]
{
rich.Edges[0] with { Gates = new[] { gate }, GateMultiplierBps = 3000 }
}
};
var result = await writer.WriteAsync(rich, temp.Path, "analysis-gates");
var json = await File.ReadAllTextAsync(result.GraphPath);
Assert.Contains("\"gate_multiplier_bps\":3000", json);
Assert.Contains("\"gates\":[", json);
Assert.Contains("\"type\":\"authRequired\"", json);
Assert.Contains("\"guard_symbol\":\"sym:dotnet:B\"", json);
}
}

View File

@@ -8,6 +8,7 @@
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />