finish off sprint advisories and sprints

This commit is contained in:
master
2026-01-24 00:12:43 +02:00
parent 726d70dc7f
commit c70e83719e
266 changed files with 46699 additions and 1328 deletions

View File

@@ -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

View File

@@ -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)}";
}
}