fix tests. new product advisories enhancements

This commit is contained in:
master
2026-01-25 19:11:36 +02:00
parent c70e83719e
commit 6e687b523a
504 changed files with 40610 additions and 3785 deletions

View File

@@ -269,7 +269,9 @@ public sealed class RiskBundleBuilder
if (!string.IsNullOrWhiteSpace(entry.SignaturePath) && File.Exists(entry.SignaturePath))
{
using var sigStream = new FileStream(entry.SignaturePath, FileMode.Open, FileAccess.Read, FileShare.Read, 64 * 1024, FileOptions.SequentialScan);
var sigEntry = new PaxTarEntry(TarEntryType.RegularFile, $"{Path.GetDirectoryName(entry.BundlePath)?.TrimEnd('/')}/signature")
// Use forward slashes for tar paths regardless of platform
var bundleDir = Path.GetDirectoryName(entry.BundlePath)?.Replace('\\', '/').TrimEnd('/');
var sigEntry = new PaxTarEntry(TarEntryType.RegularFile, $"{bundleDir}/signature")
{
Mode = DefaultFileMode,
ModificationTime = FixedTimestamp,

View File

@@ -54,25 +54,29 @@ public static class ExportDownloadHelper
using var sha256 = SHA256.Create();
await using var fileStream = File.Create(outputPath);
await using var cryptoStream = new CryptoStream(fileStream, sha256, CryptoStreamMode.Write);
var buffer = new byte[DefaultBufferSize];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0)
// Write to file and compute hash simultaneously
{
await cryptoStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
}
await using var fileStream = File.Create(outputPath);
await using var cryptoStream = new CryptoStream(fileStream, sha256, CryptoStreamMode.Write);
await cryptoStream.FlushFinalBlockAsync(cancellationToken).ConfigureAwait(false);
while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0)
{
await cryptoStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
}
await cryptoStream.FlushFinalBlockAsync(cancellationToken).ConfigureAwait(false);
}
// File is now closed after the using block
var actualHash = Convert.ToHexString(sha256.Hash!).ToLowerInvariant();
var expectedNormalized = expectedSha256.ToLowerInvariant().Replace("sha256:", "");
if (!string.Equals(actualHash, expectedNormalized, StringComparison.Ordinal))
{
// Delete the corrupted file
// Delete the corrupted file - file is now closed so this works on Windows
File.Delete(outputPath);
throw new InvalidOperationException(
$"Checksum verification failed. Expected: {expectedNormalized}, Actual: {actualHash}");

View File

@@ -239,6 +239,12 @@ public sealed record JsonRedactionOptions
/// </summary>
public IReadOnlyList<string> RedactPatterns { get; init; } = [];
/// <summary>
/// Whether to use the default sensitive field names for redaction.
/// Defaults to true.
/// </summary>
public bool UseDefaultSensitiveFields { get; init; } = true;
/// <summary>
/// Common sensitive field names to always redact.
/// </summary>

View File

@@ -52,8 +52,8 @@ public sealed partial class JsonNormalizer
var redactedCount = 0;
// Apply redaction
if (_redactionOptions.RedactFields.Count > 0)
// Apply redaction (always check default sensitive fields, plus explicit redact fields if specified)
if (_redactionOptions.RedactFields.Count > 0 || _redactionOptions.UseDefaultSensitiveFields)
{
redactedCount = RedactFields(node, _redactionOptions.RedactFields, "");
}
@@ -65,9 +65,16 @@ public sealed partial class JsonNormalizer
}
// Sort keys if requested
if (_normalizationOptions.SortKeys && node is JsonObject rootObject)
if (_normalizationOptions.SortKeys)
{
node = SortKeys(rootObject);
if (node is JsonObject rootObject)
{
node = SortKeys(rootObject);
}
else if (node is JsonArray rootArray)
{
node = SortKeysInArray(rootArray);
}
}
// Normalize timestamps
@@ -192,11 +199,14 @@ public sealed partial class JsonNormalizer
return true;
}
// Check default sensitive fields
foreach (var sensitive in JsonRedactionOptions.DefaultSensitiveFields)
// Check default sensitive fields if enabled
if (_redactionOptions.UseDefaultSensitiveFields)
{
if (fieldName.Contains(sensitive, StringComparison.OrdinalIgnoreCase))
return true;
foreach (var sensitive in JsonRedactionOptions.DefaultSensitiveFields)
{
if (fieldName.Contains(sensitive, StringComparison.OrdinalIgnoreCase))
return true;
}
}
return false;

View File

