tests fixes and sprints work
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Concelier.SbomIntegration.Models;
|
||||
using StellaOps.Scanner.ServiceSecurity;
|
||||
using StellaOps.Scanner.ServiceSecurity.Analyzers;
|
||||
using StellaOps.Scanner.ServiceSecurity.Models;
|
||||
using StellaOps.Scanner.ServiceSecurity.Policy;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.ServiceSecurity.Tests;
|
||||
|
||||
public sealed class ServiceSecurityAnalyzerTests
|
||||
{
|
||||
private static readonly TimeProvider FixedTimeProviderInstance =
|
||||
new FixedTimeProvider(new DateTimeOffset(2026, 1, 19, 0, 0, 0, TimeSpan.Zero));
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EndpointSchemeAnalyzer_FlagsInsecureSchemes()
|
||||
{
|
||||
var service = CreateService(
|
||||
"svc-1",
|
||||
"api",
|
||||
endpoints:
|
||||
[
|
||||
"http://api.example.com",
|
||||
"https://api.example.com",
|
||||
"ws://api.example.com",
|
||||
"ftp://api.example.com"
|
||||
]);
|
||||
|
||||
var report = await RunAnalyzer(ServiceSecurityPolicyDefaults.Default, new EndpointSchemeAnalyzer(), service);
|
||||
|
||||
Assert.Equal(3, report.Findings.Length);
|
||||
Assert.Contains(report.Findings, finding => finding.Type == ServiceSecurityFindingType.InsecureEndpointScheme);
|
||||
Assert.Contains(report.Findings, finding => finding.Type == ServiceSecurityFindingType.DeprecatedProtocol);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AuthenticationAnalyzer_FlagsUnauthenticatedScenarios()
|
||||
{
|
||||
var service = CreateService(
|
||||
"svc-1",
|
||||
"billing",
|
||||
authenticated: false,
|
||||
crossesTrustBoundary: true,
|
||||
endpoints: ["https://billing.example.com"],
|
||||
data: [Flow(classification: "PII")]);
|
||||
|
||||
var report = await RunAnalyzer(ServiceSecurityPolicyDefaults.Default, new AuthenticationAnalyzer(), service);
|
||||
|
||||
Assert.Contains(report.Findings, finding => finding.Type == ServiceSecurityFindingType.UnauthenticatedEndpoint);
|
||||
Assert.Contains(report.Findings, finding => finding.Type == ServiceSecurityFindingType.CrossesTrustBoundaryWithoutAuth);
|
||||
Assert.Contains(report.Findings, finding => finding.Type == ServiceSecurityFindingType.SensitiveDataExposed);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TrustBoundaryAnalyzer_BuildsDependencyChains()
|
||||
{
|
||||
var serviceA = CreateService(
|
||||
"svc-a",
|
||||
"gateway",
|
||||
authenticated: true,
|
||||
endpoints: ["https://api.example.com"],
|
||||
data: [Flow(destinationRef: "svc-b", classification: "PII")],
|
||||
properties: BuildProperties(("x-trust-boundary", "external")));
|
||||
var serviceB = CreateService(
|
||||
"svc-b",
|
||||
"auth",
|
||||
authenticated: false,
|
||||
properties: BuildProperties(("x-trust-boundary", "internal")));
|
||||
|
||||
var report = await RunAnalyzer(ServiceSecurityPolicyDefaults.Default, new TrustBoundaryAnalyzer(), serviceA, serviceB);
|
||||
|
||||
Assert.NotEmpty(report.DependencyChains);
|
||||
Assert.Contains(report.Findings, finding => finding.Type == ServiceSecurityFindingType.CrossesTrustBoundaryWithoutAuth);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DataFlowAnalyzer_FlagsSensitiveUnencryptedFlows()
|
||||
{
|
||||
var serviceA = CreateService(
|
||||
"svc-a",
|
||||
"front",
|
||||
authenticated: true,
|
||||
data: [Flow(destinationRef: "svc-b", classification: "PII")]);
|
||||
var serviceB = CreateService(
|
||||
"svc-b",
|
||||
"processor",
|
||||
authenticated: false,
|
||||
endpoints: ["http://processor.internal"]);
|
||||
|
||||
var report = await RunAnalyzer(ServiceSecurityPolicyDefaults.Default, new DataFlowAnalyzer(), serviceA, serviceB);
|
||||
|
||||
Assert.Contains(report.Findings, finding => finding.Type == ServiceSecurityFindingType.SensitiveDataExposed);
|
||||
Assert.Contains(report.Findings, finding => finding.Type == ServiceSecurityFindingType.UnencryptedDataFlow);
|
||||
Assert.NotNull(report.DataFlowGraph);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RateLimitingAnalyzer_FlagsDisabledRateLimit()
|
||||
{
|
||||
var service = CreateService(
|
||||
"svc-1",
|
||||
"api",
|
||||
endpoints: ["https://api.example.com"],
|
||||
properties: BuildProperties(("x-rate-limited", "false")));
|
||||
|
||||
var report = await RunAnalyzer(ServiceSecurityPolicyDefaults.Default, new RateLimitingAnalyzer(), service);
|
||||
|
||||
Assert.Contains(report.Findings, finding => finding.Type == ServiceSecurityFindingType.MissingRateLimiting);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ServiceVulnerabilityMatcher_FlagsDeprecatedAndAdvisoryMatches()
|
||||
{
|
||||
var policy = ServiceSecurityPolicyDefaults.Default with
|
||||
{
|
||||
DeprecatedServices = ImmutableArray.Create(new DeprecatedServicePolicy
|
||||
{
|
||||
Name = "redis",
|
||||
BeforeVersion = "6.0",
|
||||
Severity = Severity.High,
|
||||
CveId = "CVE-2026-0001",
|
||||
Reason = "Pre-6.0 releases are out of support."
|
||||
})
|
||||
};
|
||||
var service = CreateService("svc-redis", "redis", version: "5.0.1");
|
||||
var provider = new StubAdvisoryProvider();
|
||||
|
||||
var report = await RunAnalyzer(policy, new ServiceVulnerabilityMatcher(provider), service);
|
||||
|
||||
Assert.Equal(2, report.Findings.Length);
|
||||
Assert.All(report.Findings, finding => Assert.Equal(ServiceSecurityFindingType.KnownVulnerableServiceVersion, finding.Type));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NestedServiceAnalyzer_DetectsCyclesAndOrphans()
|
||||
{
|
||||
var serviceA = CreateService(
|
||||
"svc-a",
|
||||
"alpha",
|
||||
data: [Flow(sourceRef: "svc-a", destinationRef: "svc-b", classification: "PII")]);
|
||||
var serviceB = CreateService(
|
||||
"svc-b",
|
||||
"beta",
|
||||
data: [Flow(sourceRef: "svc-b", destinationRef: "svc-a", classification: "PII")]);
|
||||
var orphan = CreateService("svc-c", "orphan");
|
||||
|
||||
var report = await RunAnalyzer(ServiceSecurityPolicyDefaults.Default, new NestedServiceAnalyzer(), serviceA, serviceB, orphan);
|
||||
|
||||
Assert.Contains(report.Findings, finding => finding.Type == ServiceSecurityFindingType.CircularDependency);
|
||||
Assert.Contains(report.Findings, finding => finding.Type == ServiceSecurityFindingType.OrphanedService);
|
||||
Assert.NotNull(report.Topology);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Analyzer_SummaryCountsMatchFindings()
|
||||
{
|
||||
var service = CreateService(
|
||||
"svc-1",
|
||||
"api",
|
||||
authenticated: false,
|
||||
endpoints: ["http://api.example.com"]);
|
||||
|
||||
var report = await RunAnalyzer(ServiceSecurityPolicyDefaults.Default, new EndpointSchemeAnalyzer(), new AuthenticationAnalyzer(), service);
|
||||
|
||||
Assert.Equal(report.Findings.Length, report.Summary.TotalFindings);
|
||||
Assert.True(report.Summary.FindingsByType.Count >= 2);
|
||||
}
|
||||
|
||||
private static async Task<ServiceSecurityReport> RunAnalyzer(
|
||||
ServiceSecurityPolicy policy,
|
||||
IServiceSecurityCheck check,
|
||||
params ParsedService[] services)
|
||||
{
|
||||
var analyzer = new ServiceSecurityAnalyzer(new[] { check }, FixedTimeProviderInstance);
|
||||
return await analyzer.AnalyzeAsync(services, policy);
|
||||
}
|
||||
|
||||
private static async Task<ServiceSecurityReport> RunAnalyzer(
|
||||
ServiceSecurityPolicy policy,
|
||||
IServiceSecurityCheck first,
|
||||
IServiceSecurityCheck second,
|
||||
params ParsedService[] services)
|
||||
{
|
||||
var analyzer = new ServiceSecurityAnalyzer(new IServiceSecurityCheck[] { first, second }, FixedTimeProviderInstance);
|
||||
return await analyzer.AnalyzeAsync(services, policy);
|
||||
}
|
||||
|
||||
private static ParsedService CreateService(
|
||||
string bomRef,
|
||||
string name,
|
||||
bool authenticated = true,
|
||||
bool crossesTrustBoundary = false,
|
||||
string? version = null,
|
||||
string[]? endpoints = null,
|
||||
ParsedDataFlow[]? data = null,
|
||||
ParsedService[]? nested = null,
|
||||
IReadOnlyDictionary<string, string>? properties = null)
|
||||
{
|
||||
var props = properties is null
|
||||
? ImmutableDictionary<string, string>.Empty
|
||||
: ImmutableDictionary.CreateRange(StringComparer.OrdinalIgnoreCase, properties);
|
||||
|
||||
return new ParsedService
|
||||
{
|
||||
BomRef = bomRef,
|
||||
Name = name,
|
||||
Version = version,
|
||||
Authenticated = authenticated,
|
||||
CrossesTrustBoundary = crossesTrustBoundary,
|
||||
Endpoints = endpoints?.ToImmutableArray() ?? [],
|
||||
Data = data?.ToImmutableArray() ?? [],
|
||||
NestedServices = nested?.ToImmutableArray() ?? [],
|
||||
Properties = props
|
||||
};
|
||||
}
|
||||
|
||||
private static ParsedDataFlow Flow(
|
||||
string? sourceRef = null,
|
||||
string? destinationRef = null,
|
||||
string classification = "PII",
|
||||
DataFlowDirection direction = DataFlowDirection.Outbound)
|
||||
{
|
||||
return new ParsedDataFlow
|
||||
{
|
||||
Direction = direction,
|
||||
Classification = classification,
|
||||
SourceRef = sourceRef,
|
||||
DestinationRef = destinationRef
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> BuildProperties(params (string Key, string Value)[] entries)
|
||||
{
|
||||
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
values[entry.Key] = entry.Value;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private sealed class StubAdvisoryProvider : IServiceAdvisoryProvider
|
||||
{
|
||||
public Task<IReadOnlyList<ServiceAdvisoryMatch>> GetMatchesAsync(
|
||||
ParsedService service,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<ServiceAdvisoryMatch>>(new[]
|
||||
{
|
||||
new ServiceAdvisoryMatch
|
||||
{
|
||||
CveId = "CVE-2026-1234",
|
||||
Severity = Severity.High,
|
||||
Description = "Service version is affected."
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixed;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset fixedTime)
|
||||
{
|
||||
_fixed = fixedTime;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixed;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user