Initial commit (history squashed)
Some checks failed
Build Test Deploy / authority-container (push) Has been cancelled
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
Build Test Deploy / build-test (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Build Test Deploy / authority-container (push) Has been cancelled
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
Build Test Deploy / build-test (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Feedser.Exporter.Json;
|
||||
using StellaOps.Feedser.Models;
|
||||
|
||||
namespace StellaOps.Feedser.Exporter.Json.Tests;
|
||||
|
||||
public sealed class JsonExportSnapshotBuilderTests : IDisposable
|
||||
{
|
||||
private readonly string _root;
|
||||
|
||||
public JsonExportSnapshotBuilderTests()
|
||||
{
|
||||
_root = Directory.CreateTempSubdirectory("feedser-json-export-tests").FullName;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WritesDeterministicTree()
|
||||
{
|
||||
var options = new JsonExportOptions { OutputRoot = _root };
|
||||
var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
|
||||
var exportedAt = DateTimeOffset.Parse("2024-07-15T12:00:00Z", CultureInfo.InvariantCulture);
|
||||
|
||||
var advisories = new[]
|
||||
{
|
||||
CreateAdvisory(
|
||||
advisoryKey: "CVE-2024-9999",
|
||||
aliases: new[] { "GHSA-zzzz-yyyy-xxxx", "CVE-2024-9999" },
|
||||
title: "Deterministic Snapshot",
|
||||
severity: "critical"),
|
||||
CreateAdvisory(
|
||||
advisoryKey: "VENDOR-2024-42",
|
||||
aliases: new[] { "ALIAS-1", "ALIAS-2" },
|
||||
title: "Vendor Advisory",
|
||||
severity: "medium"),
|
||||
};
|
||||
|
||||
var result = await builder.WriteAsync(advisories, exportedAt, cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal(advisories.Length, result.AdvisoryCount);
|
||||
Assert.Equal(exportedAt, result.ExportedAt);
|
||||
|
||||
var expectedFiles = result.FilePaths.OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||
Assert.Contains("nvd/2024/CVE-2024-9999.json", expectedFiles);
|
||||
Assert.Contains("misc/VENDOR-2024-42.json", expectedFiles);
|
||||
|
||||
var cvePath = ResolvePath(result.ExportDirectory, "nvd/2024/CVE-2024-9999.json");
|
||||
Assert.True(File.Exists(cvePath));
|
||||
var actualJson = await File.ReadAllTextAsync(cvePath, CancellationToken.None);
|
||||
Assert.Equal(SnapshotSerializer.ToSnapshot(advisories[0]), actualJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProducesIdenticalBytesAcrossRuns()
|
||||
{
|
||||
var options = new JsonExportOptions { OutputRoot = _root };
|
||||
var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
|
||||
var exportedAt = DateTimeOffset.Parse("2024-05-01T00:00:00Z", CultureInfo.InvariantCulture);
|
||||
var advisories = new[]
|
||||
{
|
||||
CreateAdvisory("CVE-2024-1000", new[] { "CVE-2024-1000", "GHSA-aaaa-bbbb-cccc" }, "Snapshot Stable", "high"),
|
||||
};
|
||||
|
||||
var first = await builder.WriteAsync(advisories, exportedAt, exportName: "20240501T000000Z", CancellationToken.None);
|
||||
var firstDigest = ComputeDigest(first);
|
||||
|
||||
var second = await builder.WriteAsync(advisories, exportedAt, exportName: "20240501T000000Z", CancellationToken.None);
|
||||
var secondDigest = ComputeDigest(second);
|
||||
|
||||
Assert.Equal(Convert.ToHexString(firstDigest), Convert.ToHexString(secondDigest));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_NormalizesInputOrdering()
|
||||
{
|
||||
var options = new JsonExportOptions { OutputRoot = _root };
|
||||
var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
|
||||
var exportedAt = DateTimeOffset.Parse("2024-06-01T00:00:00Z", CultureInfo.InvariantCulture);
|
||||
|
||||
var advisoryA = CreateAdvisory("CVE-2024-1000", new[] { "CVE-2024-1000" }, "Alpha", "high");
|
||||
var advisoryB = CreateAdvisory("VENDOR-0001", new[] { "VENDOR-0001" }, "Vendor Advisory", "medium");
|
||||
|
||||
var result = await builder.WriteAsync(new[] { advisoryB, advisoryA }, exportedAt, cancellationToken: CancellationToken.None);
|
||||
|
||||
var expectedOrder = result.FilePaths.OrderBy(path => path, StringComparer.Ordinal).ToArray();
|
||||
Assert.Equal(expectedOrder, result.FilePaths.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_EnumeratesStreamOnlyOnce()
|
||||
{
|
||||
var options = new JsonExportOptions { OutputRoot = _root };
|
||||
var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
|
||||
var exportedAt = DateTimeOffset.Parse("2024-08-01T00:00:00Z", CultureInfo.InvariantCulture);
|
||||
|
||||
var advisories = new[]
|
||||
{
|
||||
CreateAdvisory("CVE-2024-2000", new[] { "CVE-2024-2000" }, "Streaming One", "medium"),
|
||||
CreateAdvisory("CVE-2024-2001", new[] { "CVE-2024-2001" }, "Streaming Two", "low"),
|
||||
};
|
||||
|
||||
var sequence = new SingleEnumerationAsyncSequence(advisories);
|
||||
var result = await builder.WriteAsync(sequence, exportedAt, cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal(advisories.Length, result.AdvisoryCount);
|
||||
}
|
||||
|
||||
private static Advisory CreateAdvisory(string advisoryKey, string[] aliases, string title, string severity)
|
||||
{
|
||||
return new Advisory(
|
||||
advisoryKey: advisoryKey,
|
||||
title: title,
|
||||
summary: null,
|
||||
language: "EN",
|
||||
published: DateTimeOffset.Parse("2024-01-01T00:00:00Z", CultureInfo.InvariantCulture),
|
||||
modified: DateTimeOffset.Parse("2024-01-02T00:00:00Z", CultureInfo.InvariantCulture),
|
||||
severity: severity,
|
||||
exploitKnown: false,
|
||||
aliases: aliases,
|
||||
references: new[]
|
||||
{
|
||||
new AdvisoryReference("https://example.com/advisory", "advisory", null, null, AdvisoryProvenance.Empty),
|
||||
},
|
||||
affectedPackages: new[]
|
||||
{
|
||||
new AffectedPackage(
|
||||
AffectedPackageTypes.SemVer,
|
||||
"sample/package",
|
||||
platform: null,
|
||||
versionRanges: Array.Empty<AffectedVersionRange>(),
|
||||
statuses: Array.Empty<AffectedPackageStatus>(),
|
||||
provenance: Array.Empty<AdvisoryProvenance>()),
|
||||
},
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: new[]
|
||||
{
|
||||
new AdvisoryProvenance("feedser", "normalized", "canonical", DateTimeOffset.Parse("2024-01-02T00:00:00Z", CultureInfo.InvariantCulture)),
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] ComputeDigest(JsonExportResult result)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
foreach (var relative in result.FilePaths.OrderBy(x => x, StringComparer.Ordinal))
|
||||
{
|
||||
var fullPath = ResolvePath(result.ExportDirectory, relative);
|
||||
var bytes = File.ReadAllBytes(fullPath);
|
||||
sha256.TransformBlock(bytes, 0, bytes.Length, null, 0);
|
||||
}
|
||||
|
||||
sha256.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||
return sha256.Hash ?? Array.Empty<byte>();
|
||||
}
|
||||
|
||||
private static string ResolvePath(string root, string relative)
|
||||
{
|
||||
var segments = relative.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
return Path.Combine(new[] { root }.Concat(segments).ToArray());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_root))
|
||||
{
|
||||
Directory.Delete(_root, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best effort cleanup
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SingleEnumerationAsyncSequence : IAsyncEnumerable<Advisory>
|
||||
{
|
||||
private readonly IReadOnlyList<Advisory> _advisories;
|
||||
private int _enumerated;
|
||||
|
||||
public SingleEnumerationAsyncSequence(IReadOnlyList<Advisory> advisories)
|
||||
{
|
||||
_advisories = advisories ?? throw new ArgumentNullException(nameof(advisories));
|
||||
}
|
||||
|
||||
public IAsyncEnumerator<Advisory> GetAsyncEnumerator(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _enumerated, 1) == 1)
|
||||
{
|
||||
throw new InvalidOperationException("Sequence was enumerated more than once.");
|
||||
}
|
||||
|
||||
return Enumerate(cancellationToken);
|
||||
|
||||
async IAsyncEnumerator<Advisory> Enumerate([EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
foreach (var advisory in _advisories)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
yield return advisory;
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user