tests fixes and sprints work

This commit is contained in:
master
2026-01-22 19:08:46 +02:00
parent c32fff8f86
commit 726d70dc7f
881 changed files with 134434 additions and 6228 deletions

View File

@@ -0,0 +1,51 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.7",
"serialNumber": "urn:uuid:00000000-0000-0000-0000-000000000000",
"version": 1,
"metadata": {
"component": {
"bom-ref": "root",
"name": "sample-app",
"version": "1.0.0"
}
},
"services": [
{
"bom-ref": "svc-api",
"name": "api-gateway",
"version": "2.1.0",
"endpoints": [
"https://api.example.com",
"http://legacy.example.com"
],
"authenticated": false,
"crossesTrustBoundary": true,
"properties": [
{ "name": "x-trust-boundary", "value": "external" },
{ "name": "x-rate-limited", "value": "false" }
],
"data": [
{
"direction": "outbound",
"classification": "PII",
"destination": "svc-auth"
}
],
"services": [
{
"bom-ref": "svc-auth",
"name": "auth",
"version": "1.0.0",
"authenticated": false,
"endpoints": [
"http://auth.internal"
],
"properties": [
{ "name": "x-trust-boundary", "value": "internal" }
]
}
]
}
]
}

View File

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

View File

@@ -0,0 +1,105 @@
using System.Diagnostics;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Concelier.SbomIntegration.Parsing;
using StellaOps.Scanner.ServiceSecurity;
using StellaOps.Scanner.ServiceSecurity.Analyzers;
using StellaOps.Scanner.ServiceSecurity.Policy;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Scanner.ServiceSecurity.Tests;
public sealed class ServiceSecurityIntegrationTests
{
private static readonly TimeProvider FixedTimeProviderInstance =
new FixedTimeProvider(new DateTimeOffset(2026, 1, 19, 0, 0, 0, TimeSpan.Zero));
private static readonly string FixturesRoot = Path.Combine(
AppContext.BaseDirectory,
"Fixtures");
[Trait("Category", TestCategories.Integration)]
[Fact]
public async Task ParsedSbom_WithServices_ProducesFindings()
{
var sbomPath = Path.Combine(FixturesRoot, "sample-services.cdx.json");
await using var stream = File.OpenRead(sbomPath);
var parser = new ParsedSbomParser(NullLogger<ParsedSbomParser>.Instance);
var parsed = await parser.ParseAsync(stream, SbomFormat.CycloneDX);
var analyzer = new ServiceSecurityAnalyzer(
new IServiceSecurityCheck[]
{
new EndpointSchemeAnalyzer(),
new AuthenticationAnalyzer(),
new RateLimitingAnalyzer(),
new TrustBoundaryAnalyzer(),
new DataFlowAnalyzer(),
new NestedServiceAnalyzer()
},
FixedTimeProviderInstance);
var report = await analyzer.AnalyzeAsync(parsed.Services, ServiceSecurityPolicyDefaults.Default);
Assert.NotEmpty(report.Findings);
Assert.NotEmpty(report.DependencyChains);
Assert.NotNull(report.DataFlowGraph);
Assert.NotNull(report.Topology);
}
[Trait("Category", TestCategories.Performance)]
[Fact]
public async Task AnalyzeAsync_HandlesHundredServicesQuickly()
{
var services = Enumerable.Range(0, 100)
.Select(index => new ParsedService
{
BomRef = $"svc-{index}",
Name = $"service-{index}",
Authenticated = index % 2 == 0,
CrossesTrustBoundary = index % 3 == 0,
Endpoints = ["https://service.example.com"],
Data =
[
new ParsedDataFlow
{
Direction = DataFlowDirection.Outbound,
Classification = index % 2 == 0 ? "PII" : "public",
DestinationRef = $"svc-{(index + 1) % 100}"
}
]
})
.ToArray();
var analyzer = new ServiceSecurityAnalyzer(
new IServiceSecurityCheck[]
{
new EndpointSchemeAnalyzer(),
new AuthenticationAnalyzer(),
new RateLimitingAnalyzer(),
new TrustBoundaryAnalyzer(),
new DataFlowAnalyzer()
},
FixedTimeProviderInstance);
var stopwatch = Stopwatch.StartNew();
var report = await analyzer.AnalyzeAsync(services, ServiceSecurityPolicyDefaults.Default);
stopwatch.Stop();
Assert.NotNull(report);
Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(5));
}
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _fixed;
public FixedTimeProvider(DateTimeOffset fixedTime)
{
_fixed = fixedTime;
}
public override DateTimeOffset GetUtcNow() => _fixed;
}
}

View File

@@ -0,0 +1,66 @@
using StellaOps.Scanner.ServiceSecurity.Policy;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Scanner.ServiceSecurity.Tests;
public sealed class ServiceSecurityPolicyLoaderTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LoadAsync_ReturnsDefaultWhenMissing()
{
var loader = new ServiceSecurityPolicyLoader();
var policy = await loader.LoadAsync(path: null);
Assert.Equal(ServiceSecurityPolicyDefaults.Default.DataClassifications.Sensitive,
policy.DataClassifications.Sensitive);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LoadAsync_LoadsYamlPolicy()
{
var yaml = """
serviceSecurityPolicy:
requireAuthentication:
forTrustBoundaryCrossing: false
forSensitiveData: true
exceptions:
- servicePattern: "internal-*"
reason: "mTLS"
allowedSchemes:
external: [https]
internal: [https, http]
dataClassifications:
sensitive: [pii, auth]
deprecatedServices:
- name: "redis"
beforeVersion: "6.0"
cveId: "CVE-2026-0001"
internalHostSuffixes: ["internal", "corp"]
version: "policy-1"
""";
var loader = new ServiceSecurityPolicyLoader();
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.yaml");
try
{
await File.WriteAllTextAsync(path, yaml);
var policy = await loader.LoadAsync(path);
Assert.False(policy.RequireAuthentication.ForTrustBoundaryCrossing);
Assert.Contains("https", policy.AllowedSchemes.External, StringComparer.OrdinalIgnoreCase);
Assert.Contains("internal", policy.InternalHostSuffixes, StringComparer.OrdinalIgnoreCase);
Assert.Equal("policy-1", policy.Version);
Assert.Single(policy.DeprecatedServices);
}
finally
{
if (File.Exists(path))
{
File.Delete(path);
}
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.ServiceSecurity/StellaOps.Scanner.ServiceSecurity.csproj" />
<ProjectReference Include="../../../Concelier/__Libraries/StellaOps.Concelier.SbomIntegration/StellaOps.Concelier.SbomIntegration.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>