Finish off old sprints

This commit is contained in:
master
2026-02-18 15:01:04 +02:00
parent af4f261de8
commit 1bcab39a2c
28 changed files with 2534 additions and 330 deletions

View File

@@ -280,7 +280,7 @@ public sealed class AstraConnector : IFeedConnector
// Create base provenance record
var baseProvenance = new AdvisoryProvenance(
source: AstraOptions.SourceName,
source: AstraConnectorPlugin.SourceName,
kind: "oval-definition",
value: definition.DefinitionId,
recordedAt: recordedAt,
@@ -379,7 +379,7 @@ public sealed class AstraConnector : IFeedConnector
/// <remarks>
/// Temporary model until full OVAL schema mapping is implemented.
/// </remarks>
internal sealed record AstraVulnerabilityDefinition
public sealed record AstraVulnerabilityDefinition
{
public required string DefinitionId { get; init; }
public required string Title { get; init; }
@@ -393,7 +393,7 @@ internal sealed record AstraVulnerabilityDefinition
/// <summary>
/// Represents an affected package from OVAL test/state elements.
/// </summary>
internal sealed record AstraAffectedPackage
public sealed record AstraAffectedPackage
{
public required string PackageName { get; init; }
public string? MinVersion { get; init; }

View File

@@ -58,6 +58,13 @@ public sealed class OvalParser
return Array.Empty<AstraVulnerabilityDefinition>();
}
// Validate this is an OVAL document by checking root element namespace
if (root.Name.Namespace != OvalDefsNs)
{
throw new OvalParseException(
$"Invalid OVAL document: expected root element in namespace '{OvalDefsNs}', got '{root.Name.Namespace}'");
}
// Extract definitions, tests, objects, and states
var definitions = ExtractDefinitions(root);
var tests = ExtractTests(root);
@@ -91,6 +98,10 @@ public sealed class OvalParser
_logger.LogDebug("Parsed {Count} vulnerability definitions from OVAL XML", results.Count);
return results;
}
catch (OvalParseException)
{
throw; // Re-throw validation exceptions as-is
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to parse OVAL XML");
@@ -281,15 +292,17 @@ public sealed class OvalParser
continue;
}
var evrElement = stateElement.Element(DpkgNs + "evr");
var version = evrElement?.Value ?? string.Empty;
var operation = evrElement?.Attribute("operation")?.Value ?? "less than";
var evrElements = stateElement.Elements(DpkgNs + "evr").ToList();
var constraints = evrElements.Select(evr => new OvalVersionConstraint
{
Version = evr.Value ?? string.Empty,
Operation = evr.Attribute("operation")?.Value ?? "less than"
}).ToList();
states.Add(new OvalState
{
Id = id,
Version = version,
Operation = operation
Constraints = constraints
});
}
@@ -318,17 +331,32 @@ public sealed class OvalParser
string? fixedVersion = null;
string? maxVersion = null;
string? minVersion = null;
if (!string.IsNullOrEmpty(test.StateRef) && stateLookup.TryGetValue(test.StateRef, out var state))
{
// Parse operation to determine if this is a fixed version or affected version range
if (state.Operation.Contains("less than", StringComparison.OrdinalIgnoreCase))
foreach (var constraint in state.Constraints)
{
fixedVersion = state.Version; // Versions less than this are affected
}
else
{
maxVersion = state.Version;
if (constraint.Operation.Contains("less than", StringComparison.OrdinalIgnoreCase) &&
!constraint.Operation.Contains("or equal", StringComparison.OrdinalIgnoreCase))
{
// "less than" -> versions below this are affected; this is the fixed version
fixedVersion = constraint.Version;
}
else if (constraint.Operation.Contains("less than or equal", StringComparison.OrdinalIgnoreCase))
{
// "less than or equal" -> upper bound of affected range
maxVersion = constraint.Version;
}
else if (constraint.Operation.Contains("greater than or equal", StringComparison.OrdinalIgnoreCase))
{
// "greater than or equal" -> lower bound of affected range
minVersion = constraint.Version;
}
else if (constraint.Operation.Contains("greater than", StringComparison.OrdinalIgnoreCase))
{
minVersion = constraint.Version;
}
}
}
@@ -340,7 +368,7 @@ public sealed class OvalParser
PackageName = obj.PackageName,
FixedVersion = fixedVersion,
MaxVersion = maxVersion,
MinVersion = null
MinVersion = minVersion
});
}
}
@@ -377,6 +405,11 @@ public sealed class OvalParser
private sealed record OvalState
{
public required string Id { get; init; }
public required List<OvalVersionConstraint> Constraints { get; init; }
}
private sealed record OvalVersionConstraint
{
public required string Version { get; init; }
public required string Operation { get; init; }
}

View File

@@ -498,23 +498,3 @@ public sealed class AstraConnectorIntegrationTests
#endregion
}
// Make internal types accessible for testing
internal sealed record AstraVulnerabilityDefinition
{
public required string DefinitionId { get; init; }
public required string Title { get; init; }
public string? Description { get; init; }
public required string[] CveIds { get; init; }
public string? Severity { get; init; }
public DateTimeOffset? PublishedDate { get; init; }
public required AstraAffectedPackage[] AffectedPackages { get; init; }
}
internal sealed record AstraAffectedPackage
{
public required string PackageName { get; init; }
public string? MinVersion { get; init; }
public string? MaxVersion { get; init; }
public string? FixedVersion { get; init; }
}

View File

