sprints work.

This commit is contained in:
master
2026-01-20 00:45:38 +02:00
parent b34bde89fa
commit 4903395618
275 changed files with 52785 additions and 79 deletions

View File

@@ -0,0 +1,189 @@
using System.Reflection;
namespace StellaOps.BinaryIndex.GroundTruth.SecDb.Tests.Fixtures;
/// <summary>
/// Provides access to deterministic test fixtures for offline testing.
/// </summary>
public static class FixtureProvider
{
private static readonly string FixturesPath;
static FixtureProvider()
{
var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
FixturesPath = Path.Combine(assemblyDir, "Fixtures");
// Also try the source directory for development
if (!Directory.Exists(FixturesPath))
{
var sourceDir = FindSourceFixturesDirectory();
if (sourceDir is not null)
{
FixturesPath = sourceDir;
}
}
}
/// <summary>
/// Get sample SecDB YAML content for main repository.
/// </summary>
public static string GetSampleSecDbMain()
{
var path = Path.Combine(FixturesPath, "main.yaml");
if (!File.Exists(path))
{
// Return inline fixture if file doesn't exist
return SampleSecDbMainContent;
}
return File.ReadAllText(path);
}
/// <summary>
/// Get sample SecDB YAML content for community repository.
/// </summary>
public static string GetSampleSecDbCommunity()
{
var path = Path.Combine(FixturesPath, "community.yaml");
if (!File.Exists(path))
{
return SampleSecDbCommunityContent;
}
return File.ReadAllText(path);
}
/// <summary>
/// Get a fixture file as a stream.
/// </summary>
public static Stream GetFixtureStream(string name)
{
var path = Path.Combine(FixturesPath, name);
if (!File.Exists(path))
{
throw new FileNotFoundException($"Fixture not found: {path}");
}
return File.OpenRead(path);
}
/// <summary>
/// Check if a fixture exists.
/// </summary>
public static bool FixtureExists(string name)
{
var path = Path.Combine(FixturesPath, name);
return File.Exists(path);
}
private static string? FindSourceFixturesDirectory()
{
var dir = Directory.GetCurrentDirectory();
while (dir is not null)
{
var candidate = Path.Combine(dir, "src", "BinaryIndex", "__Tests",
"StellaOps.BinaryIndex.GroundTruth.SecDb.Tests", "Fixtures");
if (Directory.Exists(candidate))
{
return candidate;
}
dir = Directory.GetParent(dir)?.FullName;
}
return null;
}
/// <summary>
/// Inline sample SecDB main.yaml content for deterministic testing.
/// Based on Alpine SecDB format.
/// </summary>
private const string SampleSecDbMainContent = """
distroversion: v3.19
reponame: main
urlprefix: https://dl-cdn.alpinelinux.org/alpine/v3.19/main
packages:
- pkg: curl
secfixes:
8.5.0-r0:
- CVE-2023-46218 Improper validation of HTTP headers
- CVE-2023-46219 Double free in async URL resolver
8.4.0-r0:
- CVE-2023-38545 SOCKS5 heap buffer overflow
8.1.2-r0:
- CVE-2023-27535 FTP injection vulnerability
- pkg: openssl
secfixes:
3.1.4-r3:
- CVE-2024-0727 PKCS12 decoding crash
3.1.4-r0:
- CVE-2023-5678 Denial of service
3.1.2-r0:
- CVE-2023-3817 Excessive time checking DH parameters
- pkg: linux-lts
secfixes:
6.1.67-r0:
- CVE-2023-6817 Use-after-free in netfilter
- CVE-2023-6606 Out-of-bounds read in SMB
6.1.64-r0:
- CVE-2023-5717 User-mode root exploit via perf
""";
/// <summary>
/// Inline sample SecDB community.yaml content.
/// </summary>
private const string SampleSecDbCommunityContent = """
distroversion: v3.19
reponame: community
urlprefix: https://dl-cdn.alpinelinux.org/alpine/v3.19/community
packages:
- pkg: go
secfixes:
1.21.5-r0:
- CVE-2023-45283 Path traversal on Windows
- CVE-2023-45284 Runtime panic in crypto/tls
1.21.4-r0:
- CVE-2023-44487 HTTP/2 rapid reset attack
- pkg: nodejs
secfixes:
20.10.0-r0:
- CVE-2023-46809 Permissions policy bypass
20.9.0-r0:
- CVE-2023-38552 Integrity bypass via TLS/HTTPS
- pkg: chromium
secfixes:
120.0.6099.71-r0:
- CVE-2023-6702 Type confusion in V8
119.0.6045.199-r0:
- CVE-2023-6345 Integer overflow in Skia
- pkg: unfixed-example
secfixes:
"0":
- CVE-2023-99999 Example unfixed vulnerability
""";
}
/// <summary>
/// Test fixture constants for SecDB tests.
/// </summary>
public static class FixtureConstants
{
// Sample package info
public const string SamplePackageCurl = "curl";
public const string SamplePackageOpenssl = "openssl";
public const string SamplePackageGo = "go";
public const string SamplePackageNodejs = "nodejs";
// Sample branches
public const string SampleBranchV319 = "v3.19";
public const string SampleBranchEdge = "edge";
// Sample repositories
public const string SampleRepoMain = "main";
public const string SampleRepoCommunity = "community";
// Expected CVE counts
public const int ExpectedCurlCveCount = 4;
public const int ExpectedOpensslCveCount = 3;
// Sample CVEs
public const string SampleCveCurl = "CVE-2023-46218";
public const string SampleCveOpenssl = "CVE-2024-0727";
public const string SampleCveUnfixed = "CVE-2023-99999";
}

