fix tests. new product advisories enhancements
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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("=="))
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user