@@ -8,6 +8,10 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.ExportCenter.Tests" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.ExportCenter.Core\StellaOps.ExportCenter.Core.csproj" />
<ProjectReference Include="..\..\..\TimelineIndexer\StellaOps.TimelineIndexer\StellaOps.TimelineIndexer.Core\StellaOps.TimelineIndexer.Core.csproj" />
@@ -23,6 +27,6 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Db\Migrations\*.sql" />
<EmbeddedResource Include="Db\Migrations\**\*.sql" />
</ItemGroup>
</Project>

View File

@@ -15,13 +15,22 @@ public sealed class BootstrapPackBuilderTests : IDisposable
private readonly string _tempDir;
private readonly BootstrapPackBuilder _builder;
private readonly ICryptoHash _cryptoHash;
private static readonly DateTimeOffset FixedTime = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
public BootstrapPackBuilderTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"bootstrap-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_cryptoHash = new FakeCryptoHash();
_builder = new BootstrapPackBuilder(_cryptoHash);
// Use a fixed time provider for deterministic tests
_builder = new BootstrapPackBuilder(_cryptoHash, new FakeTimeProvider(FixedTime));
}
private sealed class FakeTimeProvider : TimeProvider
{
private readonly DateTimeOffset _utcNow;
public FakeTimeProvider(DateTimeOffset utcNow) => _utcNow = utcNow;
public override DateTimeOffset GetUtcNow() => _utcNow;
}
public void Dispose()

View File