View File

@@ -0,0 +1,150 @@
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.BinaryIndex.GroundTruth.SecDb.Configuration;
using StellaOps.BinaryIndex.GroundTruth.SecDb.Tests.Fixtures;
using Xunit;
namespace StellaOps.BinaryIndex.GroundTruth.SecDb.Tests;
/// <summary>
/// Integration tests for SecDb connector.
/// These tests require network access to gitlab.alpinelinux.org.
/// Skip in CI by setting SKIP_INTEGRATION_TESTS=true.
/// </summary>
[Trait("Category", "Integration")]
public class SecDbConnectorIntegrationTests : IAsyncLifetime
{
private ServiceProvider? _services;
private readonly bool _skipTests;
public SecDbConnectorIntegrationTests()
{
_skipTests = Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS")?.ToLowerInvariant() == "true"
|| Environment.GetEnvironmentVariable("CI")?.ToLowerInvariant() == "true";
}
public Task InitializeAsync()
{
if (_skipTests)
return Task.CompletedTask;
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug));
services.AddSecDbConnector(opts =>
{
opts.Branches = ["v3.19"];
opts.Repositories = ["main"];
opts.TimeoutSeconds = 120;
opts.FetchAports = false; // Don't fetch aports for integration tests
});
_services = services.BuildServiceProvider();
return Task.CompletedTask;
}
public Task DisposeAsync()
{
_services?.Dispose();
return Task.CompletedTask;
}
[Fact]
public async Task SecDbConnector_CanTestConnectivity()
{
Skip.If(_skipTests, "Integration tests skipped");
// Arrange
var connector = _services!.GetRequiredService<SecDbConnector>();
// Act
var result = await connector.TestConnectivityAsync();
// Assert
result.IsConnected.Should().BeTrue("Should be able to connect to Alpine GitLab");
result.Latency.Should().BeLessThan(TimeSpan.FromSeconds(30));
}
[Fact]
public async Task SecDbConnector_CanGetMetadata()
{
Skip.If(_skipTests, "Integration tests skipped");
// Arrange
var connector = _services!.GetRequiredService<SecDbConnector>();
// Act
var metadata = await connector.GetMetadataAsync();
// Assert
metadata.SourceId.Should().Be("secdb-alpine");
metadata.DisplayName.Should().Contain("Alpine");
metadata.BaseUrl.Should().Contain("gitlab.alpinelinux.org");
}
[Fact]
public void SecDbConnector_HasCorrectProperties()
{
Skip.If(_skipTests, "Integration tests skipped");
// Arrange
var connector = _services!.GetRequiredService<SecDbConnector>();
// Assert
connector.SourceId.Should().Be("secdb-alpine");
connector.DisplayName.Should().Contain("SecDB");
connector.SupportedDistros.Should().Contain("alpine");
}
[Fact]
public async Task SecDbConnector_FetchAndGetVulnerabilities_ReturnsData()
{
Skip.If(_skipTests, "Integration tests skipped");
// Arrange
var connector = _services!.GetRequiredService<SecDbConnector>();
// First fetch the data
await connector.FetchAsync(_services!, CancellationToken.None);
// Act - get vulnerabilities for a well-known package
var vulnerabilities = await connector.GetVulnerabilitiesForPackageAsync("curl");
// Assert
vulnerabilities.Should().NotBeEmpty("curl should have known vulnerabilities");
vulnerabilities.Should().OnlyContain(v => v.CveId.StartsWith("CVE-"));
}
}
/// <summary>
/// Provides Skip functionality for xUnit when condition is true.
/// </summary>
public static class Skip
{
public static void If(bool condition, string reason)
{
if (condition)
{
throw new SkipException(reason);
}
}
}
/// <summary>
/// Exception to skip a test.
/// </summary>
public class SkipException : Exception
{
public SkipException(string reason) : base(reason) { }
}
/// <summary>
/// Test meter factory for diagnostics.
/// </summary>
internal sealed class TestMeterFactory : System.Diagnostics.Metrics.IMeterFactory
{
public System.Diagnostics.Metrics.Meter Create(System.Diagnostics.Metrics.MeterOptions options)
=> new(options.Name, options.Version);
public void Dispose() { }
}

