Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented EmailChannelTestProvider to generate email preview payloads. - Implemented SlackChannelTestProvider to create Slack message previews. - Implemented TeamsChannelTestProvider for generating Teams Adaptive Card previews. - Implemented WebhookChannelTestProvider to create webhook payloads. - Added INotifyChannelTestProvider interface for channel-specific preview generation. - Created ChannelTestPreviewContracts for request and response models. - Developed NotifyChannelTestService to handle test send requests and generate previews. - Added rate limit policies for test sends and delivery history. - Implemented unit tests for service registration and binding. - Updated project files to include necessary dependencies and configurations.
219 lines
8.9 KiB
C#
219 lines
8.9 KiB
C#
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.Concelier.Exporter.Json;
|
|
using StellaOps.Concelier.Models;
|
|
|
|
namespace StellaOps.Concelier.Exporter.Json.Tests;
|
|
|
|
public sealed class JsonExportSnapshotBuilderTests : IDisposable
|
|
{
|
|
private readonly string _root;
|
|
|
|
public JsonExportSnapshotBuilderTests()
|
|
{
|
|
_root = Directory.CreateTempSubdirectory("concelier-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(advisories.Length, result.Advisories.Length);
|
|
Assert.Equal(
|
|
advisories.Select(a => a.AdvisoryKey).OrderBy(key => key, StringComparer.Ordinal),
|
|
result.Advisories.Select(a => a.AdvisoryKey).OrderBy(key => key, StringComparer.Ordinal));
|
|
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);
|
|
Assert.Equal(advisories.Length, result.Advisories.Length);
|
|
}
|
|
|
|
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("concelier", "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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|