Restructure solution layout by module
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user