@@ -63,6 +63,10 @@ public sealed class FeedSnapshotPinningServiceTests
.Setup(x => x.GetLatestAsync("test-site-01", It.IsAny<CancellationToken>()))
.ReturnsAsync((SyncLedgerEntity?)null);
_snapshotRepositoryMock
.Setup(x => x.GetBySourceAndIdAsync(sourceId, snapshotId, It.IsAny<CancellationToken>()))
.ReturnsAsync((FeedSnapshotEntity?)null);
_snapshotRepositoryMock
.Setup(x => x.InsertAsync(It.IsAny<FeedSnapshotEntity>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((FeedSnapshotEntity e, CancellationToken _) => e);
@@ -130,6 +134,10 @@ public sealed class FeedSnapshotPinningServiceTests
BundleHash = "sha256:prev"
});
_snapshotRepositoryMock
.Setup(x => x.GetBySourceAndIdAsync(sourceId, snapshotId, It.IsAny<CancellationToken>()))
.ReturnsAsync((FeedSnapshotEntity?)null);
_snapshotRepositoryMock
.Setup(x => x.InsertAsync(It.IsAny<FeedSnapshotEntity>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((FeedSnapshotEntity e, CancellationToken _) => e);
@@ -388,6 +396,10 @@ public sealed class FeedSnapshotPinningServiceTests
.Setup(x => x.GetLatestAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((SyncLedgerEntity?)null);
_snapshotRepositoryMock
.Setup(x => x.GetBySourceAndIdAsync(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((FeedSnapshotEntity?)null);
_snapshotRepositoryMock
.Setup(x => x.InsertAsync(It.IsAny<FeedSnapshotEntity>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((FeedSnapshotEntity e, CancellationToken _) => e);

View File

@@ -10,6 +10,7 @@ using System.Net.Http.Json;
using FluentAssertions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
@@ -17,6 +18,7 @@ using Moq;
using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Interest;
using StellaOps.Concelier.Interest.Models;
using StellaOps.Concelier.WebService.Options;
using Xunit;
using StellaOps.TestKit;
@@ -307,12 +309,26 @@ public sealed class InterestScoreEndpointTests : IClassFixture<InterestScoreEndp
/// </summary>
public sealed class InterestScoreTestFactory : WebApplicationFactory<Program>
{
private const string TestConnectionString = "Host=localhost;Port=5432;Database=test-interest";
public Guid ExistingCanonicalId { get; } = Guid.NewGuid();
public Guid ComputeCanonicalId { get; } = Guid.NewGuid();
public Guid E2ECanonicalId { get; } = Guid.NewGuid();
private readonly Dictionary<Guid, List<SbomMatch>> _sbomMatches = new();
public InterestScoreTestFactory()
{
// Set environment variables before Program.Main executes.
// Program.cs reads these during configuration binding in the Testing environment.
Environment.SetEnvironmentVariable("CONCELIER__POSTGRESSTORAGE__CONNECTIONSTRING", TestConnectionString);
Environment.SetEnvironmentVariable("CONCELIER__POSTGRESSTORAGE__COMMANDTIMEOUTSECONDS", "30");
Environment.SetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN", TestConnectionString);
Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1");
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
}
public void AddSbomMatchForCanonical(Guid canonicalId)
{
if (!_sbomMatches.ContainsKey(canonicalId))
@@ -338,18 +354,62 @@ public sealed class InterestScoreEndpointTests : IClassFixture<InterestScoreEndp
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
Environment.SetEnvironmentVariable("CONCELIER__STORAGE__DSN", "Host=localhost;Port=5432;Database=test-interest");
Environment.SetEnvironmentVariable("CONCELIER__STORAGE__DRIVER", "postgres");
Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1");
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
builder.UseEnvironment("Testing");
builder.ConfigureAppConfiguration((_, config) =>
{
var overrides = new Dictionary<string, string?>
{
{"PostgresStorage:ConnectionString", TestConnectionString},
{"PostgresStorage:CommandTimeoutSeconds", "30"},
{"Telemetry:Enabled", "false"}
};
config.AddInMemoryCollection(overrides);
});
builder.UseSetting("CONCELIER__POSTGRESSTORAGE__CONNECTIONSTRING", TestConnectionString);
builder.UseSetting("CONCELIER__POSTGRESSTORAGE__COMMANDTIMEOUTSECONDS", "30");
builder.UseSetting("CONCELIER__TELEMETRY__ENABLED", "false");
builder.ConfigureServices(services =>
{
services.RemoveAll<ILeaseStore>();
services.AddSingleton<ILeaseStore, Fixtures.TestLeaseStore>();
// Inject ConcelierOptions with proper Postgres configuration
services.AddSingleton(new ConcelierOptions
{
PostgresStorage = new ConcelierOptions.PostgresStorageOptions
{
ConnectionString = TestConnectionString,
CommandTimeoutSeconds = 30
},
Telemetry = new ConcelierOptions.TelemetryOptions
{
Enabled = false
}
});
services.AddSingleton<IConfigureOptions<ConcelierOptions>>(sp => new ConfigureOptions<ConcelierOptions>(opts =>
{
opts.PostgresStorage ??= new ConcelierOptions.PostgresStorageOptions();
opts.PostgresStorage.ConnectionString = TestConnectionString;
opts.PostgresStorage.CommandTimeoutSeconds = 30;
opts.Telemetry ??= new ConcelierOptions.TelemetryOptions();
opts.Telemetry.Enabled = false;
}));
services.PostConfigure<ConcelierOptions>(opts =>
{
opts.PostgresStorage ??= new ConcelierOptions.PostgresStorageOptions();
opts.PostgresStorage.ConnectionString = TestConnectionString;
opts.PostgresStorage.CommandTimeoutSeconds = 30;
opts.Telemetry ??= new ConcelierOptions.TelemetryOptions();
opts.Telemetry.Enabled = false;
});
// Remove existing registrations
var scoringServiceDescriptor = services
.SingleOrDefault(d => d.ServiceType == typeof(IInterestScoringService));