Add comprehensive security tests for OWASP A02, A05, A07, and A08 categories
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
- Implemented tests for Cryptographic Failures (A02) to ensure proper handling of sensitive data, secure algorithms, and key management. - Added tests for Security Misconfiguration (A05) to validate production configurations, security headers, CORS settings, and feature management. - Developed tests for Authentication Failures (A07) to enforce strong password policies, rate limiting, session management, and MFA support. - Created tests for Software and Data Integrity Failures (A08) to verify artifact signatures, SBOM integrity, attestation chains, and feed updates.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for gate detectors.
|
||||
/// </summary>
|
||||
public interface IGateDetector
|
||||
{
|
||||
/// <summary>The type of gate this detector finds.</summary>
|
||||
GateType GateType { get; }
|
||||
|
||||
/// <summary>Detects gates in the given call path.</summary>
|
||||
Task<IReadOnlyList<DetectedGate>> DetectAsync(
|
||||
CallPathContext context,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context for gate detection on a call path.
|
||||
/// </summary>
|
||||
public sealed record CallPathContext
|
||||
{
|
||||
/// <summary>Symbols in the call path from entry to vulnerability.</summary>
|
||||
public required IReadOnlyList<string> CallPath { get; init; }
|
||||
|
||||
/// <summary>Source files associated with each symbol (if available).</summary>
|
||||
public IReadOnlyDictionary<string, string>? SourceFiles { get; init; }
|
||||
|
||||
/// <summary>AST or CFG data for deeper analysis (optional).</summary>
|
||||
public object? AstData { get; init; }
|
||||
|
||||
/// <summary>Language of the code being analyzed.</summary>
|
||||
public required string Language { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composite gate detector that orchestrates all individual detectors.
|
||||
/// SPRINT_3405_0001_0001 - Task #7
|
||||
/// </summary>
|
||||
public sealed class CompositeGateDetector
|
||||
{
|
||||
private readonly IReadOnlyList<IGateDetector> _detectors;
|
||||
private readonly GateMultiplierConfig _config;
|
||||
private readonly ILogger<CompositeGateDetector> _logger;
|
||||
|
||||
public CompositeGateDetector(
|
||||
IEnumerable<IGateDetector> detectors,
|
||||
GateMultiplierConfig? config = null,
|
||||
ILogger<CompositeGateDetector>? logger = null)
|
||||
{
|
||||
_detectors = detectors?.ToList() ?? throw new ArgumentNullException(nameof(detectors));
|
||||
_config = config ?? GateMultiplierConfig.Default;
|
||||
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<CompositeGateDetector>.Instance;
|
||||
|
||||
if (_detectors.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("CompositeGateDetector initialized with no detectors");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects all gates in the given call path using all registered detectors.
|
||||
/// </summary>
|
||||
public async Task<GateDetectionResult> DetectAllAsync(
|
||||
CallPathContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
if (context.CallPath.Count == 0)
|
||||
{
|
||||
return GateDetectionResult.Empty;
|
||||
}
|
||||
|
||||
var allGates = new List<DetectedGate>();
|
||||
|
||||
// Run all detectors in parallel
|
||||
var tasks = _detectors.Select(async detector =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var gates = await detector.DetectAsync(context, cancellationToken);
|
||||
return gates;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Gate detector {DetectorType} failed for path with {PathLength} symbols",
|
||||
detector.GateType, context.CallPath.Count);
|
||||
return Array.Empty<DetectedGate>();
|
||||
}
|
||||
});
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
foreach (var gates in results)
|
||||
{
|
||||
allGates.AddRange(gates);
|
||||
}
|
||||
|
||||
// Deduplicate gates by symbol+type
|
||||
var uniqueGates = allGates
|
||||
.GroupBy(g => (g.GuardSymbol, g.Type))
|
||||
.Select(g => g.OrderByDescending(x => x.Confidence).First())
|
||||
.OrderByDescending(g => g.Confidence)
|
||||
.ToList();
|
||||
|
||||
// Calculate combined multiplier
|
||||
var combinedMultiplier = CalculateCombinedMultiplier(uniqueGates);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Detected {GateCount} gates on path, combined multiplier: {Multiplier}bps",
|
||||
uniqueGates.Count, combinedMultiplier);
|
||||
|
||||
return new GateDetectionResult
|
||||
{
|
||||
Gates = uniqueGates,
|
||||
CombinedMultiplierBps = combinedMultiplier,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the combined multiplier for all detected gates.
|
||||
/// Gates are multiplicative: auth(30%) * feature_flag(20%) = 6%
|
||||
/// </summary>
|
||||
private int CalculateCombinedMultiplier(IReadOnlyList<DetectedGate> gates)
|
||||
{
|
||||
if (gates.Count == 0)
|
||||
{
|
||||
return 10000; // 100% - no reduction
|
||||
}
|
||||
|
||||
// Start with 100% (10000 bps)
|
||||
double multiplier = 10000.0;
|
||||
|
||||
// Group gates by type and take the lowest multiplier per type
|
||||
// (multiple auth gates don't stack, but auth + feature_flag do)
|
||||
var gatesByType = gates
|
||||
.GroupBy(g => g.Type)
|
||||
.Select(g => g.Key);
|
||||
|
||||
foreach (var gateType in gatesByType)
|
||||
{
|
||||
var typeMultiplier = GetMultiplierForType(gateType);
|
||||
multiplier = multiplier * typeMultiplier / 10000.0;
|
||||
}
|
||||
|
||||
// Apply floor
|
||||
var result = (int)Math.Round(multiplier);
|
||||
return Math.Max(result, _config.MinimumMultiplierBps);
|
||||
}
|
||||
|
||||
private int GetMultiplierForType(GateType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
GateType.AuthRequired => _config.AuthRequiredMultiplierBps,
|
||||
GateType.FeatureFlag => _config.FeatureFlagMultiplierBps,
|
||||
GateType.AdminOnly => _config.AdminOnlyMultiplierBps,
|
||||
GateType.NonDefaultConfig => _config.NonDefaultConfigMultiplierBps,
|
||||
_ => 10000, // Unknown gate type - no reduction
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
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
|
||||
Assert.False(GateDetectionResult.Empty.HasGates);
|
||||
Assert.Empty(GateDetectionResult.Empty.Gates);
|
||||
Assert.Null(GateDetectionResult.Empty.PrimaryGate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateDetectionResult_WithGates_HasPrimaryGate()
|
||||
{
|
||||
// Arrange
|
||||
var gates = new[]
|
||||
{
|
||||
CreateGate(GateType.AuthRequired, 0.7),
|
||||
CreateGate(GateType.FeatureFlag, 0.9),
|
||||
};
|
||||
|
||||
var result = new GateDetectionResult { Gates = gates };
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasGates);
|
||||
Assert.Equal(2, result.Gates.Count);
|
||||
Assert.Equal(GateType.FeatureFlag, result.PrimaryGate?.Type); // Highest confidence
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateMultiplierConfig_Default_HasExpectedValues()
|
||||
{
|
||||
// Arrange
|
||||
var config = GateMultiplierConfig.Default;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3000, config.AuthRequiredMultiplierBps); // 30%
|
||||
Assert.Equal(2000, config.FeatureFlagMultiplierBps); // 20%
|
||||
Assert.Equal(1500, config.AdminOnlyMultiplierBps); // 15%
|
||||
Assert.Equal(5000, config.NonDefaultConfigMultiplierBps); // 50%
|
||||
Assert.Equal(500, config.MinimumMultiplierBps); // 5% floor
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_NoDetectors_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var detector = new CompositeGateDetector([]);
|
||||
var context = CreateContext(["main", "vulnerable_function"]);
|
||||
|
||||
// Act
|
||||
var result = await detector.DetectAllAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasGates);
|
||||
Assert.Equal(10000, result.CombinedMultiplierBps); // 100%
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_EmptyCallPath_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var detector = new CompositeGateDetector([new MockAuthDetector()]);
|
||||
var context = CreateContext([]);
|
||||
|
||||
// Act
|
||||
var result = await detector.DetectAllAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasGates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_SingleGate_AppliesMultiplier()
|
||||
{
|
||||
// Arrange
|
||||
var authDetector = new MockAuthDetector(
|
||||
CreateGate(GateType.AuthRequired, 0.95));
|
||||
var detector = new CompositeGateDetector([authDetector]);
|
||||
var context = CreateContext(["main", "auth_check", "vulnerable"]);
|
||||
|
||||
// Act
|
||||
var result = await detector.DetectAllAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasGates);
|
||||
Assert.Single(result.Gates);
|
||||
Assert.Equal(3000, result.CombinedMultiplierBps); // 30% from auth
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_MultipleGateTypes_MultipliesMultipliers()
|
||||
{
|
||||
// Arrange
|
||||
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"]);
|
||||
|
||||
// Act
|
||||
var result = await detector.DetectAllAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasGates);
|
||||
Assert.Equal(2, result.Gates.Count);
|
||||
// 30% * 20% = 6% (600 bps), but floor is 500 bps
|
||||
Assert.Equal(600, result.CombinedMultiplierBps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_DuplicateGates_Deduplicates()
|
||||
{
|
||||
// Arrange - two detectors finding same gate
|
||||
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"]);
|
||||
|
||||
// Act
|
||||
var result = await detector.DetectAllAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result.Gates); // Deduplicated
|
||||
Assert.Equal(0.9, result.Gates[0].Confidence); // Kept higher confidence
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_AllGateTypes_AppliesMinimumFloor()
|
||||
{
|
||||
// Arrange - all gate types = very low multiplier
|
||||
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"]);
|
||||
|
||||
// Act
|
||||
var result = await detector.DetectAllAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, result.Gates.Count);
|
||||
// 30% * 20% * 15% * 50% = 0.45%, but floor is 5% (500 bps)
|
||||
Assert.Equal(500, result.CombinedMultiplierBps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompositeGateDetector_DetectorException_ContinuesWithOthers()
|
||||
{
|
||||
// Arrange
|
||||
var failingDetector = new FailingGateDetector();
|
||||
var authDetector = new MockAuthDetector(
|
||||
CreateGate(GateType.AuthRequired, 0.9));
|
||||
|
||||
var detector = new CompositeGateDetector([failingDetector, authDetector]);
|
||||
var context = CreateContext(["main", "vulnerable"]);
|
||||
|
||||
// Act
|
||||
var result = await detector.DetectAllAsync(context);
|
||||
|
||||
// Assert - should still get auth gate despite failing detector
|
||||
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",
|
||||
};
|
||||
}
|
||||
|
||||
// Mock detectors for testing
|
||||
private 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 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 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 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 class FailingGateDetector : IGateDetector
|
||||
{
|
||||
public GateType GateType => GateType.AuthRequired;
|
||||
|
||||
public Task<IReadOnlyList<DetectedGate>> DetectAsync(CallPathContext context, CancellationToken ct)
|
||||
=> throw new InvalidOperationException("Simulated detector failure");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user