Finish off old sprints
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user