View File

@@ -0,0 +1,273 @@
using FluentAssertions;
using StellaOps.BinaryIndex.GroundTruth.SecDb.Internal;
using StellaOps.BinaryIndex.GroundTruth.SecDb.Tests.Fixtures;
using Xunit;
namespace StellaOps.BinaryIndex.GroundTruth.SecDb.Tests;
/// <summary>
/// Unit tests for SecDbParser using deterministic fixtures.
/// </summary>
public class SecDbParserTests
{
private readonly SecDbParser _parser = new();
[Fact]
public void Parse_SampleMainYaml_ParsesDistroVersion()
{
// Arrange
var content = FixtureProvider.GetSampleSecDbMain();
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
result.DistroVersion.Should().Be(FixtureConstants.SampleBranchV319);
result.RepoName.Should().Be(FixtureConstants.SampleRepoMain);
}
[Fact]
public void Parse_SampleMainYaml_ExtractsPackages()
{
// Arrange
var content = FixtureProvider.GetSampleSecDbMain();
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
result.Packages.Should().HaveCountGreaterThanOrEqualTo(3);
result.Packages.Should().Contain(p => p.Name == FixtureConstants.SamplePackageCurl);
result.Packages.Should().Contain(p => p.Name == FixtureConstants.SamplePackageOpenssl);
}
[Fact]
public void Parse_SampleMainYaml_ExtractsCurlVulnerabilities()
{
// Arrange
var content = FixtureProvider.GetSampleSecDbMain();
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
var curl = result.Packages.First(p => p.Name == FixtureConstants.SamplePackageCurl);
curl.Vulnerabilities.Should().HaveCount(FixtureConstants.ExpectedCurlCveCount);
curl.Vulnerabilities.Should().Contain(v => v.CveId == FixtureConstants.SampleCveCurl);
}
[Fact]
public void Parse_SampleMainYaml_ExtractsFixedVersions()
{
// Arrange
var content = FixtureProvider.GetSampleSecDbMain();
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
var curl = result.Packages.First(p => p.Name == FixtureConstants.SamplePackageCurl);
var cve = curl.Vulnerabilities.First(v => v.CveId == FixtureConstants.SampleCveCurl);
cve.FixedInVersion.Should().Be("8.5.0-r0");
cve.IsUnfixed.Should().BeFalse();
}
[Fact]
public void Parse_SampleCommunityYaml_ParsesCommunityPackages()
{
// Arrange
var content = FixtureProvider.GetSampleSecDbCommunity();
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoCommunity);
// Assert
result.Packages.Should().Contain(p => p.Name == FixtureConstants.SamplePackageGo);
result.Packages.Should().Contain(p => p.Name == FixtureConstants.SamplePackageNodejs);
}
[Fact]
public void Parse_SampleCommunityYaml_DetectsUnfixedVulnerabilities()
{
// Arrange
var content = FixtureProvider.GetSampleSecDbCommunity();
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoCommunity);
// Assert
var unfixedPkg = result.Packages.First(p => p.Name == "unfixed-example");
var unfixedCve = unfixedPkg.Vulnerabilities.First(v => v.CveId == FixtureConstants.SampleCveUnfixed);
unfixedCve.FixedInVersion.Should().Be("0");
unfixedCve.IsUnfixed.Should().BeTrue();
}
[Fact]
public void Parse_SampleMainYaml_CalculatesTotalVulnerabilityCount()
{
// Arrange
var content = FixtureProvider.GetSampleSecDbMain();
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
result.VulnerabilityCount.Should().BeGreaterThan(0);
result.VulnerabilityCount.Should().Be(result.Packages.Sum(p => p.Vulnerabilities.Count));
}
[Fact]
public void Parse_CveWithDescription_ExtractsDescription()
{
// Arrange
var content = FixtureProvider.GetSampleSecDbMain();
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
var curl = result.Packages.First(p => p.Name == FixtureConstants.SamplePackageCurl);
var cve = curl.Vulnerabilities.First(v => v.CveId == FixtureConstants.SampleCveCurl);
cve.Description.Should().Contain("HTTP headers");
}
[Fact]
public void Parse_EmptyContent_ThrowsFormatException()
{
// Act
var act = () => _parser.Parse("", FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
act.Should().Throw<FormatException>();
}
[Fact]
public void Parse_InvalidYaml_ThrowsFormatException()
{
// Arrange
var content = "this is not valid yaml: [incomplete";
// Act
var act = () => _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
act.Should().Throw<FormatException>();
}
[Fact]
public void Parse_EmptyPackagesArray_ReturnsEmptyPackageList()
{
// Arrange
var content = """
distroversion: v3.19
reponame: main
packages: []
""";
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
result.Packages.Should().BeEmpty();
}
[Fact]
public void Parse_PackageWithNoSecfixes_ReturnsEmptyVulnerabilities()
{
// Arrange
var content = """
distroversion: v3.19
reponame: main
packages:
- pkg: no-vulns-pkg
""";
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
result.Packages.Should().HaveCount(1);
result.Packages[0].Vulnerabilities.Should().BeEmpty();
}
[Fact]
public void Parse_NonCveEntry_SkipsNonCveIdentifiers()
{
// Arrange - Alpine secdb sometimes has XSA-xxx or other identifiers
var content = """
distroversion: v3.19
reponame: main
packages:
- pkg: xen
secfixes:
4.18.0-r1:
- XSA-445 Not a CVE
- CVE-2023-12345 Actual CVE
""";
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
var xen = result.Packages.First();
xen.Vulnerabilities.Should().HaveCount(1);
xen.Vulnerabilities[0].CveId.Should().Be("CVE-2023-12345");
}
[Fact]
public void Parse_CveIdNormalization_ConvertsToUppercase()
{
// Arrange
var content = """
distroversion: v3.19
reponame: main
packages:
- pkg: test
secfixes:
1.0-r0:
- cve-2023-12345 lowercase
""";
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
var pkg = result.Packages.First();
pkg.Vulnerabilities[0].CveId.Should().Be("CVE-2023-12345");
}
[Fact]
public void Parse_MultipleCvesInSameVersion_ParsesAll()
{
// Arrange
var content = FixtureProvider.GetSampleSecDbMain();
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
var linuxLts = result.Packages.First(p => p.Name == "linux-lts");
var version6167 = linuxLts.Vulnerabilities.Where(v => v.FixedInVersion == "6.1.67-r0").ToList();
version6167.Should().HaveCount(2);
}
[Fact]
public void Parse_SetsBranchAndRepository()
{
// Arrange
var content = FixtureProvider.GetSampleSecDbMain();
// Act
var result = _parser.Parse(content, FixtureConstants.SampleBranchV319, FixtureConstants.SampleRepoMain);
// Assert
result.Branch.Should().Be(FixtureConstants.SampleBranchV319);
result.Repository.Should().Be(FixtureConstants.SampleRepoMain);
foreach (var pkg in result.Packages)
{
pkg.Branch.Should().Be(FixtureConstants.SampleBranchV319);
pkg.Repository.Should().Be(FixtureConstants.SampleRepoMain);
}
}
}

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.BinaryIndex.GroundTruth.SecDb\StellaOps.BinaryIndex.GroundTruth.SecDb.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="Fixtures\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>