Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized function calls.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Models.Observations;
|
||||
using StellaOps.Concelier.RawModels;
|
||||
using StellaOps.Concelier.WebService.Services;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests;
|
||||
|
||||
public class AdvisoryChunkBuilderTests
|
||||
{
|
||||
private readonly ICryptoHash _hash = CryptoHashFactory.CreateDefault();
|
||||
|
||||
[Fact]
|
||||
public void Build_UsesJsonPointerFromMaskForObservationPath()
|
||||
{
|
||||
var recordedAt = DateTimeOffset.Parse("2025-11-18T00:00:00Z", CultureInfo.InvariantCulture);
|
||||
var observationId = "obs-1";
|
||||
var observation = BuildObservation(observationId);
|
||||
var advisory = BuildAdvisory(recordedAt, observationId, new[] { "/references/0" });
|
||||
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
advisory.AdvisoryKey,
|
||||
fingerprint: "fp",
|
||||
chunkLimit: 5,
|
||||
observationLimit: 5,
|
||||
sectionFilter: ImmutableHashSet.Create("workaround"),
|
||||
formatFilter: ImmutableHashSet<string>.Empty,
|
||||
minimumLength: 1);
|
||||
|
||||
var builder = new AdvisoryChunkBuilder(_hash);
|
||||
var result = builder.Build(options, advisory, new[] { observation });
|
||||
|
||||
var entry = Assert.Single(result.Response.Entries);
|
||||
Assert.Equal("/references/0", entry.Provenance.ObservationPath);
|
||||
Assert.Contains("/references/0", entry.Provenance.FieldMask);
|
||||
|
||||
var expectedChunkId = ComputeChunkId(observationId, "/references/0");
|
||||
Assert.Equal(expectedChunkId, entry.ChunkId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_FallsBackToFieldPathWhenMaskMissing()
|
||||
{
|
||||
var recordedAt = DateTimeOffset.Parse("2025-11-18T00:00:00Z", CultureInfo.InvariantCulture);
|
||||
var observationId = "obs-2";
|
||||
var observation = BuildObservation(observationId);
|
||||
var advisory = BuildAdvisory(recordedAt, observationId, fieldMask: null);
|
||||
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
advisory.AdvisoryKey,
|
||||
fingerprint: "fp",
|
||||
chunkLimit: 5,
|
||||
observationLimit: 5,
|
||||
sectionFilter: ImmutableHashSet.Create("workaround"),
|
||||
formatFilter: ImmutableHashSet<string>.Empty,
|
||||
minimumLength: 1);
|
||||
|
||||
var builder = new AdvisoryChunkBuilder(_hash);
|
||||
var result = builder.Build(options, advisory, new[] { observation });
|
||||
|
||||
var entry = Assert.Single(result.Response.Entries);
|
||||
Assert.Equal("/references/0", entry.Provenance.ObservationPath);
|
||||
Assert.Contains("/references/0", entry.Provenance.FieldMask);
|
||||
|
||||
var expectedChunkId = ComputeChunkId(observationId, "/references/0");
|
||||
Assert.Equal(expectedChunkId, entry.ChunkId);
|
||||
}
|
||||
|
||||
private Advisory BuildAdvisory(DateTimeOffset recordedAt, string observationId, IEnumerable<string>? fieldMask)
|
||||
{
|
||||
var provenance = fieldMask is null
|
||||
? new AdvisoryProvenance("nvd", "workaround", observationId, recordedAt)
|
||||
: new AdvisoryProvenance("nvd", "workaround", observationId, recordedAt, fieldMask);
|
||||
|
||||
var reference = new AdvisoryReference(
|
||||
url: "https://example.test/workaround",
|
||||
kind: "workaround",
|
||||
sourceTag: "Vendor guidance",
|
||||
summary: "Apply the workaround.",
|
||||
provenance);
|
||||
|
||||
return new Advisory(
|
||||
advisoryKey: "CVE-2025-0001",
|
||||
title: "Test advisory",
|
||||
summary: "",
|
||||
language: "en",
|
||||
published: recordedAt,
|
||||
modified: recordedAt,
|
||||
severity: "high",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { "CVE-2025-0001" },
|
||||
references: new[] { reference },
|
||||
affectedPackages: Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: new[] { new AdvisoryProvenance("nvd", "advisory", observationId, recordedAt) });
|
||||
}
|
||||
|
||||
private static AdvisoryObservation BuildObservation(string observationId)
|
||||
{
|
||||
var timestamp = DateTimeOffset.Parse("2025-11-18T00:00:00Z", CultureInfo.InvariantCulture);
|
||||
|
||||
return new AdvisoryObservation(
|
||||
observationId,
|
||||
tenant: "tenant-a",
|
||||
source: new AdvisoryObservationSource("nvd", "stream", "api"),
|
||||
upstream: new AdvisoryObservationUpstream(
|
||||
upstreamId: observationId,
|
||||
documentVersion: "1",
|
||||
fetchedAt: timestamp,
|
||||
receivedAt: timestamp,
|
||||
contentHash: "sha256:deadbeef",
|
||||
signature: new AdvisoryObservationSignature(present: false)),
|
||||
content: new AdvisoryObservationContent("csaf", "2.0", JsonNode.Parse("{}")!),
|
||||
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
|
||||
rawLinkset: new RawLinkset(),
|
||||
createdAt: timestamp);
|
||||
}
|
||||
|
||||
private string ComputeChunkId(string documentId, string observationPath)
|
||||
{
|
||||
var input = string.Concat(documentId, '|', observationPath);
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
var digest = _hash.ComputeHash(bytes, HashAlgorithms.Sha256);
|
||||
return Convert.ToHexString(digest.AsSpan(0, 8));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Concelier.Models.Observations;
|
||||
using StellaOps.Concelier.RawModels;
|
||||
using StellaOps.Concelier.WebService.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests;
|
||||
|
||||
public class AdvisoryChunkCacheKeyTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_NormalizesObservationOrdering()
|
||||
{
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
AdvisoryKey: "CVE-2025-0001",
|
||||
Fingerprint: "fp",
|
||||
ChunkLimit: 10,
|
||||
ObservationLimit: 10,
|
||||
SectionFilter: ImmutableHashSet.Create("workaround"),
|
||||
FormatFilter: ImmutableHashSet<string>.Empty,
|
||||
MinimumLength: 8);
|
||||
|
||||
var first = BuildObservation("obs-1", "sha256:one", "2025-11-18T00:00:00Z");
|
||||
var second = BuildObservation("obs-2", "sha256:two", "2025-11-18T00:05:00Z");
|
||||
|
||||
var ordered = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0001", options, new[] { first, second }, "fp");
|
||||
var reversed = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0001", options, new[] { second, first }, "fp");
|
||||
|
||||
Assert.Equal(ordered.Value, reversed.Value);
|
||||
Assert.Equal(ordered.ComputeHash(), reversed.ComputeHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_NormalizesFilterCasing()
|
||||
{
|
||||
var optionsLower = new AdvisoryChunkBuildOptions(
|
||||
"CVE-2025-0002",
|
||||
Fingerprint: "fp",
|
||||
ChunkLimit: 5,
|
||||
ObservationLimit: 5,
|
||||
SectionFilter: ImmutableHashSet.Create("workaround", "fix"),
|
||||
FormatFilter: ImmutableHashSet.Create("ndjson"),
|
||||
MinimumLength: 1);
|
||||
|
||||
var optionsUpper = new AdvisoryChunkBuildOptions(
|
||||
"CVE-2025-0002",
|
||||
Fingerprint: "fp",
|
||||
ChunkLimit: 5,
|
||||
ObservationLimit: 5,
|
||||
SectionFilter: ImmutableHashSet.Create("WorkAround", "FIX"),
|
||||
FormatFilter: ImmutableHashSet.Create("NDJSON"),
|
||||
MinimumLength: 1);
|
||||
|
||||
var observation = BuildObservation("obs-3", "sha256:three", "2025-11-18T00:10:00Z");
|
||||
|
||||
var lower = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0002", optionsLower, new[] { observation }, "fp");
|
||||
var upper = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0002", optionsUpper, new[] { observation }, "fp");
|
||||
|
||||
Assert.Equal(lower.Value, upper.Value);
|
||||
Assert.Equal(lower.ComputeHash(), upper.ComputeHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_ChangesWhenContentHashDiffers()
|
||||
{
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
"CVE-2025-0003",
|
||||
Fingerprint: "fp",
|
||||
ChunkLimit: 5,
|
||||
ObservationLimit: 5,
|
||||
SectionFilter: ImmutableHashSet<string>.Empty,
|
||||
FormatFilter: ImmutableHashSet<string>.Empty,
|
||||
MinimumLength: 1);
|
||||
|
||||
var original = BuildObservation("obs-4", "sha256:orig", "2025-11-18T00:15:00Z");
|
||||
var mutated = BuildObservation("obs-4", "sha256:mut", "2025-11-18T00:15:00Z");
|
||||
|
||||
var originalKey = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0003", options, new[] { original }, "fp");
|
||||
var mutatedKey = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0003", options, new[] { mutated }, "fp");
|
||||
|
||||
Assert.NotEqual(originalKey.Value, mutatedKey.Value);
|
||||
Assert.NotEqual(originalKey.ComputeHash(), mutatedKey.ComputeHash());
|
||||
}
|
||||
|
||||
private static AdvisoryObservation BuildObservation(string id, string contentHash, string timestamp)
|
||||
{
|
||||
var createdAt = DateTimeOffset.Parse(timestamp, CultureInfo.InvariantCulture);
|
||||
|
||||
return new AdvisoryObservation(
|
||||
id,
|
||||
tenant: "tenant-a",
|
||||
source: new AdvisoryObservationSource("nvd", "stream", "api"),
|
||||
upstream: new AdvisoryObservationUpstream(
|
||||
upstreamId: id,
|
||||
documentVersion: "1",
|
||||
fetchedAt: createdAt,
|
||||
receivedAt: createdAt,
|
||||
contentHash: contentHash,
|
||||
signature: new AdvisoryObservationSignature(false)),
|
||||
content: new AdvisoryObservationContent("csaf", "2.0", JsonNode.Parse("{}")!),
|
||||
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
|
||||
rawLinkset: new RawLinkset(),
|
||||
createdAt: createdAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Models.Observations;
|
||||
using StellaOps.Concelier.RawModels;
|
||||
using StellaOps.Concelier.WebService.Services;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests.Services;
|
||||
|
||||
public sealed class AdvisoryChunkBuilderTests
|
||||
{
|
||||
private static readonly DateTimeOffset RecordedAt = DateTimeOffset.Parse("2025-11-18T00:00:00Z");
|
||||
|
||||
[Fact]
|
||||
public void Build_UsesJsonPointerFromFieldMaskForObservationPath()
|
||||
{
|
||||
var observation = CreateObservation("tenant-a:obs-1", "sha256:abc123");
|
||||
var provenance = new AdvisoryProvenance(
|
||||
"nvd",
|
||||
"workaround",
|
||||
observation.ObservationId,
|
||||
RecordedAt,
|
||||
new[] { " /references/0/title " });
|
||||
|
||||
var advisory = CreateAdvisory("CVE-2025-0001", provenance);
|
||||
var builder = new AdvisoryChunkBuilder(new TestCryptoHash());
|
||||
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
advisory.AdvisoryKey,
|
||||
"fingerprint-1",
|
||||
chunkLimit: 5,
|
||||
observationLimit: 5,
|
||||
SectionFilter: ImmutableHashSet<string>.Empty,
|
||||
FormatFilter: ImmutableHashSet<string>.Empty,
|
||||
MinimumLength: 0);
|
||||
|
||||
var result = builder.Build(options, advisory, new[] { observation });
|
||||
|
||||
var entry = Assert.Single(result.Response.Entries);
|
||||
Assert.Equal("/references/0/title", entry.Provenance.ObservationPath);
|
||||
Assert.Equal(observation.ObservationId, entry.Provenance.DocumentId);
|
||||
Assert.Equal(observation.Upstream.ContentHash, entry.Provenance.ContentHash);
|
||||
Assert.Equal(new[] { "/references/0/title" }, entry.Provenance.FieldMask);
|
||||
Assert.Equal(ComputeChunkId(observation.ObservationId, "/references/0/title"), entry.ChunkId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_FallsBackToFieldPathWhenMaskIsEmpty()
|
||||
{
|
||||
var observation = CreateObservation("tenant-b:obs-2", "sha256:def456");
|
||||
var provenance = new AdvisoryProvenance(
|
||||
"nvd",
|
||||
"workaround",
|
||||
observation.ObservationId,
|
||||
RecordedAt);
|
||||
|
||||
var advisory = CreateAdvisory("CVE-2025-0002", provenance);
|
||||
var builder = new AdvisoryChunkBuilder(new TestCryptoHash());
|
||||
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
advisory.AdvisoryKey,
|
||||
"fingerprint-2",
|
||||
chunkLimit: 5,
|
||||
observationLimit: 5,
|
||||
SectionFilter: ImmutableHashSet<string>.Empty,
|
||||
FormatFilter: ImmutableHashSet<string>.Empty,
|
||||
MinimumLength: 0);
|
||||
|
||||
var result = builder.Build(options, advisory, new[] { observation });
|
||||
|
||||
var entry = Assert.Single(result.Response.Entries);
|
||||
Assert.Equal("/references/0", entry.Provenance.ObservationPath);
|
||||
Assert.Equal(observation.ObservationId, entry.Provenance.DocumentId);
|
||||
Assert.Equal(observation.Upstream.ContentHash, entry.Provenance.ContentHash);
|
||||
Assert.Equal(new[] { "/references/0" }, entry.Provenance.FieldMask);
|
||||
Assert.Equal(ComputeChunkId(observation.ObservationId, "/references/0"), entry.ChunkId);
|
||||
}
|
||||
|
||||
private static Advisory CreateAdvisory(string advisoryKey, AdvisoryProvenance provenance)
|
||||
{
|
||||
var reference = new AdvisoryReference(
|
||||
"https://vendor.example/workaround",
|
||||
kind: "workaround",
|
||||
sourceTag: "Vendor guidance",
|
||||
summary: "Apply configuration change",
|
||||
provenance: provenance);
|
||||
|
||||
return new Advisory(
|
||||
advisoryKey,
|
||||
title: "Fixture advisory",
|
||||
summary: "Structured payload",
|
||||
language: "en",
|
||||
published: RecordedAt,
|
||||
modified: RecordedAt,
|
||||
severity: "critical",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { advisoryKey },
|
||||
references: new[] { reference },
|
||||
affectedPackages: Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: new[] { provenance });
|
||||
}
|
||||
|
||||
private static AdvisoryObservation CreateObservation(string observationId, string contentHash)
|
||||
{
|
||||
var source = new AdvisoryObservationSource("nvd", "default", "v1");
|
||||
var upstream = new AdvisoryObservationUpstream(
|
||||
"upstream-id",
|
||||
documentVersion: "1",
|
||||
fetchedAt: DateTimeOffset.Parse("2025-11-17T00:00:00Z"),
|
||||
receivedAt: DateTimeOffset.Parse("2025-11-17T00:01:00Z"),
|
||||
contentHash: contentHash,
|
||||
signature: new AdvisoryObservationSignature(present: false, format: null, keyId: null, signature: null));
|
||||
|
||||
var content = new AdvisoryObservationContent(
|
||||
format: "json",
|
||||
specVersion: "1.0",
|
||||
raw: JsonNode.Parse("{}")!);
|
||||
|
||||
var linkset = new AdvisoryObservationLinkset(
|
||||
aliases: new[] { "CVE-2025-0001" },
|
||||
purls: Array.Empty<string>(),
|
||||
cpes: Array.Empty<string>(),
|
||||
references: Enumerable.Empty<AdvisoryObservationReference>());
|
||||
|
||||
return new AdvisoryObservation(
|
||||
observationId,
|
||||
tenant: "tenant-a",
|
||||
source,
|
||||
upstream,
|
||||
content,
|
||||
linkset,
|
||||
rawLinkset: new RawLinkset(),
|
||||
createdAt: DateTimeOffset.Parse("2025-11-17T01:00:00Z"));
|
||||
}
|
||||
|
||||
private static string ComputeChunkId(string documentId, string observationPath)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(string.Concat(documentId, '|', observationPath));
|
||||
var digest = SHA256.HashData(bytes);
|
||||
return Convert.ToHexString(digest.AsSpan(0, 8));
|
||||
}
|
||||
|
||||
private sealed class TestCryptoHash : ICryptoHash
|
||||
{
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
=> SHA256.HashData(data.ToArray());
|
||||
|
||||
public string ComputeHashHex(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
=> Convert.ToHexString(ComputeHash(data, algorithmId)).ToLowerInvariant();
|
||||
|
||||
public string ComputeHashBase64(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
=> Convert.ToBase64String(ComputeHash(data, algorithmId));
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var memory = new MemoryStream();
|
||||
await stream.CopyToAsync(memory, cancellationToken).ConfigureAwait(false);
|
||||
return ComputeHash(memory.ToArray(), algorithmId);
|
||||
}
|
||||
|
||||
public async ValueTask<string> ComputeHashHexAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = await ComputeHashAsync(stream, algorithmId, cancellationToken).ConfigureAwait(false);
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user