@@ -240,11 +240,13 @@ public sealed class ExportManifestWriterTests : IDisposable
[Fact]
public async Task WriteAsync_NoOutputDirectory_ReturnsJsonButNoFiles()
{
var exportId = Guid.NewGuid();
var tenantId = Guid.NewGuid();
var request = new ExportManifestWriteRequest(
Guid.NewGuid(),
Guid.NewGuid(),
CreateManifestContent(),
CreateProvenanceContent(),
exportId,
tenantId,
CreateManifestContent(exportId, tenantId),
CreateProvenanceContent(exportId, tenantId),
SigningOptions: null,
OutputDirectory: null);
@@ -261,11 +263,13 @@ public sealed class ExportManifestWriterTests : IDisposable
public async Task WriteAsync_CreatesOutputDirectory()
{
var newDir = Path.Combine(_tempDir, "new-export");
var exportId = Guid.NewGuid();
var tenantId = Guid.NewGuid();
var request = new ExportManifestWriteRequest(
Guid.NewGuid(),
Guid.NewGuid(),
CreateManifestContent(),
CreateProvenanceContent(),
exportId,
tenantId,
CreateManifestContent(exportId, tenantId),
CreateProvenanceContent(exportId, tenantId),
SigningOptions: null,
OutputDirectory: newDir);
@@ -356,21 +360,23 @@ public sealed class ExportManifestWriterTests : IDisposable
private ExportManifestWriteRequest CreateRequest(
ExportManifestSigningOptions? signingOptions = null)
{
var exportId = Guid.NewGuid();
var tenantId = Guid.NewGuid();
return new ExportManifestWriteRequest(
Guid.NewGuid(),
Guid.NewGuid(),
CreateManifestContent(),
CreateProvenanceContent(),
exportId,
tenantId,
CreateManifestContent(exportId, tenantId),
CreateProvenanceContent(exportId, tenantId),
signingOptions,
_tempDir);
}
private ExportManifestContent CreateManifestContent()
private ExportManifestContent CreateManifestContent(Guid exportId, Guid tenantId)
{
return new ExportManifestContent(
"v1",
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString(),
exportId.ToString(),
tenantId.ToString(),
new ExportManifestProfile(null, "mirror", "full"),
new ExportManifestScope(
["sbom", "vex"],
@@ -392,12 +398,12 @@ public sealed class ExportManifestWriterTests : IDisposable
"sha256:root-hash-here");
}
private ExportProvenanceContent CreateProvenanceContent()
private ExportProvenanceContent CreateProvenanceContent(Guid exportId, Guid tenantId)
{
return new ExportProvenanceContent(
"v1",
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString(),
exportId.ToString(),
tenantId.ToString(),
[
new ExportProvenanceSubject("export-bundle.tgz", new Dictionary<string, string>
{

View File

@@ -15,13 +15,22 @@ public sealed class MirrorBundleBuilderTests : IDisposable
private readonly string _tempDir;
private readonly MirrorBundleBuilder _builder;
private readonly ICryptoHash _cryptoHash;
private static readonly DateTimeOffset FixedTime = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
public MirrorBundleBuilderTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"mirror-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_cryptoHash = new FakeCryptoHash();
_builder = new MirrorBundleBuilder(_cryptoHash);
// Use a fixed time provider for deterministic tests
_builder = new MirrorBundleBuilder(_cryptoHash, new FakeTimeProvider(FixedTime));
}
private sealed class FakeTimeProvider : TimeProvider
{
private readonly DateTimeOffset _utcNow;
public FakeTimeProvider(DateTimeOffset utcNow) => _utcNow = utcNow;
public override DateTimeOffset GetUtcNow() => _utcNow;
}
public void Dispose()

View File

@@ -15,13 +15,22 @@ public sealed class PortableEvidenceExportBuilderTests : IDisposable
private readonly string _tempDir;
private readonly PortableEvidenceExportBuilder _builder;
private readonly ICryptoHash _cryptoHash;
private static readonly DateTimeOffset FixedTime = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
public PortableEvidenceExportBuilderTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"portable-evidence-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_cryptoHash = new FakeCryptoHash();
_builder = new PortableEvidenceExportBuilder(_cryptoHash);
// Use a fixed time provider for deterministic tests
_builder = new PortableEvidenceExportBuilder(_cryptoHash, new FakeTimeProvider(FixedTime));
}
private sealed class FakeTimeProvider : TimeProvider
{
private readonly DateTimeOffset _utcNow;
public FakeTimeProvider(DateTimeOffset utcNow) => _utcNow = utcNow;
public override DateTimeOffset GetUtcNow() => _utcNow;
}
public void Dispose()

View File

@@ -352,6 +352,13 @@ public sealed partial class TrivyJavaDbAdapter : ITrivyJavaDbAdapter
return range;
}
// Simple less than or equal (check before less than to avoid partial match)
if (range.StartsWith("<= ") || range.StartsWith("<="))
{
var version = range.TrimStart('<', '=', ' ');
return $"(,{version}]";
}
// Simple less than
if (range.StartsWith("< ") || range.StartsWith("<"))
{
@@ -359,11 +366,11 @@ public sealed partial class TrivyJavaDbAdapter : ITrivyJavaDbAdapter
return $"(,{version})";
}
// Simple less than or equal
if (range.StartsWith("<= ") || range.StartsWith("<="))
// Simple greater than or equal (check before greater than to avoid partial match)
if (range.StartsWith(">= ") || range.StartsWith(">="))
{
var version = range.TrimStart('<', '=', ' ');
return $"(,{version}]";
var version = range.TrimStart('>', '=', ' ');
return $"[{version},)";
}
// Simple greater than
@@ -373,13 +380,6 @@ public sealed partial class TrivyJavaDbAdapter : ITrivyJavaDbAdapter
return $"({version},)";
}
// Simple greater than or equal
if (range.StartsWith(">= ") || range.StartsWith(">="))
{
var version = range.TrimStart('>', '=', ' ');
return $"[{version},)";
}
// Exact version
if (range.StartsWith("= ") || range.StartsWith("=="))
{

View File

@@ -96,15 +96,10 @@ public sealed partial class TrivyNamespaceMapper
}
var normalizedVendor = vendor.Trim().ToLowerInvariant();
// Remove spaces for namespace matching (e.g., "red hat" -> "redhat")
var normalizedVendorNoSpaces = normalizedVendor.Replace(" ", "");
// Check if vendor is in supported namespaces
if (_options.SupportedNamespaces.Count > 0 &&
!_options.SupportedNamespaces.Any(ns => normalizedVendor.Contains(ns, StringComparison.OrdinalIgnoreCase)))
{
return null;
}
// Try exact distribution mapping first
// Try exact distribution mapping first - if we have an exact mapping, use it regardless of namespace filter
var productKey = string.IsNullOrWhiteSpace(product) ? vendor : $"{vendor} {product}";
if (DistributionMappings.TryGetValue(productKey, out var mapped))
{
@@ -125,6 +120,15 @@ public sealed partial class TrivyNamespaceMapper
NamespaceKind.Distribution);
}
// For fallback cases (no exact mapping), check if vendor is in supported namespaces
if (_options.SupportedNamespaces.Count > 0 &&
!_options.SupportedNamespaces.Any(ns =>
normalizedVendor.Contains(ns, StringComparison.OrdinalIgnoreCase) ||
normalizedVendorNoSpaces.Contains(ns, StringComparison.OrdinalIgnoreCase)))
{
return null;
}
// Try to extract version from product string
var versionMatch = VersionPattern().Match(product ?? "");
if (versionMatch.Success)