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

- 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:
master
2025-12-16 16:40:19 +02:00
parent 415eff1207
commit 2170a58734
206 changed files with 30547 additions and 534 deletions

View File

@@ -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
};
}
}

View File

@@ -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");
}
}