finish off sprint advisories and sprints
This commit is contained in:
@@ -28,8 +28,8 @@ public sealed class EvidenceReconcilerVexTests
|
||||
var researcherEnvelope = BuildDsseEnvelope(researcherVex, digest);
|
||||
|
||||
var attestations = Path.Combine(input, "attestations");
|
||||
await File.WriteAllTextAsync(Path.Combine(attestations, "vendor.dsse.json"), vendorEnvelope);
|
||||
await File.WriteAllTextAsync(Path.Combine(attestations, "researcher.dsse.json"), researcherEnvelope);
|
||||
await File.WriteAllTextAsync(Path.Combine(attestations, "vendor.intoto.json"), vendorEnvelope);
|
||||
await File.WriteAllTextAsync(Path.Combine(attestations, "researcher.intoto.json"), researcherEnvelope);
|
||||
|
||||
var reconciler = new EvidenceReconciler();
|
||||
var options = new ReconciliationOptions
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SbomNormalizerVolatileFieldsTests.cs
|
||||
// Sprint: SPRINT_20260123_041_Scanner_sbom_oci_deterministic_publication
|
||||
// Task: 041-01 - Expand volatile field stripping in SbomNormalizer
|
||||
// Description: Verifies volatile fields are stripped for deterministic canonical hashes
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.AirGap.Importer.Reconciliation;
|
||||
using StellaOps.AirGap.Importer.Reconciliation.Parsers;
|
||||
|
||||
namespace StellaOps.AirGap.Importer.Tests.Reconciliation;
|
||||
|
||||
public sealed class SbomNormalizerVolatileFieldsTests
|
||||
{
|
||||
private readonly SbomNormalizer _normalizer = new(new NormalizationOptions
|
||||
{
|
||||
SortArrays = true,
|
||||
LowercaseUris = true,
|
||||
StripTimestamps = true,
|
||||
StripVolatileFields = true,
|
||||
NormalizeKeys = false
|
||||
});
|
||||
|
||||
private readonly SbomNormalizer _normalizerNoStrip = new(new NormalizationOptions
|
||||
{
|
||||
SortArrays = true,
|
||||
LowercaseUris = true,
|
||||
StripTimestamps = true,
|
||||
StripVolatileFields = false,
|
||||
NormalizeKeys = false
|
||||
});
|
||||
|
||||
#region CycloneDX volatile field stripping
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_SerialNumber_Stripped_Produces_Same_Hash()
|
||||
{
|
||||
var sbomA = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"serialNumber": "urn:uuid:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||
"version": 1,
|
||||
"components": [
|
||||
{"type": "library", "name": "lodash", "version": "4.17.21", "purl": "pkg:npm/lodash@4.17.21"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var sbomB = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"serialNumber": "urn:uuid:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||
"version": 1,
|
||||
"components": [
|
||||
{"type": "library", "name": "lodash", "version": "4.17.21", "purl": "pkg:npm/lodash@4.17.21"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var hashA = ComputeHash(_normalizer.Normalize(sbomA, SbomFormat.CycloneDx));
|
||||
var hashB = ComputeHash(_normalizer.Normalize(sbomB, SbomFormat.CycloneDx));
|
||||
|
||||
Assert.Equal(hashA, hashB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_MetadataTools_Stripped_Produces_Same_Hash()
|
||||
{
|
||||
var sbomA = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tools": [{"vendor": "anchore", "name": "syft", "version": "1.0.0"}],
|
||||
"component": {"type": "application", "name": "myapp", "version": "2.0.0"}
|
||||
},
|
||||
"components": [
|
||||
{"type": "library", "name": "express", "version": "4.18.2", "purl": "pkg:npm/express@4.18.2"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var sbomB = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tools": [{"vendor": "anchore", "name": "syft", "version": "2.5.0"}],
|
||||
"component": {"type": "application", "name": "myapp", "version": "2.0.0"}
|
||||
},
|
||||
"components": [
|
||||
{"type": "library", "name": "express", "version": "4.18.2", "purl": "pkg:npm/express@4.18.2"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var hashA = ComputeHash(_normalizer.Normalize(sbomA, SbomFormat.CycloneDx));
|
||||
var hashB = ComputeHash(_normalizer.Normalize(sbomB, SbomFormat.CycloneDx));
|
||||
|
||||
Assert.Equal(hashA, hashB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_MetadataTimestamp_Stripped_Produces_Same_Hash()
|
||||
{
|
||||
var sbomA = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
"component": {"type": "application", "name": "myapp", "version": "1.0.0"}
|
||||
},
|
||||
"components": []
|
||||
}
|
||||
""";
|
||||
|
||||
var sbomB = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2026-01-23T12:34:56Z",
|
||||
"component": {"type": "application", "name": "myapp", "version": "1.0.0"}
|
||||
},
|
||||
"components": []
|
||||
}
|
||||
""";
|
||||
|
||||
var hashA = ComputeHash(_normalizer.Normalize(sbomA, SbomFormat.CycloneDx));
|
||||
var hashB = ComputeHash(_normalizer.Normalize(sbomB, SbomFormat.CycloneDx));
|
||||
|
||||
Assert.Equal(hashA, hashB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_MetadataAuthors_Stripped_Produces_Same_Hash()
|
||||
{
|
||||
var sbomA = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"authors": [{"name": "Alice"}],
|
||||
"component": {"type": "application", "name": "myapp", "version": "1.0.0"}
|
||||
},
|
||||
"components": []
|
||||
}
|
||||
""";
|
||||
|
||||
var sbomB = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"authors": [{"name": "Bob"}],
|
||||
"component": {"type": "application", "name": "myapp", "version": "1.0.0"}
|
||||
},
|
||||
"components": []
|
||||
}
|
||||
""";
|
||||
|
||||
var hashA = ComputeHash(_normalizer.Normalize(sbomA, SbomFormat.CycloneDx));
|
||||
var hashB = ComputeHash(_normalizer.Normalize(sbomB, SbomFormat.CycloneDx));
|
||||
|
||||
Assert.Equal(hashA, hashB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_ContentChange_Produces_Different_Hash()
|
||||
{
|
||||
var sbomA = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"components": [
|
||||
{"type": "library", "name": "lodash", "version": "4.17.21", "purl": "pkg:npm/lodash@4.17.21"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var sbomB = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"components": [
|
||||
{"type": "library", "name": "lodash", "version": "4.17.22", "purl": "pkg:npm/lodash@4.17.22"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var hashA = ComputeHash(_normalizer.Normalize(sbomA, SbomFormat.CycloneDx));
|
||||
var hashB = ComputeHash(_normalizer.Normalize(sbomB, SbomFormat.CycloneDx));
|
||||
|
||||
Assert.NotEqual(hashA, hashB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_StripVolatileFields_Disabled_Preserves_SerialNumber()
|
||||
{
|
||||
var sbom = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"serialNumber": "urn:uuid:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||
"version": 1,
|
||||
"components": []
|
||||
}
|
||||
""";
|
||||
|
||||
var result = _normalizerNoStrip.Normalize(sbom, SbomFormat.CycloneDx);
|
||||
|
||||
Assert.Contains("serialNumber", result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SPDX volatile field stripping
|
||||
|
||||
[Fact]
|
||||
public void Spdx_CreationInfoCreators_Stripped_Produces_Same_Hash()
|
||||
{
|
||||
var sbomA = """
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "myapp",
|
||||
"creationInfo": {
|
||||
"created": "2026-01-01T00:00:00Z",
|
||||
"creators": ["Tool: syft-1.0.0"],
|
||||
"licenseListVersion": "3.19"
|
||||
},
|
||||
"packages": [
|
||||
{"SPDXID": "SPDXRef-Package-lodash", "name": "lodash", "versionInfo": "4.17.21"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var sbomB = """
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "myapp",
|
||||
"creationInfo": {
|
||||
"created": "2026-01-23T12:00:00Z",
|
||||
"creators": ["Tool: syft-2.5.0", "Organization: ACME"],
|
||||
"licenseListVersion": "3.22"
|
||||
},
|
||||
"packages": [
|
||||
{"SPDXID": "SPDXRef-Package-lodash", "name": "lodash", "versionInfo": "4.17.21"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var hashA = ComputeHash(_normalizer.Normalize(sbomA, SbomFormat.Spdx));
|
||||
var hashB = ComputeHash(_normalizer.Normalize(sbomB, SbomFormat.Spdx));
|
||||
|
||||
Assert.Equal(hashA, hashB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx_ContentChange_Produces_Different_Hash()
|
||||
{
|
||||
var sbomA = """
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "myapp",
|
||||
"creationInfo": {
|
||||
"created": "2026-01-01T00:00:00Z",
|
||||
"creators": ["Tool: syft-1.0.0"]
|
||||
},
|
||||
"packages": [
|
||||
{"SPDXID": "SPDXRef-Package-lodash", "name": "lodash", "versionInfo": "4.17.21"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var sbomB = """
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "myapp",
|
||||
"creationInfo": {
|
||||
"created": "2026-01-01T00:00:00Z",
|
||||
"creators": ["Tool: syft-1.0.0"]
|
||||
},
|
||||
"packages": [
|
||||
{"SPDXID": "SPDXRef-Package-lodash", "name": "lodash", "versionInfo": "4.17.22"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var hashA = ComputeHash(_normalizer.Normalize(sbomA, SbomFormat.Spdx));
|
||||
var hashB = ComputeHash(_normalizer.Normalize(sbomB, SbomFormat.Spdx));
|
||||
|
||||
Assert.NotEqual(hashA, hashB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx_StripVolatileFields_Disabled_Preserves_Creators()
|
||||
{
|
||||
var sbom = """
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "myapp",
|
||||
"creationInfo": {
|
||||
"creators": ["Tool: syft-1.0.0"],
|
||||
"licenseListVersion": "3.19"
|
||||
},
|
||||
"packages": []
|
||||
}
|
||||
""";
|
||||
|
||||
var result = _normalizerNoStrip.Normalize(sbom, SbomFormat.Spdx);
|
||||
|
||||
Assert.Contains("creators", result);
|
||||
Assert.Contains("licenseListVersion", result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Combined volatile field tests (determinism guard)
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_AllVolatileFields_Different_Same_Hash()
|
||||
{
|
||||
// Simulates two scans of the same image with completely different volatile metadata
|
||||
var sbomA = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"serialNumber": "urn:uuid:11111111-1111-1111-1111-111111111111",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
"tools": [{"vendor": "anchore", "name": "syft", "version": "0.90.0"}],
|
||||
"authors": [{"name": "CI Bot 1"}],
|
||||
"component": {"type": "application", "name": "myapp", "version": "3.0.0"}
|
||||
},
|
||||
"components": [
|
||||
{"type": "library", "name": "react", "version": "18.2.0", "purl": "pkg:npm/react@18.2.0"},
|
||||
{"type": "library", "name": "typescript", "version": "5.3.0", "purl": "pkg:npm/typescript@5.3.0"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var sbomB = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"serialNumber": "urn:uuid:99999999-9999-9999-9999-999999999999",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2026-01-23T23:59:59Z",
|
||||
"tools": [{"vendor": "anchore", "name": "syft", "version": "1.5.0"}],
|
||||
"authors": [{"name": "CI Bot 2", "email": "bot@example.com"}],
|
||||
"component": {"type": "application", "name": "myapp", "version": "3.0.0"}
|
||||
},
|
||||
"components": [
|
||||
{"type": "library", "name": "typescript", "version": "5.3.0", "purl": "pkg:npm/typescript@5.3.0"},
|
||||
{"type": "library", "name": "react", "version": "18.2.0", "purl": "pkg:npm/react@18.2.0"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var hashA = ComputeHash(_normalizer.Normalize(sbomA, SbomFormat.CycloneDx));
|
||||
var hashB = ComputeHash(_normalizer.Normalize(sbomB, SbomFormat.CycloneDx));
|
||||
|
||||
Assert.Equal(hashA, hashB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_Twice_Identical_Bytes()
|
||||
{
|
||||
// Non-determinism guard: run canonicalizer twice, assert identical bytes
|
||||
var sbom = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"serialNumber": "urn:uuid:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2026-01-23T12:00:00Z",
|
||||
"tools": [{"vendor": "anchore", "name": "syft", "version": "1.0.0"}]
|
||||
},
|
||||
"components": [
|
||||
{"type": "library", "name": "b-lib", "version": "2.0.0", "purl": "pkg:npm/b-lib@2.0.0"},
|
||||
{"type": "library", "name": "a-lib", "version": "1.0.0", "purl": "pkg:npm/a-lib@1.0.0"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var pass1 = _normalizer.Normalize(sbom, SbomFormat.CycloneDx);
|
||||
var pass2 = _normalizer.Normalize(sbom, SbomFormat.CycloneDx);
|
||||
|
||||
Assert.Equal(pass1, pass2);
|
||||
Assert.Equal(Encoding.UTF8.GetBytes(pass1), Encoding.UTF8.GetBytes(pass2));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static string ComputeHash(string json)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return $"sha256:{Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user