Rename Concelier Source modules to Connector
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
"suse-su-2025_0001-1.json","2025-01-21T10:00:00Z"
|
||||
"suse-su-2025_0002-1.json","2025-01-22T08:30:00Z"
|
||||
|
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"document": {
|
||||
"title": "openssl - security update",
|
||||
"tracking": {
|
||||
"id": "SUSE-SU-2025:0001-1",
|
||||
"initial_release_date": "2025-01-21T00:00:00Z",
|
||||
"current_release_date": "2025-01-21T00:00:00Z"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"category": "self",
|
||||
"summary": "SUSE notice",
|
||||
"url": "https://www.suse.com/security/cve/CVE-2025-0001/"
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
{
|
||||
"category": "summary",
|
||||
"text": "Security update for openssl"
|
||||
}
|
||||
]
|
||||
},
|
||||
"product_tree": {
|
||||
"branches": [
|
||||
{
|
||||
"category": "vendor",
|
||||
"name": "SUSE",
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_family",
|
||||
"name": "SUSE Linux Enterprise Server 15 SP5",
|
||||
"branches": [
|
||||
{
|
||||
"category": "architecture",
|
||||
"name": "x86_64",
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_version",
|
||||
"name": "openssl-1.1.1w-150500.17.25.1.x86_64",
|
||||
"product": {
|
||||
"name": "openssl-1.1.1w-150500.17.25.1.x86_64",
|
||||
"product_id": "SUSE Linux Enterprise Server 15 SP5:openssl-1.1.1w-150500.17.25.1.x86_64"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": "CVE-2025-0001",
|
||||
"product_status": {
|
||||
"recommended": [
|
||||
"SUSE Linux Enterprise Server 15 SP5:openssl-1.1.1w-150500.17.25.1.x86_64"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"document": {
|
||||
"title": "postgresql - investigation update",
|
||||
"tracking": {
|
||||
"id": "SUSE-SU-2025:0002-1",
|
||||
"initial_release_date": "2025-01-22T00:00:00Z",
|
||||
"current_release_date": "2025-01-22T00:00:00Z"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"category": "external",
|
||||
"summary": "Upstream CVE",
|
||||
"url": "https://www.postgresql.org/support/security/CVE-2025-0002/"
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
{
|
||||
"category": "summary",
|
||||
"text": "Investigation ongoing for postgresql security issue."
|
||||
}
|
||||
]
|
||||
},
|
||||
"product_tree": {
|
||||
"branches": [
|
||||
{
|
||||
"category": "vendor",
|
||||
"name": "SUSE",
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_family",
|
||||
"name": "openSUSE Tumbleweed",
|
||||
"branches": [
|
||||
{
|
||||
"category": "architecture",
|
||||
"name": "x86_64",
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_version",
|
||||
"name": "postgresql16-16.3-2.1.x86_64",
|
||||
"product": {
|
||||
"name": "postgresql16-16.3-2.1.x86_64",
|
||||
"product_id": "openSUSE Tumbleweed:postgresql16-16.3-2.1.x86_64"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": "CVE-2025-0002",
|
||||
"product_status": {
|
||||
"known_affected": [
|
||||
"openSUSE Tumbleweed:postgresql16-16.3-2.1.x86_64"
|
||||
],
|
||||
"under_investigation": [
|
||||
"openSUSE Tumbleweed:postgresql16-16.3-2.1.x86_64"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Concelier.Connector.Distro.Suse/StellaOps.Concelier.Connector.Distro.Suse.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="Source\Distro\Suse\Fixtures\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,168 @@
|
||||
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.Time.Testing;
|
||||
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.Suse;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse.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.Suse.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class SuseConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri ChangesUri = new("https://ftp.suse.com/pub/projects/security/csaf/changes.csv");
|
||||
private static readonly Uri AdvisoryResolvedUri = new("https://ftp.suse.com/pub/projects/security/csaf/suse-su-2025_0001-1.json");
|
||||
private static readonly Uri AdvisoryOpenUri = new("https://ftp.suse.com/pub/projects/security/csaf/suse-su-2025_0002-1.json");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public SuseConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 22, 0, 0, 0, TimeSpan.Zero));
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProcessesResolvedAndOpenNotices()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
|
||||
SeedInitialResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<SuseConnector>();
|
||||
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 == "SUSE-SU-2025:0001-1");
|
||||
var resolvedPackage = Assert.Single(resolved.AffectedPackages);
|
||||
var resolvedRange = Assert.Single(resolvedPackage.VersionRanges);
|
||||
Assert.Equal("nevra", resolvedRange.RangeKind);
|
||||
Assert.NotNull(resolvedRange.Primitives);
|
||||
Assert.NotNull(resolvedRange.Primitives!.Nevra?.Fixed);
|
||||
|
||||
var open = advisories.Single(a => a.AdvisoryKey == "SUSE-SU-2025:0002-1");
|
||||
var openPackage = Assert.Single(open.AffectedPackages);
|
||||
Assert.Equal(AffectedPackageStatusCatalog.UnderInvestigation, openPackage.Statuses.Single().Status);
|
||||
|
||||
SeedNotModifiedResponses();
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
_handler.AssertNoPendingResponses();
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
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.AddSuseConnector(options =>
|
||||
{
|
||||
options.ChangesEndpoint = ChangesUri;
|
||||
options.AdvisoryBaseUri = new Uri("https://ftp.suse.com/pub/projects/security/csaf/");
|
||||
options.MaxAdvisoriesPerFetch = 5;
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(SuseOptions.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()
|
||||
{
|
||||
_handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.OK, "suse-changes.csv", "\"changes-v1\""));
|
||||
_handler.AddResponse(AdvisoryResolvedUri, () => BuildResponse(HttpStatusCode.OK, "suse-su-2025_0001-1.json", "\"adv-1\""));
|
||||
_handler.AddResponse(AdvisoryOpenUri, () => BuildResponse(HttpStatusCode.OK, "suse-su-2025_0002-1.json", "\"adv-2\""));
|
||||
}
|
||||
|
||||
private void SeedNotModifiedResponses()
|
||||
{
|
||||
_handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.NotModified, "suse-changes.csv", "\"changes-v1\""));
|
||||
}
|
||||
|
||||
private HttpResponseMessage BuildResponse(HttpStatusCode statusCode, string fixture, string etag)
|
||||
{
|
||||
var response = new HttpResponseMessage(statusCode);
|
||||
if (statusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var contentType = fixture.EndsWith(".csv", StringComparison.OrdinalIgnoreCase) ? "text/csv" : "application/json";
|
||||
response.Content = new StringContent(ReadFixture(Path.Combine("Source", "Distro", "Suse", "Fixtures", fixture)), Encoding.UTF8, contentType);
|
||||
}
|
||||
|
||||
response.Headers.ETag = new EntityTagHeaderValue(etag);
|
||||
return response;
|
||||
}
|
||||
|
||||
private static string ReadFixture(string relativePath)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException($"Fixture '{relativePath}' not found.", path);
|
||||
}
|
||||
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Suse.Tests;
|
||||
|
||||
public sealed class SuseCsafParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_ProducesRecommendedAndAffectedPackages()
|
||||
{
|
||||
var json = ReadFixture("Source/Distro/Suse/Fixtures/suse-su-2025_0001-1.json");
|
||||
var dto = SuseCsafParser.Parse(json);
|
||||
|
||||
Assert.Equal("SUSE-SU-2025:0001-1", dto.AdvisoryId);
|
||||
Assert.Contains("CVE-2025-0001", dto.CveIds);
|
||||
var package = Assert.Single(dto.Packages);
|
||||
Assert.Equal("openssl", package.Package);
|
||||
Assert.Equal("resolved", package.Status);
|
||||
Assert.NotNull(package.FixedVersion);
|
||||
Assert.Equal("SUSE Linux Enterprise Server 15 SP5", package.Platform);
|
||||
Assert.Equal("openssl-1.1.1w-150500.17.25.1.x86_64", package.CanonicalNevra);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_HandlesOpenInvestigation()
|
||||
{
|
||||
var json = ReadFixture("Source/Distro/Suse/Fixtures/suse-su-2025_0002-1.json");
|
||||
var dto = SuseCsafParser.Parse(json);
|
||||
|
||||
Assert.Equal("SUSE-SU-2025:0002-1", dto.AdvisoryId);
|
||||
Assert.Contains("CVE-2025-0002", dto.CveIds);
|
||||
var package = Assert.Single(dto.Packages);
|
||||
Assert.Equal("open", package.Status);
|
||||
Assert.Equal("postgresql16", package.Package);
|
||||
Assert.NotNull(package.LastAffectedVersion);
|
||||
}
|
||||
|
||||
private static string ReadFixture(string relativePath)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException($"Fixture '{relativePath}' not found.", path);
|
||||
}
|
||||
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Suse.Tests;
|
||||
|
||||
public sealed class SuseMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void Map_BuildsNevraRangePrimitives()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "Suse", "Fixtures", "suse-su-2025_0001-1.json"));
|
||||
var dto = SuseCsafParser.Parse(json);
|
||||
|
||||
var document = new DocumentRecord(
|
||||
Guid.NewGuid(),
|
||||
SuseConnectorPlugin.SourceName,
|
||||
"https://ftp.suse.com/pub/projects/security/csaf/suse-su-2025_0001-1.json",
|
||||
DateTimeOffset.UtcNow,
|
||||
"sha256",
|
||||
DocumentStatuses.PendingParse,
|
||||
"application/json",
|
||||
Headers: null,
|
||||
Metadata: new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["suse.id"] = dto.AdvisoryId
|
||||
},
|
||||
Etag: "adv-1",
|
||||
LastModified: DateTimeOffset.UtcNow,
|
||||
GridFsId: ObjectId.Empty);
|
||||
|
||||
var mapped = SuseMapper.Map(dto, document, DateTimeOffset.UtcNow);
|
||||
|
||||
Assert.Equal(dto.AdvisoryId, mapped.AdvisoryKey);
|
||||
var package = Assert.Single(mapped.AffectedPackages);
|
||||
Assert.Equal(AffectedPackageTypes.Rpm, package.Type);
|
||||
var range = Assert.Single(package.VersionRanges);
|
||||
Assert.Equal("nevra", range.RangeKind);
|
||||
Assert.NotNull(range.Primitives);
|
||||
Assert.NotNull(range.Primitives!.Nevra);
|
||||
Assert.NotNull(range.Primitives.Nevra!.Fixed);
|
||||
Assert.Equal("openssl", range.Primitives.Nevra.Fixed!.Name);
|
||||
Assert.Equal("SUSE Linux Enterprise Server 15 SP5", package.Platform);
|
||||
|
||||
var normalizedRule = Assert.Single(package.NormalizedVersions);
|
||||
Assert.Equal(NormalizedVersionSchemes.Nevra, normalizedRule.Scheme);
|
||||
Assert.Equal($"suse:{package.Platform}", normalizedRule.Notes);
|
||||
var primitives = range.Primitives.Nevra;
|
||||
if (primitives!.Introduced is not null)
|
||||
{
|
||||
Assert.Equal(primitives.Introduced.ToCanonicalString(), normalizedRule.Min);
|
||||
}
|
||||
if (primitives.Fixed is not null)
|
||||
{
|
||||
Assert.Equal(primitives.Fixed.ToCanonicalString(), normalizedRule.Max);
|
||||
}
|
||||
if (primitives.LastAffected is not null && normalizedRule.Type is NormalizedVersionRuleTypes.Range or NormalizedVersionRuleTypes.LessThanOrEqual)
|
||||
{
|
||||
Assert.Equal(primitives.LastAffected.ToCanonicalString(), normalizedRule.Max);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user