test fixes and new product advisories work
This commit is contained in:
@@ -231,4 +231,257 @@ public sealed class ImportValidatorTests
|
||||
public Task<int> CleanupExpiredAsync(TimeSpan retentionPeriod, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WithReferrerValidator_MissingReferrer_ShouldFailAndQuarantine()
|
||||
{
|
||||
// Arrange
|
||||
var root = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\"}";
|
||||
var snapshot = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"meta\":{\"snapshot\":{\"hashes\":{\"sha256\":\"abc\"}}}}";
|
||||
var timestamp = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"snapshot\":{\"meta\":{\"hashes\":{\"sha256\":\"abc\"}}}}";
|
||||
|
||||
using var rsa = RSA.Create(2048);
|
||||
var pub = rsa.ExportSubjectPublicKeyInfo();
|
||||
|
||||
var payload = "bundle-body";
|
||||
var payloadType = "application/vnd.stella.bundle";
|
||||
var pae = BuildPae(payloadType, payload);
|
||||
var sig = rsa.SignData(pae, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
|
||||
|
||||
var envelope = new DsseEnvelope(payloadType, Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload)), new[]
|
||||
{
|
||||
new DsseSignature("k1", Convert.ToBase64String(sig))
|
||||
});
|
||||
|
||||
var trustStore = new TrustStore();
|
||||
trustStore.LoadActive(new Dictionary<string, byte[]> { ["k1"] = pub });
|
||||
trustStore.StagePending(new Dictionary<string, byte[]> { ["k2"] = pub });
|
||||
|
||||
var quarantine = new CapturingQuarantineService();
|
||||
var monotonicity = new CapturingMonotonicityChecker();
|
||||
var referrerValidator = new ReferrerValidator(NullLogger<ReferrerValidator>.Instance);
|
||||
|
||||
var validator = new ImportValidator(
|
||||
new DsseVerifier(),
|
||||
new TufMetadataValidator(),
|
||||
new MerkleRootCalculator(),
|
||||
new RootRotationPolicy(),
|
||||
monotonicity,
|
||||
quarantine,
|
||||
NullLogger<ImportValidator>.Instance,
|
||||
referrerValidator);
|
||||
|
||||
// Manifest with referrer that doesn't exist in entries
|
||||
var manifestJson = """
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"merkleRoot": "dummy",
|
||||
"referrers": {
|
||||
"subjects": [
|
||||
{
|
||||
"subject": "sha256:abc123",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:ref001",
|
||||
"path": "referrers/sha256-abc123/sha256-ref001.json",
|
||||
"sha256": "abcd1234",
|
||||
"size": 100,
|
||||
"category": "sbom"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var payloadEntries = new List<NamedStream> { new("a.txt", new MemoryStream("data"u8.ToArray())) };
|
||||
var merkleRoot = new MerkleRootCalculator().ComputeRoot(payloadEntries);
|
||||
manifestJson = manifestJson.Replace("\"merkleRoot\": \"dummy\"", $"\"merkleRoot\": \"{merkleRoot}\"");
|
||||
|
||||
var tempRoot = Path.Combine(Path.GetTempPath(), "stellaops-airgap-tests", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
var bundlePath = Path.Combine(tempRoot, "bundle.tar.zst");
|
||||
await File.WriteAllTextAsync(bundlePath, "bundle-bytes");
|
||||
|
||||
try
|
||||
{
|
||||
var request = new ImportValidationRequest(
|
||||
TenantId: "tenant-a",
|
||||
BundleType: "mirror-bundle",
|
||||
BundleDigest: "sha256:bundle",
|
||||
BundlePath: bundlePath,
|
||||
ManifestJson: manifestJson,
|
||||
ManifestVersion: "1.0.0",
|
||||
ManifestCreatedAt: DateTimeOffset.Parse("2025-12-15T00:00:00Z"),
|
||||
ForceActivate: false,
|
||||
ForceActivateReason: null,
|
||||
Envelope: envelope,
|
||||
TrustRoots: new TrustRootConfig("/tmp/root.json", new[] { Fingerprint(pub) }, new[] { "rsassa-pss-sha256" }, null, null, new Dictionary<string, byte[]> { ["k1"] = pub }),
|
||||
RootJson: root,
|
||||
SnapshotJson: snapshot,
|
||||
TimestampJson: timestamp,
|
||||
PayloadEntries: payloadEntries,
|
||||
TrustStore: trustStore,
|
||||
ApproverIds: new[] { "approver-1", "approver-2" });
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Reason.Should().Contain("referrer-validation-failed");
|
||||
result.ReferrerSummary.Should().NotBeNull();
|
||||
result.ReferrerSummary!.MissingReferrers.Should().Be(1);
|
||||
quarantine.Requests.Should().HaveCount(1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(tempRoot, recursive: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WithReferrerValidator_AllReferrersPresent_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var root = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\"}";
|
||||
var snapshot = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"meta\":{\"snapshot\":{\"hashes\":{\"sha256\":\"abc\"}}}}";
|
||||
var timestamp = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"snapshot\":{\"meta\":{\"hashes\":{\"sha256\":\"abc\"}}}}";
|
||||
|
||||
using var rsa = RSA.Create(2048);
|
||||
var pub = rsa.ExportSubjectPublicKeyInfo();
|
||||
|
||||
var payload = "bundle-body";
|
||||
var payloadType = "application/vnd.stella.bundle";
|
||||
var pae = BuildPae(payloadType, payload);
|
||||
var sig = rsa.SignData(pae, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
|
||||
|
||||
var envelope = new DsseEnvelope(payloadType, Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload)), new[]
|
||||
{
|
||||
new DsseSignature("k1", Convert.ToBase64String(sig))
|
||||
});
|
||||
|
||||
var trustStore = new TrustStore();
|
||||
trustStore.LoadActive(new Dictionary<string, byte[]> { ["k1"] = pub });
|
||||
trustStore.StagePending(new Dictionary<string, byte[]> { ["k2"] = pub });
|
||||
|
||||
var quarantine = new CapturingQuarantineService();
|
||||
var monotonicity = new CapturingMonotonicityChecker();
|
||||
var referrerValidator = new ReferrerValidator(NullLogger<ReferrerValidator>.Instance);
|
||||
|
||||
var validator = new ImportValidator(
|
||||
new DsseVerifier(),
|
||||
new TufMetadataValidator(),
|
||||
new MerkleRootCalculator(),
|
||||
new RootRotationPolicy(),
|
||||
monotonicity,
|
||||
quarantine,
|
||||
NullLogger<ImportValidator>.Instance,
|
||||
referrerValidator);
|
||||
|
||||
// Create referrer content and compute its hash
|
||||
var referrerContent = "{\"sbom\":\"content\"}"u8.ToArray();
|
||||
var referrerSha256 = Convert.ToHexString(SHA256.HashData(referrerContent)).ToLowerInvariant();
|
||||
|
||||
// Manifest with referrer that exists in entries
|
||||
var manifestJsonTemplate = """
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"merkleRoot": "MERKLE_PLACEHOLDER",
|
||||
"referrers": {
|
||||
"subjects": [
|
||||
{
|
||||
"subject": "sha256:abc123",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:ref001",
|
||||
"path": "referrers/sha256-abc123/sha256-ref001.json",
|
||||
"sha256": "CHECKSUM_PLACEHOLDER",
|
||||
"size": SIZE_PLACEHOLDER,
|
||||
"category": "sbom"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var payloadEntries = new List<NamedStream>
|
||||
{
|
||||
new("a.txt", new MemoryStream("data"u8.ToArray())),
|
||||
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(referrerContent))
|
||||
};
|
||||
|
||||
var merkleRoot = new MerkleRootCalculator().ComputeRoot(payloadEntries);
|
||||
var manifestJson = manifestJsonTemplate
|
||||
.Replace("MERKLE_PLACEHOLDER", merkleRoot)
|
||||
.Replace("CHECKSUM_PLACEHOLDER", referrerSha256)
|
||||
.Replace("SIZE_PLACEHOLDER", referrerContent.Length.ToString());
|
||||
|
||||
var tempRoot = Path.Combine(Path.GetTempPath(), "stellaops-airgap-tests", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
var bundlePath = Path.Combine(tempRoot, "bundle.tar.zst");
|
||||
await File.WriteAllTextAsync(bundlePath, "bundle-bytes");
|
||||
|
||||
try
|
||||
{
|
||||
// Reset streams for re-reading
|
||||
foreach (var entry in payloadEntries)
|
||||
{
|
||||
entry.Stream.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
var request = new ImportValidationRequest(
|
||||
TenantId: "tenant-a",
|
||||
BundleType: "mirror-bundle",
|
||||
BundleDigest: "sha256:bundle",
|
||||
BundlePath: bundlePath,
|
||||
ManifestJson: manifestJson,
|
||||
ManifestVersion: "1.0.0",
|
||||
ManifestCreatedAt: DateTimeOffset.Parse("2025-12-15T00:00:00Z"),
|
||||
ForceActivate: false,
|
||||
ForceActivateReason: null,
|
||||
Envelope: envelope,
|
||||
TrustRoots: new TrustRootConfig("/tmp/root.json", new[] { Fingerprint(pub) }, new[] { "rsassa-pss-sha256" }, null, null, new Dictionary<string, byte[]> { ["k1"] = pub }),
|
||||
RootJson: root,
|
||||
SnapshotJson: snapshot,
|
||||
TimestampJson: timestamp,
|
||||
PayloadEntries: payloadEntries,
|
||||
TrustStore: trustStore,
|
||||
ApproverIds: new[] { "approver-1", "approver-2" });
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.ReferrerSummary.Should().NotBeNull();
|
||||
result.ReferrerSummary!.TotalReferrers.Should().Be(1);
|
||||
result.ReferrerSummary.ValidReferrers.Should().Be(1);
|
||||
result.ReferrerSummary.MissingReferrers.Should().Be(0);
|
||||
quarantine.Requests.Should().BeEmpty();
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(tempRoot, recursive: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,599 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.AirGap.Importer.Validation;
|
||||
using StellaOps.TestKit;
|
||||
|
||||
namespace StellaOps.AirGap.Importer.Tests.Validation;
|
||||
|
||||
public sealed class ReferrerValidatorTests
|
||||
{
|
||||
private readonly ReferrerValidator _validator;
|
||||
|
||||
public ReferrerValidatorTests()
|
||||
{
|
||||
_validator = new ReferrerValidator(NullLogger<ReferrerValidator>.Instance);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_NullManifest_ReturnsEmptySummary()
|
||||
{
|
||||
// Act
|
||||
var result = _validator.Validate(null, []);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.TotalSubjects.Should().Be(0);
|
||||
result.TotalReferrers.Should().Be(0);
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_EmptyManifest_ReturnsEmptySummary()
|
||||
{
|
||||
// Act
|
||||
var result = _validator.Validate("", []);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.TotalSubjects.Should().Be(0);
|
||||
result.TotalReferrers.Should().Be(0);
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ManifestWithoutReferrers_ReturnsEmptySummary()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = """{"version":"1.0.0","counts":{"advisories":5}}""";
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, []);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.TotalSubjects.Should().Be(0);
|
||||
result.TotalReferrers.Should().Be(0);
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_AllReferrersPresent_ReturnsValid()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test content for referrer"u8.ToArray();
|
||||
var sha256 = ComputeSha256(content);
|
||||
|
||||
var manifest = $$"""
|
||||
{
|
||||
"referrers": {
|
||||
"subjects": [
|
||||
{
|
||||
"subject": "sha256:abc123",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:ref001",
|
||||
"path": "referrers/sha256-abc123/sha256-ref001.json",
|
||||
"sha256": "{{sha256}}",
|
||||
"size": {{content.Length}},
|
||||
"category": "sbom"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var entries = new List<NamedStream>
|
||||
{
|
||||
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, entries);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.TotalSubjects.Should().Be(1);
|
||||
result.TotalReferrers.Should().Be(1);
|
||||
result.ValidReferrers.Should().Be(1);
|
||||
result.MissingReferrers.Should().Be(0);
|
||||
result.ChecksumMismatches.Should().Be(0);
|
||||
result.SizeMismatches.Should().Be(0);
|
||||
result.Issues.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_MissingReferrer_ReturnsInvalidWithIssue()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = """
|
||||
{
|
||||
"referrers": {
|
||||
"subjects": [
|
||||
{
|
||||
"subject": "sha256:abc123",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:ref001",
|
||||
"path": "referrers/sha256-abc123/sha256-ref001.json",
|
||||
"sha256": "abcd1234",
|
||||
"size": 100,
|
||||
"category": "sbom"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
// Act - no entries provided, so referrer is missing
|
||||
var result = _validator.Validate(manifest, []);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.MissingReferrers.Should().Be(1);
|
||||
result.Issues.Should().HaveCount(1);
|
||||
result.Issues[0].IssueType.Should().Be(ReferrerValidationIssueType.ReferrerMissing);
|
||||
result.Issues[0].Severity.Should().Be(ReferrerValidationSeverity.Error);
|
||||
result.Issues[0].SubjectDigest.Should().Be("sha256:abc123");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ChecksumMismatch_ReturnsInvalidWithIssue()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test content"u8.ToArray();
|
||||
var wrongChecksum = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
var manifest = $$"""
|
||||
{
|
||||
"referrers": {
|
||||
"subjects": [
|
||||
{
|
||||
"subject": "sha256:abc123",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:ref001",
|
||||
"path": "referrers/sha256-abc123/sha256-ref001.json",
|
||||
"sha256": "{{wrongChecksum}}",
|
||||
"size": {{content.Length}},
|
||||
"category": "sbom"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var entries = new List<NamedStream>
|
||||
{
|
||||
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, entries);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.ChecksumMismatches.Should().Be(1);
|
||||
result.Issues.Should().HaveCount(1);
|
||||
result.Issues[0].IssueType.Should().Be(ReferrerValidationIssueType.ReferrerChecksumMismatch);
|
||||
result.Issues[0].Severity.Should().Be(ReferrerValidationSeverity.Error);
|
||||
result.Issues[0].ExpectedValue.Should().Be(wrongChecksum);
|
||||
result.Issues[0].ActualValue.Should().NotBe(wrongChecksum);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_SizeMismatch_ReturnsInvalidWithIssue()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test content"u8.ToArray();
|
||||
var sha256 = ComputeSha256(content);
|
||||
var wrongSize = content.Length + 100;
|
||||
|
||||
var manifest = $$"""
|
||||
{
|
||||
"referrers": {
|
||||
"subjects": [
|
||||
{
|
||||
"subject": "sha256:abc123",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:ref001",
|
||||
"path": "referrers/sha256-abc123/sha256-ref001.json",
|
||||
"sha256": "{{sha256}}",
|
||||
"size": {{wrongSize}},
|
||||
"category": "sbom"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var entries = new List<NamedStream>
|
||||
{
|
||||
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, entries);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.SizeMismatches.Should().Be(1);
|
||||
result.Issues.Should().HaveCount(1);
|
||||
result.Issues[0].IssueType.Should().Be(ReferrerValidationIssueType.ReferrerSizeMismatch);
|
||||
result.Issues[0].Severity.Should().Be(ReferrerValidationSeverity.Error);
|
||||
result.Issues[0].ExpectedValue.Should().Be(wrongSize.ToString());
|
||||
result.Issues[0].ActualValue.Should().Be(content.Length.ToString());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_OrphanedReferrer_ReturnsValidWithWarning()
|
||||
{
|
||||
// Arrange - manifest has no referrers but bundle has referrer files
|
||||
var manifest = """{"version":"1.0.0"}""";
|
||||
var content = "orphaned content"u8.ToArray();
|
||||
|
||||
var entries = new List<NamedStream>
|
||||
{
|
||||
new("referrers/sha256-abc123/sha256-orphan.json", new MemoryStream(content))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, entries);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue(); // Orphans are warnings, not errors
|
||||
result.OrphanedReferrers.Should().Be(1);
|
||||
result.Issues.Should().HaveCount(1);
|
||||
result.Issues[0].IssueType.Should().Be(ReferrerValidationIssueType.OrphanedReferrer);
|
||||
result.Issues[0].Severity.Should().Be(ReferrerValidationSeverity.Warning);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_MultipleSubjectsAndArtifacts_ValidatesAll()
|
||||
{
|
||||
// Arrange
|
||||
var content1 = "content for subject 1 artifact 1"u8.ToArray();
|
||||
var content2 = "content for subject 1 artifact 2"u8.ToArray();
|
||||
var content3 = "content for subject 2 artifact 1"u8.ToArray();
|
||||
var sha256_1 = ComputeSha256(content1);
|
||||
var sha256_2 = ComputeSha256(content2);
|
||||
var sha256_3 = ComputeSha256(content3);
|
||||
|
||||
var manifest = $$"""
|
||||
{
|
||||
"referrers": {
|
||||
"subjects": [
|
||||
{
|
||||
"subject": "sha256:subject1",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:ref001",
|
||||
"path": "referrers/sha256-subject1/sha256-ref001.json",
|
||||
"sha256": "{{sha256_1}}",
|
||||
"size": {{content1.Length}},
|
||||
"category": "sbom"
|
||||
},
|
||||
{
|
||||
"digest": "sha256:ref002",
|
||||
"path": "referrers/sha256-subject1/sha256-ref002.json",
|
||||
"sha256": "{{sha256_2}}",
|
||||
"size": {{content2.Length}},
|
||||
"category": "attestation"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"subject": "sha256:subject2",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:ref003",
|
||||
"path": "referrers/sha256-subject2/sha256-ref003.json",
|
||||
"sha256": "{{sha256_3}}",
|
||||
"size": {{content3.Length}},
|
||||
"category": "vex"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var entries = new List<NamedStream>
|
||||
{
|
||||
new("referrers/sha256-subject1/sha256-ref001.json", new MemoryStream(content1)),
|
||||
new("referrers/sha256-subject1/sha256-ref002.json", new MemoryStream(content2)),
|
||||
new("referrers/sha256-subject2/sha256-ref003.json", new MemoryStream(content3))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, entries);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.TotalSubjects.Should().Be(2);
|
||||
result.TotalReferrers.Should().Be(3);
|
||||
result.ValidReferrers.Should().Be(3);
|
||||
result.Issues.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_MixedErrors_ReportsAllIssues()
|
||||
{
|
||||
// Arrange
|
||||
var validContent = "valid content"u8.ToArray();
|
||||
var validSha256 = ComputeSha256(validContent);
|
||||
var wrongChecksum = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
var manifest = $$"""
|
||||
{
|
||||
"referrers": {
|
||||
"subjects": [
|
||||
{
|
||||
"subject": "sha256:subject1",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:valid",
|
||||
"path": "referrers/sha256-subject1/sha256-valid.json",
|
||||
"sha256": "{{validSha256}}",
|
||||
"size": {{validContent.Length}},
|
||||
"category": "sbom"
|
||||
},
|
||||
{
|
||||
"digest": "sha256:missing",
|
||||
"path": "referrers/sha256-subject1/sha256-missing.json",
|
||||
"sha256": "abcd1234",
|
||||
"size": 100,
|
||||
"category": "attestation"
|
||||
},
|
||||
{
|
||||
"digest": "sha256:badchecksum",
|
||||
"path": "referrers/sha256-subject1/sha256-badchecksum.json",
|
||||
"sha256": "{{wrongChecksum}}",
|
||||
"size": {{validContent.Length}},
|
||||
"category": "vex"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var entries = new List<NamedStream>
|
||||
{
|
||||
new("referrers/sha256-subject1/sha256-valid.json", new MemoryStream(validContent)),
|
||||
new("referrers/sha256-subject1/sha256-badchecksum.json", new MemoryStream(validContent)),
|
||||
new("referrers/sha256-subject1/sha256-orphan.json", new MemoryStream(validContent))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, entries);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.ValidReferrers.Should().Be(1);
|
||||
result.MissingReferrers.Should().Be(1);
|
||||
result.ChecksumMismatches.Should().Be(1);
|
||||
result.OrphanedReferrers.Should().Be(1);
|
||||
result.Issues.Should().HaveCount(3); // missing, checksum mismatch, orphan
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_PathNormalization_HandlesBackslashes()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test content"u8.ToArray();
|
||||
var sha256 = ComputeSha256(content);
|
||||
|
||||
var manifest = $$"""
|
||||
{
|
||||
"referrers": {
|
||||
"subjects": [
|
||||
{
|
||||
"subject": "sha256:abc123",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:ref001",
|
||||
"path": "referrers\\sha256-abc123\\sha256-ref001.json",
|
||||
"sha256": "{{sha256}}",
|
||||
"size": {{content.Length}},
|
||||
"category": "sbom"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var entries = new List<NamedStream>
|
||||
{
|
||||
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, entries);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.ValidReferrers.Should().Be(1);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_CaseInsensitivePaths_MatchesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test content"u8.ToArray();
|
||||
var sha256 = ComputeSha256(content);
|
||||
|
||||
var manifest = $$"""
|
||||
{
|
||||
"referrers": {
|
||||
"subjects": [
|
||||
{
|
||||
"subject": "sha256:abc123",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:ref001",
|
||||
"path": "REFERRERS/SHA256-ABC123/SHA256-REF001.JSON",
|
||||
"sha256": "{{sha256}}",
|
||||
"size": {{content.Length}},
|
||||
"category": "sbom"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var entries = new List<NamedStream>
|
||||
{
|
||||
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, entries);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.ValidReferrers.Should().Be(1);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ZeroSizeInManifest_SkipsSizeValidation()
|
||||
{
|
||||
// Arrange - when size is 0 or not specified, size validation is skipped
|
||||
var content = "test content"u8.ToArray();
|
||||
var sha256 = ComputeSha256(content);
|
||||
|
||||
var manifest = $$"""
|
||||
{
|
||||
"referrers": {
|
||||
"subjects": [
|
||||
{
|
||||
"subject": "sha256:abc123",
|
||||
"artifacts": [
|
||||
{
|
||||
"digest": "sha256:ref001",
|
||||
"path": "referrers/sha256-abc123/sha256-ref001.json",
|
||||
"sha256": "{{sha256}}",
|
||||
"size": 0,
|
||||
"category": "sbom"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var entries = new List<NamedStream>
|
||||
{
|
||||
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, entries);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.SizeMismatches.Should().Be(0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_InvalidJson_ReturnsEmptySummary()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = "this is not valid json {{{";
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, []);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.TotalReferrers.Should().Be(0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_NonReferrerFiles_NotReportedAsOrphans()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = """{"version":"1.0.0"}""";
|
||||
var content = "some content"u8.ToArray();
|
||||
|
||||
var entries = new List<NamedStream>
|
||||
{
|
||||
new("advisories/adv-001.json", new MemoryStream(content)),
|
||||
new("sboms/sbom-001.json", new MemoryStream(content)),
|
||||
new("manifest.yaml", new MemoryStream(content))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(manifest, entries);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.OrphanedReferrers.Should().Be(0);
|
||||
result.Issues.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsValid_StaticMethod_ChecksCorrectly()
|
||||
{
|
||||
// Valid summary
|
||||
var valid = new ReferrerValidationSummary
|
||||
{
|
||||
TotalReferrers = 5,
|
||||
ValidReferrers = 5,
|
||||
MissingReferrers = 0,
|
||||
ChecksumMismatches = 0,
|
||||
SizeMismatches = 0,
|
||||
OrphanedReferrers = 2 // Warnings are OK
|
||||
};
|
||||
ReferrerValidator.IsValid(valid).Should().BeTrue();
|
||||
|
||||
// Invalid - missing
|
||||
var missing = valid with { MissingReferrers = 1 };
|
||||
ReferrerValidator.IsValid(missing).Should().BeFalse();
|
||||
|
||||
// Invalid - checksum
|
||||
var checksum = valid with { ChecksumMismatches = 1 };
|
||||
ReferrerValidator.IsValid(checksum).Should().BeFalse();
|
||||
|
||||
// Invalid - size
|
||||
var size = valid with { SizeMismatches = 1 };
|
||||
ReferrerValidator.IsValid(size).Should().BeFalse();
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] data)
|
||||
{
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(data);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user