Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,281 @@
using System.Collections.Generic;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Driver;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.Distro.Debian.Configuration;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Concelier.Testing;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Concelier.Connector.Distro.Debian.Tests;
[Collection("mongo-fixture")]
public sealed class DebianConnectorTests : IAsyncLifetime
{
private static readonly Uri ListUri = new("https://salsa.debian.org/security-tracker-team/security-tracker/-/raw/master/data/DSA/list");
private static readonly Uri DetailResolved = new("https://security-tracker.debian.org/tracker/DSA-2024-123");
private static readonly Uri DetailOpen = new("https://security-tracker.debian.org/tracker/DSA-2024-124");
private readonly MongoIntegrationFixture _fixture;
private readonly FakeTimeProvider _timeProvider;
private readonly CannedHttpMessageHandler _handler;
private readonly Dictionary<Uri, Func<HttpRequestMessage, HttpResponseMessage>> _fallbackFactories = new();
private readonly ITestOutputHelper _output;
public DebianConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
{
_fixture = fixture;
_handler = new CannedHttpMessageHandler();
_handler.SetFallback(request =>
{
if (request.RequestUri is null)
{
throw new InvalidOperationException("Request URI required for fallback response.");
}
if (_fallbackFactories.TryGetValue(request.RequestUri, out var factory))
{
return factory(request);
}
throw new InvalidOperationException($"No canned or fallback response registered for {request.Method} {request.RequestUri}.");
});
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 9, 12, 0, 0, 0, TimeSpan.Zero));
_output = output;
}
[Fact]
public async Task FetchParseMap_PopulatesRangePrimitivesAndResumesWithNotModified()
{
await using var provider = await BuildServiceProviderAsync();
SeedInitialResponses();
var connector = provider.GetRequiredService<DebianConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
_timeProvider.Advance(TimeSpan.FromMinutes(1));
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
Assert.Equal(2, advisories.Count);
var resolved = advisories.Single(a => a.AdvisoryKey == "DSA-2024-123");
_output.WriteLine("Resolved aliases: " + string.Join(",", resolved.Aliases));
var resolvedBookworm = Assert.Single(resolved.AffectedPackages, p => p.Platform == "bookworm");
var resolvedRange = Assert.Single(resolvedBookworm.VersionRanges);
Assert.Equal("evr", resolvedRange.RangeKind);
Assert.Equal("1:1.1.1n-0+deb11u2", resolvedRange.IntroducedVersion);
Assert.Equal("1:1.1.1n-0+deb11u5", resolvedRange.FixedVersion);
Assert.NotNull(resolvedRange.Primitives);
Assert.NotNull(resolvedRange.Primitives!.Evr);
Assert.Equal(1, resolvedRange.Primitives.Evr!.Introduced!.Epoch);
Assert.Equal("1.1.1n", resolvedRange.Primitives.Evr.Introduced.UpstreamVersion);
var open = advisories.Single(a => a.AdvisoryKey == "DSA-2024-124");
var openBookworm = Assert.Single(open.AffectedPackages, p => p.Platform == "bookworm");
var openRange = Assert.Single(openBookworm.VersionRanges);
Assert.Equal("evr", openRange.RangeKind);
Assert.Equal("1:1.3.1-1", openRange.IntroducedVersion);
Assert.Null(openRange.FixedVersion);
Assert.NotNull(openRange.Primitives);
Assert.NotNull(openRange.Primitives!.Evr);
// Ensure data persisted through Mongo round-trip.
var found = await advisoryStore.FindAsync("DSA-2024-123", CancellationToken.None);
Assert.NotNull(found);
var persistedRange = Assert.Single(found!.AffectedPackages, pkg => pkg.Platform == "bookworm").VersionRanges.Single();
Assert.NotNull(persistedRange.Primitives);
Assert.NotNull(persistedRange.Primitives!.Evr);
// Second run should issue conditional requests and no additional parsing/mapping.
SeedNotModifiedResponses();
await connector.FetchAsync(provider, CancellationToken.None);
_timeProvider.Advance(TimeSpan.FromMinutes(1));
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
var documents = provider.GetRequiredService<IDocumentStore>();
var listDoc = await documents.FindBySourceAndUriAsync(DebianConnectorPlugin.SourceName, DetailResolved.ToString(), CancellationToken.None);
Assert.NotNull(listDoc);
var refreshed = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
Assert.Equal(2, refreshed.Count);
}
private async Task<ServiceProvider> BuildServiceProviderAsync()
{
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName, CancellationToken.None);
_handler.Clear();
_fallbackFactories.Clear();
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddProvider(new TestOutputLoggerProvider(_output)));
services.AddSingleton<TimeProvider>(_timeProvider);
services.AddSingleton(_handler);
services.AddMongoStorage(options =>
{
options.ConnectionString = _fixture.Runner.ConnectionString;
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
options.CommandTimeout = TimeSpan.FromSeconds(5);
});
services.AddSourceCommon();
services.AddDebianConnector(options =>
{
options.ListEndpoint = ListUri;
options.DetailBaseUri = new Uri("https://security-tracker.debian.org/tracker/");
options.MaxAdvisoriesPerFetch = 10;
options.RequestDelay = TimeSpan.Zero;
});
services.Configure<HttpClientFactoryOptions>(DebianOptions.HttpClientName, builderOptions =>
{
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
{
builder.PrimaryHandler = _handler;
});
});
var provider = services.BuildServiceProvider();
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
await bootstrapper.InitializeAsync(CancellationToken.None);
return provider;
}
private void SeedInitialResponses()
{
AddListResponse("debian-list.txt", "\"list-v1\"");
AddDetailResponse(DetailResolved, "debian-detail-dsa-2024-123.html", "\"detail-123\"");
AddDetailResponse(DetailOpen, "debian-detail-dsa-2024-124.html", "\"detail-124\"");
}
private void SeedNotModifiedResponses()
{
AddNotModifiedResponse(ListUri, "\"list-v1\"");
AddNotModifiedResponse(DetailResolved, "\"detail-123\"");
AddNotModifiedResponse(DetailOpen, "\"detail-124\"");
}
private void AddListResponse(string fixture, string etag)
{
RegisterResponseFactory(ListUri, () =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/plain"),
};
response.Headers.ETag = new EntityTagHeaderValue(etag);
return response;
});
}
private void AddDetailResponse(Uri uri, string fixture, string etag)
{
RegisterResponseFactory(uri, () =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/html"),
};
response.Headers.ETag = new EntityTagHeaderValue(etag);
return response;
});
}
private void AddNotModifiedResponse(Uri uri, string etag)
{
RegisterResponseFactory(uri, () =>
{
var response = new HttpResponseMessage(HttpStatusCode.NotModified);
response.Headers.ETag = new EntityTagHeaderValue(etag);
return response;
});
}
private void RegisterResponseFactory(Uri uri, Func<HttpResponseMessage> factory)
{
_handler.AddResponse(uri, () => factory());
_fallbackFactories[uri] = _ => factory();
}
private static string ReadFixture(string filename)
{
var candidates = new[]
{
Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "Debian", "Fixtures", filename),
Path.Combine(AppContext.BaseDirectory, "Distro", "Debian", "Fixtures", filename),
Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Source", "Distro", "Debian", "Fixtures", filename),
};
foreach (var candidate in candidates)
{
var fullPath = Path.GetFullPath(candidate);
if (File.Exists(fullPath))
{
return File.ReadAllText(fullPath);
}
}
throw new FileNotFoundException($"Fixture '{filename}' not found", filename);
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => Task.CompletedTask;
private sealed class TestOutputLoggerProvider : ILoggerProvider
{
private readonly ITestOutputHelper _output;
public TestOutputLoggerProvider(ITestOutputHelper output) => _output = output;
public ILogger CreateLogger(string categoryName) => new TestOutputLogger(_output);
public void Dispose()
{
}
private sealed class TestOutputLogger : ILogger
{
private readonly ITestOutputHelper _output;
public TestOutputLogger(ITestOutputHelper output) => _output = output;
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullLogger.Instance.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => false;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (IsEnabled(logLevel))
{
_output.WriteLine(formatter(state, exception));
}
}
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using Xunit;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Distro.Debian;
using StellaOps.Concelier.Connector.Distro.Debian.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
namespace StellaOps.Concelier.Connector.Distro.Debian.Tests;
public sealed class DebianMapperTests
{
[Fact]
public void Map_BuildsRangePrimitives_ForResolvedPackage()
{
var dto = new DebianAdvisoryDto(
AdvisoryId: "DSA-2024-123",
SourcePackage: "openssl",
Title: "Openssl security update",
Description: "Fixes multiple issues.",
CveIds: new[] { "CVE-2024-1000", "CVE-2024-1001" },
Packages: new[]
{
new DebianPackageStateDto(
Package: "openssl",
Release: "bullseye",
Status: "resolved",
IntroducedVersion: "1:1.1.1n-0+deb11u2",
FixedVersion: "1:1.1.1n-0+deb11u5",
LastAffectedVersion: null,
Published: new DateTimeOffset(2024, 9, 1, 0, 0, 0, TimeSpan.Zero)),
new DebianPackageStateDto(
Package: "openssl",
Release: "bookworm",
Status: "open",
IntroducedVersion: null,
FixedVersion: null,
LastAffectedVersion: null,
Published: null)
},
References: new[]
{
new DebianReferenceDto(
Url: "https://security-tracker.debian.org/tracker/DSA-2024-123",
Kind: "advisory",
Title: "Debian Security Advisory 2024-123"),
});
var document = new DocumentRecord(
Id: Guid.NewGuid(),
SourceName: DebianConnectorPlugin.SourceName,
Uri: "https://security-tracker.debian.org/tracker/DSA-2024-123",
FetchedAt: new DateTimeOffset(2024, 9, 1, 1, 0, 0, TimeSpan.Zero),
Sha256: "sha",
Status: "Fetched",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: null,
LastModified: null,
GridFsId: null);
Advisory advisory = DebianMapper.Map(dto, document, new DateTimeOffset(2024, 9, 1, 2, 0, 0, TimeSpan.Zero));
Assert.Equal("DSA-2024-123", advisory.AdvisoryKey);
Assert.Contains("CVE-2024-1000", advisory.Aliases);
Assert.Contains("CVE-2024-1001", advisory.Aliases);
var resolvedPackage = Assert.Single(advisory.AffectedPackages, p => p.Platform == "bullseye");
var range = Assert.Single(resolvedPackage.VersionRanges);
Assert.Equal("evr", range.RangeKind);
Assert.Equal("1:1.1.1n-0+deb11u2", range.IntroducedVersion);
Assert.Equal("1:1.1.1n-0+deb11u5", range.FixedVersion);
Assert.NotNull(range.Primitives);
var evr = range.Primitives!.Evr;
Assert.NotNull(evr);
Assert.NotNull(evr!.Introduced);
Assert.Equal(1, evr.Introduced!.Epoch);
Assert.Equal("1.1.1n", evr.Introduced.UpstreamVersion);
Assert.Equal("0+deb11u2", evr.Introduced.Revision);
Assert.NotNull(evr.Fixed);
Assert.Equal(1, evr.Fixed!.Epoch);
Assert.Equal("1.1.1n", evr.Fixed.UpstreamVersion);
Assert.Equal("0+deb11u5", evr.Fixed.Revision);
var normalizedRule = Assert.Single(resolvedPackage.NormalizedVersions);
Assert.Equal(NormalizedVersionSchemes.Evr, normalizedRule.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.Range, normalizedRule.Type);
Assert.Equal("1:1.1.1n-0+deb11u2", normalizedRule.Min);
Assert.True(normalizedRule.MinInclusive);
Assert.Equal("1:1.1.1n-0+deb11u5", normalizedRule.Max);
Assert.False(normalizedRule.MaxInclusive);
Assert.Equal("debian:bullseye", normalizedRule.Notes);
var openPackage = Assert.Single(advisory.AffectedPackages, p => p.Platform == "bookworm");
Assert.Empty(openPackage.VersionRanges);
Assert.Empty(openPackage.NormalizedVersions);
}
}

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>DSA-2024-123</title>
</head>
<body>
<header><h1>DSA-2024-123</h1></header>
<table>
<tr><td><b>Name</b></td><td>DSA-2024-123</td></tr>
<tr><td><b>Description</b></td><td>openssl - security update</td></tr>
<tr><td><b>Source</b></td><td><a href="https://www.debian.org/security/dsa-2024-123">Debian</a></td></tr>
<tr><td><b>References</b></td><td><a href="/tracker/CVE-2024-1000">CVE-2024-1000</a>, <a href="/tracker/CVE-2024-1001">CVE-2024-1001</a></td></tr>
</table>
<h2>Vulnerable and fixed packages</h2>
<table>
<tr><th>Source Package</th><th>Release</th><th>Version</th><th>Status</th></tr>
<tr><td><a href="/tracker/source-package/openssl">openssl</a></td><td>bookworm</td><td><span class="red">1:1.1.1n-0+deb11u2</span></td><td><span class="red">vulnerable</span></td></tr>
<tr><td></td><td>bookworm (security)</td><td>1:1.1.1n-0+deb11u5</td><td>fixed</td></tr>
<tr><td></td><td>trixie</td><td><span class="red">3.0.8-2</span></td><td><span class="red">vulnerable</span></td></tr>
<tr><td></td><td>trixie (security)</td><td>3.0.12-1</td><td>fixed</td></tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>DSA-2024-124</title>
</head>
<body>
<header><h1>DSA-2024-124</h1></header>
<table>
<tr><td><b>Name</b></td><td>DSA-2024-124</td></tr>
<tr><td><b>Description</b></td><td>zlib - security update</td></tr>
<tr><td><b>Source</b></td><td><a href="https://www.debian.org/security/dsa-2024-124">Debian</a></td></tr>
<tr><td><b>References</b></td><td><a href="/tracker/CVE-2024-2000">CVE-2024-2000</a></td></tr>
</table>
<h2>Vulnerable and fixed packages</h2>
<table>
<tr><th>Source Package</th><th>Release</th><th>Version</th><th>Status</th></tr>
<tr><td><a href="/tracker/source-package/zlib">zlib</a></td><td>bookworm</td><td><span class="red">1:1.3.1-1</span></td><td><span class="red">vulnerable</span></td></tr>
<tr><td></td><td>trixie</td><td><span class="red">1:1.3.1-2</span></td><td><span class="red">vulnerable</span></td></tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,7 @@
[12 Sep 2024] DSA-2024-123 openssl - security update
{CVE-2024-1000 CVE-2024-1001}
[bookworm] - openssl 1:1.1.1n-0+deb11u5
[trixie] - openssl 3.0.12-1
[10 Sep 2024] DSA-2024-124 zlib - security update
{CVE-2024-2000}
[bookworm] - zlib 1:1.3.2-1

View File

@@ -0,0 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Distro.Debian/StellaOps.Concelier.Connector.Distro.Debian.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
</ItemGroup>
</Project>