license switch agpl -> busl1, sprints work, new product advisories
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// <copyright file="RekorEntryEventTests.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Sprint: SPRINT_20260112_007_ATTESTOR_rekor_entry_events (ATT-REKOR-004)
|
||||
// </copyright>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -180,20 +180,16 @@ public sealed class HttpRekorTileClientTests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(RekorLogVersion.V2, false, true)]
|
||||
[InlineData(RekorLogVersion.V1, false, false)]
|
||||
[InlineData(RekorLogVersion.V1, true, false)]
|
||||
[InlineData(RekorLogVersion.Auto, false, false)]
|
||||
[InlineData(RekorLogVersion.Auto, true, true)]
|
||||
public void ShouldUseTileProofs_ReturnsExpected(RekorLogVersion version, bool preferTiles, bool expected)
|
||||
[InlineData(RekorLogVersion.V2, true)]
|
||||
[InlineData(RekorLogVersion.Auto, true)]
|
||||
public void ShouldUseTileProofs_ReturnsExpected(RekorLogVersion version, bool expected)
|
||||
{
|
||||
// Arrange
|
||||
var backend = new RekorBackend
|
||||
{
|
||||
Name = "test",
|
||||
Url = new Uri("https://rekor.sigstore.dev"),
|
||||
Version = version,
|
||||
PreferTileProofs = preferTiles
|
||||
Version = version
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -40,15 +40,11 @@ public sealed class RekorBackendResolverTests
|
||||
[Theory]
|
||||
[InlineData("Auto", RekorLogVersion.Auto)]
|
||||
[InlineData("auto", RekorLogVersion.Auto)]
|
||||
[InlineData("V1", RekorLogVersion.V1)]
|
||||
[InlineData("v1", RekorLogVersion.V1)]
|
||||
[InlineData("1", RekorLogVersion.V1)]
|
||||
[InlineData("V2", RekorLogVersion.V2)]
|
||||
[InlineData("v2", RekorLogVersion.V2)]
|
||||
[InlineData("2", RekorLogVersion.V2)]
|
||||
[InlineData("", RekorLogVersion.Auto)]
|
||||
[InlineData(null, RekorLogVersion.Auto)]
|
||||
[InlineData("invalid", RekorLogVersion.Auto)]
|
||||
public void ResolveBackend_ParsesVersionCorrectly(string? versionString, RekorLogVersion expected)
|
||||
{
|
||||
var options = new AttestorOptions
|
||||
@@ -68,6 +64,55 @@ public sealed class RekorBackendResolverTests
|
||||
backend.Version.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("V1")]
|
||||
[InlineData("v1")]
|
||||
[InlineData("1")]
|
||||
public void ResolveBackend_V1Rejected(string versionString)
|
||||
{
|
||||
var options = new AttestorOptions
|
||||
{
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.sigstore.dev",
|
||||
Version = versionString
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var action = () => RekorBackendResolver.ResolveBackend(options, "primary", allowFallbackToPrimary: false);
|
||||
|
||||
action.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("Rekor v1 is no longer supported. Use Auto or V2.");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("invalid")]
|
||||
[InlineData("v3")]
|
||||
public void ResolveBackend_InvalidVersionRejected(string versionString)
|
||||
{
|
||||
var options = new AttestorOptions
|
||||
{
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.sigstore.dev",
|
||||
Version = versionString
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var action = () => RekorBackendResolver.ResolveBackend(options, "primary", allowFallbackToPrimary: false);
|
||||
|
||||
action.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage($"Unsupported Rekor version '{versionString}'. Use Auto or V2.");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolveBackend_WithTileBaseUrl_SetsProperty()
|
||||
@@ -112,41 +157,17 @@ public sealed class RekorBackendResolverTests
|
||||
backend.LogId.Should().Be(RekorBackend.SigstoreProductionLogId);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolveBackend_WithPreferTileProofs_SetsProperty()
|
||||
{
|
||||
var options = new AttestorOptions
|
||||
{
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.sigstore.dev",
|
||||
PreferTileProofs = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var backend = RekorBackendResolver.ResolveBackend(options, "primary", allowFallbackToPrimary: false);
|
||||
|
||||
backend.PreferTileProofs.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(RekorLogVersion.V2, false, true)]
|
||||
[InlineData(RekorLogVersion.V1, true, false)]
|
||||
[InlineData(RekorLogVersion.Auto, true, true)]
|
||||
[InlineData(RekorLogVersion.Auto, false, false)]
|
||||
public void ShouldUseTileProofs_ReturnsCorrectValue(RekorLogVersion version, bool preferTileProofs, bool expected)
|
||||
[InlineData(RekorLogVersion.V2, true)]
|
||||
[InlineData(RekorLogVersion.Auto, true)]
|
||||
public void ShouldUseTileProofs_ReturnsCorrectValue(RekorLogVersion version, bool expected)
|
||||
{
|
||||
var backend = new RekorBackend
|
||||
{
|
||||
Name = "test",
|
||||
Url = new Uri("https://rekor.sigstore.dev"),
|
||||
Version = version,
|
||||
PreferTileProofs = preferTileProofs
|
||||
Version = version
|
||||
};
|
||||
|
||||
var result = RekorBackendResolver.ShouldUseTileProofs(backend);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// © StellaOps Contributors. See LICENSE and NOTICE.md in the repository root.
|
||||
|
||||
using System;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// © StellaOps Contributors. See LICENSE and NOTICE.md in the repository root.
|
||||
|
||||
using FluentAssertions;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// © StellaOps Contributors. See LICENSE and NOTICE.md in the repository root.
|
||||
|
||||
using System;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Copyright (c) StellaOps Contributors
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Copyright (c) StellaOps Contributors
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CycloneDxFeatureGenerationTests.cs
|
||||
// Sprint: SPRINT_20260119_013_Attestor_cyclonedx_1.7_generation
|
||||
// Task: TASK-013-009 - CycloneDX 1.7 feature tests
|
||||
// Description: Validates CycloneDX 1.7 writer output for new sections.
|
||||
// -----------------------------------------------------------------------------
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Attestor.StandardPredicates.Models;
|
||||
using StellaOps.Attestor.StandardPredicates.Parsers;
|
||||
using StellaOps.Attestor.StandardPredicates.Writers;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests;
|
||||
|
||||
public sealed class CycloneDxFeatureGenerationTests
|
||||
{
|
||||
private readonly CycloneDxWriter _writer = new();
|
||||
private readonly CycloneDxPredicateParser _parser =
|
||||
new(NullLogger<CycloneDxPredicateParser>.Instance);
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Write_FullFeatureSet_EmitsBomLevelSections()
|
||||
{
|
||||
var document = CycloneDxTestData.CreateFullDocument();
|
||||
var result = _writer.Write(document);
|
||||
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
var root = json.RootElement;
|
||||
|
||||
root.GetProperty("specVersion").GetString().Should().Be("1.7");
|
||||
root.GetProperty("serialNumber").GetString()
|
||||
.Should().StartWith("urn:sha256:");
|
||||
|
||||
root.TryGetProperty("services", out var services).Should().BeTrue();
|
||||
services.GetArrayLength().Should().Be(1);
|
||||
services[0].GetProperty("endpoints")[0].GetString()
|
||||
.Should().Be("https://api.example.com/a");
|
||||
|
||||
root.TryGetProperty("formulation", out var formulation).Should().BeTrue();
|
||||
formulation.GetArrayLength().Should().Be(1);
|
||||
formulation[0].GetProperty("workflows")[0].GetProperty("tasks").GetArrayLength()
|
||||
.Should().Be(1);
|
||||
|
||||
root.TryGetProperty("annotations", out var annotations).Should().BeTrue();
|
||||
annotations.GetArrayLength().Should().Be(1);
|
||||
|
||||
root.TryGetProperty("compositions", out var compositions).Should().BeTrue();
|
||||
compositions.GetArrayLength().Should().Be(1);
|
||||
|
||||
root.TryGetProperty("declarations", out var declarations).Should().BeTrue();
|
||||
declarations.GetProperty("attestations").GetArrayLength().Should().Be(1);
|
||||
|
||||
root.TryGetProperty("definitions", out var definitions).Should().BeTrue();
|
||||
definitions.GetProperty("standards").GetArrayLength().Should().Be(1);
|
||||
|
||||
root.TryGetProperty("signature", out var signature).Should().BeTrue();
|
||||
signature.GetProperty("algorithm").GetString().Should().Be("RS256");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Write_ComponentExtensions_EmitNewFields()
|
||||
{
|
||||
var document = CycloneDxTestData.CreateFullDocument();
|
||||
var result = _writer.Write(document);
|
||||
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
var component = FindComponent(json.RootElement, "component-a");
|
||||
|
||||
component.GetProperty("scope").GetString().Should().Be("required");
|
||||
component.GetProperty("modified").GetBoolean().Should().BeTrue();
|
||||
component.GetProperty("pedigree").ValueKind.Should().Be(JsonValueKind.Object);
|
||||
component.GetProperty("swid").ValueKind.Should().Be(JsonValueKind.Object);
|
||||
component.GetProperty("evidence").ValueKind.Should().Be(JsonValueKind.Object);
|
||||
component.GetProperty("releaseNotes").ValueKind.Should().Be(JsonValueKind.Object);
|
||||
component.GetProperty("modelCard").ValueKind.Should().Be(JsonValueKind.Object);
|
||||
component.GetProperty("cryptoProperties").ValueKind.Should().Be(JsonValueKind.Object);
|
||||
component.GetProperty("signature").ValueKind.Should().Be(JsonValueKind.Object);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Write_CompositionAggregates_MapToSpecValues()
|
||||
{
|
||||
var aggregates = Enum.GetValues<SbomCompositionAggregate>();
|
||||
var compositions = aggregates
|
||||
.Select((aggregate, index) => new SbomComposition
|
||||
{
|
||||
BomRef = $"composition-{index}",
|
||||
Aggregate = aggregate
|
||||
})
|
||||
.ToImmutableArray();
|
||||
|
||||
var document = new SbomDocument
|
||||
{
|
||||
Name = "composition-bom",
|
||||
Version = "1",
|
||||
Timestamp = CycloneDxTestData.FixedTimestamp,
|
||||
Components =
|
||||
[
|
||||
new SbomComponent
|
||||
{
|
||||
BomRef = "component-a",
|
||||
Name = "component-a",
|
||||
Version = "1.0.0",
|
||||
Type = SbomComponentType.Library
|
||||
}
|
||||
],
|
||||
Compositions = compositions
|
||||
};
|
||||
|
||||
var result = _writer.Write(document);
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
var actual = json.RootElement.GetProperty("compositions")
|
||||
.EnumerateArray()
|
||||
.ToDictionary(
|
||||
entry => entry.GetProperty("bom-ref").GetString() ?? string.Empty,
|
||||
entry => entry.GetProperty("aggregate").GetString() ?? string.Empty,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
for (var i = 0; i < aggregates.Length; i++)
|
||||
{
|
||||
var expected = ExpectedAggregate(aggregates[i]);
|
||||
actual[$"composition-{i}"].Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Write_CryptoProperties_AssetTypesMapped()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
new SbomComponent
|
||||
{
|
||||
BomRef = "crypto-algorithm",
|
||||
Name = "crypto-algorithm",
|
||||
Version = "1.0.0",
|
||||
Type = SbomComponentType.Library,
|
||||
CryptoProperties = new SbomCryptoProperties
|
||||
{
|
||||
AssetType = SbomCryptoAssetType.Algorithm
|
||||
}
|
||||
},
|
||||
new SbomComponent
|
||||
{
|
||||
BomRef = "crypto-certificate",
|
||||
Name = "crypto-certificate",
|
||||
Version = "1.0.0",
|
||||
Type = SbomComponentType.Library,
|
||||
CryptoProperties = new SbomCryptoProperties
|
||||
{
|
||||
AssetType = SbomCryptoAssetType.Certificate,
|
||||
CertificateProperties = new SbomCryptoCertificateProperties
|
||||
{
|
||||
SerialNumber = "1234"
|
||||
}
|
||||
}
|
||||
},
|
||||
new SbomComponent
|
||||
{
|
||||
BomRef = "crypto-protocol",
|
||||
Name = "crypto-protocol",
|
||||
Version = "1.0.0",
|
||||
Type = SbomComponentType.Library,
|
||||
CryptoProperties = new SbomCryptoProperties
|
||||
{
|
||||
AssetType = SbomCryptoAssetType.Protocol,
|
||||
ProtocolProperties = new SbomCryptoProtocolProperties
|
||||
{
|
||||
Type = "tls",
|
||||
Version = "1.3"
|
||||
}
|
||||
}
|
||||
},
|
||||
new SbomComponent
|
||||
{
|
||||
BomRef = "crypto-material",
|
||||
Name = "crypto-material",
|
||||
Version = "1.0.0",
|
||||
Type = SbomComponentType.Library,
|
||||
CryptoProperties = new SbomCryptoProperties
|
||||
{
|
||||
AssetType = SbomCryptoAssetType.RelatedCryptoMaterial,
|
||||
RelatedCryptoMaterialProperties = new SbomRelatedCryptoMaterialProperties
|
||||
{
|
||||
Type = "key",
|
||||
Id = "key-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var document = new SbomDocument
|
||||
{
|
||||
Name = "crypto-bom",
|
||||
Version = "1",
|
||||
Timestamp = CycloneDxTestData.FixedTimestamp,
|
||||
Components = [.. components]
|
||||
};
|
||||
|
||||
var result = _writer.Write(document);
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
|
||||
FindComponent(json.RootElement, "crypto-algorithm")
|
||||
.GetProperty("cryptoProperties").GetProperty("assetType").GetString()
|
||||
.Should().Be("algorithm");
|
||||
FindComponent(json.RootElement, "crypto-certificate")
|
||||
.GetProperty("cryptoProperties").GetProperty("assetType").GetString()
|
||||
.Should().Be("certificate");
|
||||
FindComponent(json.RootElement, "crypto-protocol")
|
||||
.GetProperty("cryptoProperties").GetProperty("assetType").GetString()
|
||||
.Should().Be("protocol");
|
||||
FindComponent(json.RootElement, "crypto-material")
|
||||
.GetProperty("cryptoProperties").GetProperty("assetType").GetString()
|
||||
.Should().Be("related-crypto-material");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RoundTrip_GeneratedBomMatchesParserHash()
|
||||
{
|
||||
var document = CycloneDxTestData.CreateFullDocument();
|
||||
var result = _writer.Write(document);
|
||||
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
var extraction = _parser.ExtractSbom(json.RootElement);
|
||||
|
||||
extraction.Should().NotBeNull();
|
||||
extraction!.SbomSha256.Should().Be(result.GoldenHash);
|
||||
}
|
||||
|
||||
private static JsonElement FindComponent(JsonElement root, string bomRef)
|
||||
{
|
||||
foreach (var component in root.GetProperty("components").EnumerateArray())
|
||||
{
|
||||
if (component.TryGetProperty("bom-ref", out var reference) &&
|
||||
string.Equals(reference.GetString(), bomRef, StringComparison.Ordinal))
|
||||
{
|
||||
return component;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Component '{bomRef}' not found.");
|
||||
}
|
||||
|
||||
private static string ExpectedAggregate(SbomCompositionAggregate aggregate)
|
||||
{
|
||||
return aggregate switch
|
||||
{
|
||||
SbomCompositionAggregate.Complete => "complete",
|
||||
SbomCompositionAggregate.Incomplete => "incomplete",
|
||||
SbomCompositionAggregate.IncompleteFirstPartyOnly =>
|
||||
"incomplete_first_party_only",
|
||||
SbomCompositionAggregate.IncompleteFirstPartyProprietaryOnly =>
|
||||
"incomplete_first_party_proprietary_only",
|
||||
SbomCompositionAggregate.IncompleteFirstPartyOpensourceOnly =>
|
||||
"incomplete_first_party_opensource_only",
|
||||
SbomCompositionAggregate.IncompleteThirdPartyOnly =>
|
||||
"incomplete_third_party_only",
|
||||
SbomCompositionAggregate.IncompleteThirdPartyProprietaryOnly =>
|
||||
"incomplete_third_party_proprietary_only",
|
||||
SbomCompositionAggregate.IncompleteThirdPartyOpensourceOnly =>
|
||||
"incomplete_third_party_opensource_only",
|
||||
SbomCompositionAggregate.Unknown => "unknown",
|
||||
SbomCompositionAggregate.NotSpecified => "not_specified",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CycloneDxSchemaValidationTests.cs
|
||||
// Sprint: SPRINT_20260119_013_Attestor_cyclonedx_1.7_generation
|
||||
// Task: TASK-013-010 - Schema validation coverage
|
||||
// Description: Validates CycloneDX 1.7 output against stored schema.
|
||||
// -----------------------------------------------------------------------------
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Json.Schema;
|
||||
using StellaOps.Attestor.StandardPredicates.Writers;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests;
|
||||
|
||||
public sealed class CycloneDxSchemaValidationTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SchemaFile_ValidatesGeneratedBom()
|
||||
{
|
||||
var schema = LoadSchemaFromDocs();
|
||||
var writer = new CycloneDxWriter();
|
||||
var document = CycloneDxTestData.CreateMinimalDocument();
|
||||
var result = writer.Write(document);
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
|
||||
var evaluation = schema.Evaluate(json.RootElement, new EvaluationOptions
|
||||
{
|
||||
OutputFormat = OutputFormat.List,
|
||||
RequireFormatValidation = true
|
||||
});
|
||||
|
||||
evaluation.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
private static JsonSchema LoadSchemaFromDocs()
|
||||
{
|
||||
var root = FindRepoRoot();
|
||||
var schemaPath = Path.Combine(root, "docs", "schemas", "cyclonedx-bom-1.7.schema.json");
|
||||
File.Exists(schemaPath).Should().BeTrue($"schema file should exist at '{schemaPath}'");
|
||||
var schemaText = File.ReadAllText(schemaPath);
|
||||
return JsonSchema.FromText(schemaText, new BuildOptions
|
||||
{
|
||||
SchemaRegistry = new SchemaRegistry()
|
||||
});
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
var docs = Path.Combine(directory.FullName, "docs");
|
||||
var src = Path.Combine(directory.FullName, "src");
|
||||
if (Directory.Exists(docs) && Directory.Exists(src))
|
||||
{
|
||||
return directory.FullName;
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Repository root not found.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CycloneDxTestData.cs
|
||||
// Sprint: SPRINT_20260119_013_Attestor_cyclonedx_1.7_generation
|
||||
// Task: TASK-013-009 - CycloneDX 1.7 feature fixtures
|
||||
// Description: Deterministic CycloneDX 1.7 test fixtures.
|
||||
// -----------------------------------------------------------------------------
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Attestor.StandardPredicates.Models;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests;
|
||||
|
||||
internal static class CycloneDxTestData
|
||||
{
|
||||
internal static readonly DateTimeOffset FixedTimestamp =
|
||||
new(2026, 1, 20, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
internal static SbomDocument CreateFullDocument()
|
||||
{
|
||||
var componentSignature = new SbomSignature
|
||||
{
|
||||
Algorithm = SbomSignatureAlgorithm.ES256,
|
||||
KeyId = "component-key",
|
||||
PublicKey = new SbomJsonWebKey
|
||||
{
|
||||
KeyType = "EC",
|
||||
Curve = "P-256",
|
||||
X = "x-value",
|
||||
Y = "y-value"
|
||||
},
|
||||
CertificatePath = ["component-cert"],
|
||||
Value = "Y29tcC1zaWc="
|
||||
};
|
||||
|
||||
var component = new SbomComponent
|
||||
{
|
||||
BomRef = "component-a",
|
||||
Name = "component-a",
|
||||
Version = "1.0.0",
|
||||
Type = SbomComponentType.MachineLearningModel,
|
||||
Description = "ml component",
|
||||
Scope = SbomComponentScope.Required,
|
||||
Modified = true,
|
||||
Pedigree = new SbomComponentPedigree
|
||||
{
|
||||
Ancestors =
|
||||
[
|
||||
new SbomComponent
|
||||
{
|
||||
BomRef = "component-ancestor",
|
||||
Name = "component-ancestor",
|
||||
Version = "0.9.0",
|
||||
Type = SbomComponentType.Library
|
||||
}
|
||||
],
|
||||
Notes = "pedigree notes"
|
||||
},
|
||||
Swid = new SbomSwid
|
||||
{
|
||||
TagId = "swid-tag",
|
||||
Name = "component-a",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
Evidence = new SbomComponentEvidence
|
||||
{
|
||||
Identity =
|
||||
[
|
||||
new SbomComponentIdentityEvidence
|
||||
{
|
||||
Field = "purl",
|
||||
Confidence = 0.9,
|
||||
ConcludedValue = "pkg:generic/component-a@1.0.0",
|
||||
Methods =
|
||||
[
|
||||
new SbomComponentIdentityEvidenceMethod
|
||||
{
|
||||
Technique = "hash",
|
||||
Confidence = 0.8,
|
||||
Value = "sha256:abc"
|
||||
}
|
||||
],
|
||||
Tools = ["scanner"]
|
||||
}
|
||||
],
|
||||
Occurrences =
|
||||
[
|
||||
new SbomComponentEvidenceOccurrence
|
||||
{
|
||||
BomRef = "occ-1",
|
||||
Location = "/opt/component-a",
|
||||
Line = 12
|
||||
}
|
||||
],
|
||||
Callstack = new SbomComponentEvidenceCallstack
|
||||
{
|
||||
Frames =
|
||||
[
|
||||
new SbomComponentCallstackFrame
|
||||
{
|
||||
Module = "module-a",
|
||||
Function = "run",
|
||||
Parameters = ["arg1"],
|
||||
Line = 42
|
||||
}
|
||||
]
|
||||
},
|
||||
Licenses = [new SbomLicense { Id = "MIT" }],
|
||||
Copyright = ["(c) 2026"]
|
||||
},
|
||||
ReleaseNotes = new SbomReleaseNotes
|
||||
{
|
||||
Type = "added",
|
||||
Title = "release",
|
||||
Description = "release notes",
|
||||
Timestamp = FixedTimestamp,
|
||||
Aliases = ["v1.0.0"],
|
||||
Tags = ["stable"],
|
||||
Resolves =
|
||||
[
|
||||
new SbomIssue
|
||||
{
|
||||
Type = "bug",
|
||||
Id = "BUG-1",
|
||||
Name = "issue",
|
||||
Description = "fixed",
|
||||
Source = new SbomIssueSource
|
||||
{
|
||||
Name = "tracker",
|
||||
Url = "https://example.com/bugs"
|
||||
}
|
||||
}
|
||||
],
|
||||
Notes = [new SbomReleaseNote { Locale = "en-US", Text = "note" }],
|
||||
Properties = [new SbomProperty { Name = "severity", Value = "low" }]
|
||||
},
|
||||
ModelCard = new SbomModelCard
|
||||
{
|
||||
BomRef = "modelcard-1",
|
||||
ModelParameters = new SbomModelParameters
|
||||
{
|
||||
Approach = new SbomModelApproach { Type = "supervised" },
|
||||
Task = "classification",
|
||||
ArchitectureFamily = "transformer",
|
||||
ModelArchitecture = "bert",
|
||||
Datasets =
|
||||
[
|
||||
new SbomModelDataset { Reference = "dataset-ref" },
|
||||
new SbomModelDataset
|
||||
{
|
||||
Data = new SbomComponentData
|
||||
{
|
||||
BomRef = "data-1",
|
||||
Name = "dataset",
|
||||
Type = "training"
|
||||
}
|
||||
}
|
||||
],
|
||||
Inputs = [new SbomModelInputOutput { Format = "text/plain" }],
|
||||
Outputs = [new SbomModelInputOutput { Format = "text/plain" }]
|
||||
},
|
||||
QuantitativeAnalysis = new SbomQuantitativeAnalysis
|
||||
{
|
||||
PerformanceMetrics =
|
||||
[
|
||||
new SbomPerformanceMetric
|
||||
{
|
||||
Type = "accuracy",
|
||||
Value = "0.95",
|
||||
Slice = "overall",
|
||||
ConfidenceInterval = new SbomPerformanceMetricConfidenceInterval
|
||||
{
|
||||
LowerBound = "0.94",
|
||||
UpperBound = "0.96"
|
||||
}
|
||||
}
|
||||
],
|
||||
Graphics = new SbomGraphicsCollection
|
||||
{
|
||||
Description = "performance",
|
||||
Collection = [new SbomGraphic { Name = "chart", Image = "chart.png" }]
|
||||
}
|
||||
},
|
||||
Considerations = new SbomModelConsiderations
|
||||
{
|
||||
Users = ["analysts"],
|
||||
UseCases = ["classification"],
|
||||
TechnicalLimitations = ["limited data"],
|
||||
PerformanceTradeoffs = ["latency"],
|
||||
EthicalConsiderations =
|
||||
[
|
||||
new SbomRisk
|
||||
{
|
||||
Name = "bias",
|
||||
MitigationStrategy = "review"
|
||||
}
|
||||
],
|
||||
EnvironmentalConsiderations = new SbomEnvironmentalConsiderations
|
||||
{
|
||||
EnergyConsumptions =
|
||||
[
|
||||
new SbomEnergyConsumption
|
||||
{
|
||||
Activity = "training",
|
||||
EnergyProviders =
|
||||
[
|
||||
new SbomEnergyProvider
|
||||
{
|
||||
BomRef = "energy-1",
|
||||
Description = "solar",
|
||||
Organization = new SbomOrganizationalEntity
|
||||
{
|
||||
Name = "energy-org"
|
||||
},
|
||||
EnergySource = "solar",
|
||||
EnergyProvided = "100kwh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
Properties = [new SbomProperty { Name = "co2", Value = "low" }]
|
||||
},
|
||||
FairnessAssessments =
|
||||
[
|
||||
new SbomFairnessAssessment
|
||||
{
|
||||
GroupAtRisk = "group",
|
||||
Benefits = "benefit",
|
||||
Harms = "harm",
|
||||
MitigationStrategy = "mitigate"
|
||||
}
|
||||
]
|
||||
},
|
||||
Properties = [new SbomProperty { Name = "model", Value = "v1" }]
|
||||
},
|
||||
CryptoProperties = new SbomCryptoProperties
|
||||
{
|
||||
AssetType = SbomCryptoAssetType.Algorithm,
|
||||
AlgorithmProperties = new SbomCryptoAlgorithmProperties
|
||||
{
|
||||
Primitive = "aes",
|
||||
AlgorithmFamily = "aes",
|
||||
Mode = "gcm",
|
||||
Padding = "none",
|
||||
CryptoFunctions = ["encrypt", "decrypt"],
|
||||
ClassicalSecurityLevel = 128,
|
||||
NistQuantumSecurityLevel = 1,
|
||||
KeySize = 256
|
||||
},
|
||||
Oid = "1.2.840.113549.1.1.1"
|
||||
},
|
||||
Signature = componentSignature,
|
||||
Properties = ImmutableDictionary<string, string>.Empty
|
||||
.Add("build", "release")
|
||||
.Add("source", "unit-test")
|
||||
};
|
||||
|
||||
var service = new SbomService
|
||||
{
|
||||
BomRef = "service-a",
|
||||
Provider = new SbomOrganizationalEntity { Name = "service-org" },
|
||||
Group = "services",
|
||||
Name = "api-service",
|
||||
Version = "2.0.0",
|
||||
Description = "service description",
|
||||
Endpoints = ["https://api.example.com/b", "https://api.example.com/a"],
|
||||
Authenticated = true,
|
||||
TrustBoundary = true,
|
||||
TrustZone = "zone-a",
|
||||
Data =
|
||||
[
|
||||
new SbomServiceData
|
||||
{
|
||||
Flow = "inbound",
|
||||
Classification = "restricted",
|
||||
Name = "payload",
|
||||
Description = "service data",
|
||||
Source = ["client"],
|
||||
Destination = ["service"]
|
||||
}
|
||||
],
|
||||
Services =
|
||||
[
|
||||
new SbomService
|
||||
{
|
||||
BomRef = "service-nested",
|
||||
Name = "nested-service",
|
||||
Endpoints = ["https://nested.example.com"]
|
||||
}
|
||||
],
|
||||
Properties = [new SbomProperty { Name = "tier", Value = "gold" }],
|
||||
Tags = ["backend", "api"],
|
||||
Signature = new SbomSignature
|
||||
{
|
||||
Algorithm = SbomSignatureAlgorithm.HS256,
|
||||
KeyId = "service-key",
|
||||
PublicKey = new SbomJsonWebKey { KeyType = "RSA", Modulus = "mod", Exponent = "AQAB" },
|
||||
Value = "c2VydmljZS1zaWc="
|
||||
}
|
||||
};
|
||||
|
||||
var formulation = new SbomFormulation
|
||||
{
|
||||
BomRef = "formulation-1",
|
||||
Components = [component],
|
||||
Services = [service],
|
||||
Workflows =
|
||||
[
|
||||
new SbomWorkflow
|
||||
{
|
||||
BomRef = "workflow-1",
|
||||
Uid = "workflow-uid",
|
||||
Name = "workflow",
|
||||
Description = "build workflow",
|
||||
ResourceReferences = ["resource-b", "resource-a"],
|
||||
Tasks =
|
||||
[
|
||||
new SbomTask
|
||||
{
|
||||
BomRef = "task-1",
|
||||
Uid = "task-uid",
|
||||
Name = "task",
|
||||
TaskTypes = ["build"],
|
||||
Steps =
|
||||
[
|
||||
new SbomStep
|
||||
{
|
||||
Name = "step",
|
||||
Description = "run",
|
||||
Commands = ["make build"]
|
||||
}
|
||||
],
|
||||
Inputs =
|
||||
[
|
||||
new SbomWorkflowInput
|
||||
{
|
||||
Source = "source-a",
|
||||
Target = "target-a",
|
||||
Resource = "resource-a",
|
||||
Parameters = [new SbomProperty { Name = "flag", Value = "on" }]
|
||||
}
|
||||
],
|
||||
Outputs =
|
||||
[
|
||||
new SbomWorkflowOutput
|
||||
{
|
||||
Type = "artifact",
|
||||
Source = "source-b",
|
||||
Target = "target-b",
|
||||
Resource = "resource-b",
|
||||
Data = ["output-a"]
|
||||
}
|
||||
],
|
||||
TimeStart = FixedTimestamp,
|
||||
TimeEnd = FixedTimestamp.AddMinutes(10)
|
||||
}
|
||||
],
|
||||
TaskDependencies = ["task-1"],
|
||||
TaskTypes = ["build"],
|
||||
Trigger = new SbomTrigger
|
||||
{
|
||||
BomRef = "trigger-1",
|
||||
Uid = "trigger-uid",
|
||||
Name = "trigger",
|
||||
Description = "on commit",
|
||||
Type = "event",
|
||||
Event = "push",
|
||||
Conditions = ["branch=main"],
|
||||
TimeActivated = FixedTimestamp
|
||||
},
|
||||
Steps =
|
||||
[
|
||||
new SbomStep
|
||||
{
|
||||
Name = "workflow-step",
|
||||
Description = "workflow step",
|
||||
Commands = ["echo start"]
|
||||
}
|
||||
],
|
||||
Inputs =
|
||||
[
|
||||
new SbomWorkflowInput
|
||||
{
|
||||
Source = "workflow-source",
|
||||
Target = "workflow-target",
|
||||
Resource = "workflow-resource",
|
||||
Data = ["input-a"]
|
||||
}
|
||||
],
|
||||
Outputs =
|
||||
[
|
||||
new SbomWorkflowOutput
|
||||
{
|
||||
Type = "result",
|
||||
Source = "workflow-source",
|
||||
Target = "workflow-target",
|
||||
Data = ["output-b"]
|
||||
}
|
||||
],
|
||||
TimeStart = FixedTimestamp,
|
||||
TimeEnd = FixedTimestamp.AddMinutes(30)
|
||||
}
|
||||
],
|
||||
Properties = [new SbomProperty { Name = "pipeline", Value = "ci" }]
|
||||
};
|
||||
|
||||
var annotation = new SbomAnnotation
|
||||
{
|
||||
BomRef = "annotation-1",
|
||||
Subjects = ["component-a"],
|
||||
Annotator = new SbomAnnotationAnnotator
|
||||
{
|
||||
Organization = new SbomOrganizationalEntity { Name = "annotator-org" }
|
||||
},
|
||||
Timestamp = FixedTimestamp,
|
||||
Text = "annotation text",
|
||||
Signature = new SbomSignature
|
||||
{
|
||||
Algorithm = SbomSignatureAlgorithm.RS256,
|
||||
KeyId = "annotation-key",
|
||||
PublicKey = new SbomJsonWebKey { KeyType = "RSA", Modulus = "mod", Exponent = "AQAB" },
|
||||
Value = "YW5ub3RhdGlvbi1zaWc="
|
||||
}
|
||||
};
|
||||
|
||||
var composition = new SbomComposition
|
||||
{
|
||||
BomRef = "composition-1",
|
||||
Aggregate = SbomCompositionAggregate.IncompleteFirstPartyOnly,
|
||||
Assemblies = ["component-a"],
|
||||
Dependencies = ["component-a"]
|
||||
};
|
||||
|
||||
var declarations = new SbomDeclaration
|
||||
{
|
||||
Assessors =
|
||||
[
|
||||
new SbomAssessor
|
||||
{
|
||||
BomRef = "assessor-1",
|
||||
ThirdParty = true,
|
||||
Organization = new SbomOrganizationalEntity { Name = "assessor-org" }
|
||||
}
|
||||
],
|
||||
Attestations =
|
||||
[
|
||||
new SbomAttestation
|
||||
{
|
||||
Summary = "attestation",
|
||||
Assessor = "assessor-1",
|
||||
Map =
|
||||
[
|
||||
new SbomAttestationMap
|
||||
{
|
||||
Requirement = "req-1",
|
||||
Claims = ["claim-1"],
|
||||
Conformance = new SbomAttestationConformance
|
||||
{
|
||||
Score = 0.9,
|
||||
Rationale = "meets requirement",
|
||||
MitigationStrategies = ["review"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
Claims =
|
||||
[
|
||||
new SbomClaim
|
||||
{
|
||||
BomRef = "claim-1",
|
||||
Target = "component-a",
|
||||
Predicate = "predicate",
|
||||
MitigationStrategies = ["mitigation"],
|
||||
Reasoning = "verified"
|
||||
}
|
||||
],
|
||||
Evidence =
|
||||
[
|
||||
new SbomDeclarationEvidence
|
||||
{
|
||||
BomRef = "evidence-1",
|
||||
PropertyName = "property",
|
||||
Description = "evidence entry",
|
||||
Data = "evidence-data",
|
||||
Created = FixedTimestamp,
|
||||
Author = new SbomOrganizationalContact { Name = "author" },
|
||||
Reviewer = new SbomOrganizationalContact { Name = "reviewer" }
|
||||
}
|
||||
],
|
||||
Targets = new SbomDeclarationTargets
|
||||
{
|
||||
Organizations = [new SbomOrganizationalEntity { Name = "target-org" }],
|
||||
Components = [component],
|
||||
Services = [service]
|
||||
},
|
||||
Affirmation = new SbomAffirmation
|
||||
{
|
||||
Statement = "affirmed",
|
||||
Signatories =
|
||||
[
|
||||
new SbomSignatory
|
||||
{
|
||||
Name = "signer",
|
||||
Role = "reviewer",
|
||||
Organization = new SbomOrganizationalEntity { Name = "signer-org" },
|
||||
ExternalReference = new SbomExternalReference
|
||||
{
|
||||
Type = "website",
|
||||
Url = "https://example.com/signers"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
Signature = new SbomSignature
|
||||
{
|
||||
Algorithm = SbomSignatureAlgorithm.RS256,
|
||||
KeyId = "declaration-key",
|
||||
PublicKey = new SbomJsonWebKey { KeyType = "RSA", Modulus = "mod", Exponent = "AQAB" },
|
||||
Value = "ZGVjbGFyYXRpb24="
|
||||
}
|
||||
};
|
||||
|
||||
var definitions = new SbomDefinition
|
||||
{
|
||||
Standards =
|
||||
[
|
||||
new SbomStandard
|
||||
{
|
||||
BomRef = "standard-1",
|
||||
Name = "Standard",
|
||||
Version = "1.0",
|
||||
Description = "standard description",
|
||||
Owner = new SbomOrganizationalEntity { Name = "standards-org" },
|
||||
Requirements =
|
||||
[
|
||||
new SbomRequirement
|
||||
{
|
||||
BomRef = "req-1",
|
||||
Identifier = "REQ-1",
|
||||
Title = "Requirement",
|
||||
Text = "Requirement text",
|
||||
Descriptions = ["Requirement description"]
|
||||
}
|
||||
],
|
||||
ExternalReferences =
|
||||
[
|
||||
new SbomExternalReference
|
||||
{
|
||||
Type = "website",
|
||||
Url = "https://example.com/standard"
|
||||
}
|
||||
],
|
||||
Signature = new SbomSignature
|
||||
{
|
||||
Algorithm = SbomSignatureAlgorithm.RS384,
|
||||
KeyId = "standard-key",
|
||||
PublicKey = new SbomJsonWebKey { KeyType = "RSA", Modulus = "mod", Exponent = "AQAB" },
|
||||
Value = "c3RhbmRhcmQ="
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return new SbomDocument
|
||||
{
|
||||
Name = "full-bom",
|
||||
Version = "1",
|
||||
Timestamp = FixedTimestamp,
|
||||
ArtifactDigest = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
Metadata = new SbomMetadata
|
||||
{
|
||||
Tools = ["stella-writer"],
|
||||
Authors = ["test@example.com"]
|
||||
},
|
||||
Components = [component],
|
||||
Relationships =
|
||||
[
|
||||
new SbomRelationship
|
||||
{
|
||||
SourceRef = "component-a",
|
||||
TargetRef = "component-a",
|
||||
Type = SbomRelationshipType.DependsOn
|
||||
}
|
||||
],
|
||||
Services = [service],
|
||||
Formulation = [formulation],
|
||||
Annotations = [annotation],
|
||||
Compositions = [composition],
|
||||
Declarations = declarations,
|
||||
Definitions = definitions,
|
||||
Signature = new SbomSignature
|
||||
{
|
||||
Algorithm = SbomSignatureAlgorithm.RS256,
|
||||
KeyId = "bom-key",
|
||||
PublicKey = new SbomJsonWebKey
|
||||
{
|
||||
KeyType = "RSA",
|
||||
Modulus = "mod",
|
||||
Exponent = "AQAB"
|
||||
},
|
||||
CertificatePath = ["bom-cert"],
|
||||
Value = "Ym9tLXNpZw=="
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
internal static SbomDocument CreateMinimalDocument()
|
||||
{
|
||||
return new SbomDocument
|
||||
{
|
||||
Name = "schema-bom",
|
||||
Version = "1",
|
||||
Timestamp = FixedTimestamp,
|
||||
ArtifactDigest = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
Metadata = new SbomMetadata
|
||||
{
|
||||
Tools = ["stella-writer"]
|
||||
},
|
||||
Components =
|
||||
[
|
||||
new SbomComponent
|
||||
{
|
||||
BomRef = "component-a",
|
||||
Name = "component-a",
|
||||
Version = "1.0.0",
|
||||
Type = SbomComponentType.Library
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
// Description: Tests for deterministic serialNumber generation using artifact digest
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Attestor.StandardPredicates.Models;
|
||||
using StellaOps.Attestor.StandardPredicates.Writers;
|
||||
using Xunit;
|
||||
using System.Text.Json;
|
||||
@@ -33,8 +34,8 @@ public sealed class SerialNumberDerivationTests
|
||||
var document = CreateDocument(artifactDigest);
|
||||
|
||||
// Act
|
||||
var bytes = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(bytes);
|
||||
var result = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(result.CanonicalBytes);
|
||||
var parsed = JsonDocument.Parse(json);
|
||||
var serialNumber = parsed.RootElement.GetProperty("serialNumber").GetString();
|
||||
|
||||
@@ -55,8 +56,8 @@ public sealed class SerialNumberDerivationTests
|
||||
var document = CreateDocument(rawDigest);
|
||||
|
||||
// Act
|
||||
var bytes = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(bytes);
|
||||
var result = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(result.CanonicalBytes);
|
||||
var parsed = JsonDocument.Parse(json);
|
||||
var serialNumber = parsed.RootElement.GetProperty("serialNumber").GetString();
|
||||
|
||||
@@ -76,8 +77,8 @@ public sealed class SerialNumberDerivationTests
|
||||
var document = CreateDocument(uppercaseDigest);
|
||||
|
||||
// Act
|
||||
var bytes = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(bytes);
|
||||
var result = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(result.CanonicalBytes);
|
||||
var parsed = JsonDocument.Parse(json);
|
||||
var serialNumber = parsed.RootElement.GetProperty("serialNumber").GetString();
|
||||
|
||||
@@ -96,8 +97,8 @@ public sealed class SerialNumberDerivationTests
|
||||
var document = CreateDocument(null);
|
||||
|
||||
// Act
|
||||
var bytes = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(bytes);
|
||||
var result = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(result.CanonicalBytes);
|
||||
var parsed = JsonDocument.Parse(json);
|
||||
var serialNumber = parsed.RootElement.GetProperty("serialNumber").GetString();
|
||||
|
||||
@@ -116,8 +117,8 @@ public sealed class SerialNumberDerivationTests
|
||||
var document = CreateDocument("");
|
||||
|
||||
// Act
|
||||
var bytes = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(bytes);
|
||||
var result = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(result.CanonicalBytes);
|
||||
var parsed = JsonDocument.Parse(json);
|
||||
var serialNumber = parsed.RootElement.GetProperty("serialNumber").GetString();
|
||||
|
||||
@@ -137,8 +138,8 @@ public sealed class SerialNumberDerivationTests
|
||||
var document = CreateDocument(shortDigest);
|
||||
|
||||
// Act
|
||||
var bytes = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(bytes);
|
||||
var result = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(result.CanonicalBytes);
|
||||
var parsed = JsonDocument.Parse(json);
|
||||
var serialNumber = parsed.RootElement.GetProperty("serialNumber").GetString();
|
||||
|
||||
@@ -158,8 +159,8 @@ public sealed class SerialNumberDerivationTests
|
||||
var document = CreateDocument(invalidDigest);
|
||||
|
||||
// Act
|
||||
var bytes = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(bytes);
|
||||
var result = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(result.CanonicalBytes);
|
||||
var parsed = JsonDocument.Parse(json);
|
||||
var serialNumber = parsed.RootElement.GetProperty("serialNumber").GetString();
|
||||
|
||||
@@ -184,11 +185,11 @@ public sealed class SerialNumberDerivationTests
|
||||
var doc2 = CreateDocument(artifactDigest);
|
||||
|
||||
// Act
|
||||
var bytes1 = _writer.Write(doc1);
|
||||
var bytes2 = _writer.Write(doc2);
|
||||
var result1 = _writer.Write(doc1);
|
||||
var result2 = _writer.Write(doc2);
|
||||
|
||||
var json1 = Encoding.UTF8.GetString(bytes1);
|
||||
var json2 = Encoding.UTF8.GetString(bytes2);
|
||||
var json1 = Encoding.UTF8.GetString(result1.CanonicalBytes);
|
||||
var json2 = Encoding.UTF8.GetString(result2.CanonicalBytes);
|
||||
|
||||
var parsed1 = JsonDocument.Parse(json1);
|
||||
var parsed2 = JsonDocument.Parse(json2);
|
||||
@@ -214,11 +215,11 @@ public sealed class SerialNumberDerivationTests
|
||||
var doc2 = CreateDocument(digest2);
|
||||
|
||||
// Act
|
||||
var bytes1 = _writer.Write(doc1);
|
||||
var bytes2 = _writer.Write(doc2);
|
||||
var result1 = _writer.Write(doc1);
|
||||
var result2 = _writer.Write(doc2);
|
||||
|
||||
var json1 = Encoding.UTF8.GetString(bytes1);
|
||||
var json2 = Encoding.UTF8.GetString(bytes2);
|
||||
var json1 = Encoding.UTF8.GetString(result1.CanonicalBytes);
|
||||
var json2 = Encoding.UTF8.GetString(result2.CanonicalBytes);
|
||||
|
||||
var parsed1 = JsonDocument.Parse(json1);
|
||||
var parsed2 = JsonDocument.Parse(json2);
|
||||
@@ -246,8 +247,8 @@ public sealed class SerialNumberDerivationTests
|
||||
// Act
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
var bytes = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(bytes);
|
||||
var result = _writer.Write(document);
|
||||
var json = Encoding.UTF8.GetString(result.CanonicalBytes);
|
||||
var parsed = JsonDocument.Parse(json);
|
||||
var serialNumber = parsed.RootElement.GetProperty("serialNumber").GetString()!;
|
||||
serialNumbers.Add(serialNumber);
|
||||
@@ -268,8 +269,12 @@ public sealed class SerialNumberDerivationTests
|
||||
{
|
||||
Name = "test-app",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = new DateTimeOffset(2026, 1, 18, 12, 0, 0, TimeSpan.Zero),
|
||||
Timestamp = new DateTimeOffset(2026, 1, 18, 12, 0, 0, TimeSpan.Zero),
|
||||
ArtifactDigest = artifactDigest,
|
||||
Metadata = new SbomMetadata
|
||||
{
|
||||
Tools = ["stella-scanner@1.0.0"]
|
||||
},
|
||||
Components =
|
||||
[
|
||||
new SbomComponent
|
||||
@@ -277,14 +282,9 @@ public sealed class SerialNumberDerivationTests
|
||||
BomRef = "lodash",
|
||||
Name = "lodash",
|
||||
Version = "4.17.21",
|
||||
Type = "library"
|
||||
Type = SbomComponentType.Library
|
||||
}
|
||||
],
|
||||
Tool = new SbomTool
|
||||
{
|
||||
Name = "stella-scanner",
|
||||
Version = "1.0.0"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Description: Tests proving deterministic SPDX output
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.StandardPredicates.Models;
|
||||
using StellaOps.Attestor.StandardPredicates.Writers;
|
||||
using Xunit;
|
||||
@@ -133,11 +134,137 @@ public sealed class SpdxDeterminismTests
|
||||
var document = CreateTestDocument("context-test", "1.0.0");
|
||||
var result = _writer.Write(document);
|
||||
|
||||
var json = System.Text.Encoding.UTF8.GetString(result.CanonicalBytes);
|
||||
var json = System.Text.Encoding.UTF8.GetString(result.CanonicalBytes);
|
||||
Assert.Contains("@context", json);
|
||||
Assert.Contains("spdx.org", json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test Case 6: External references are serialized for packages.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExternalReferences_AreSerialized()
|
||||
{
|
||||
var component = CreateComponent("pkg:npm/ext@1.0.0", "ext")
|
||||
with
|
||||
{
|
||||
ExternalReferences =
|
||||
[
|
||||
new SbomExternalReference
|
||||
{
|
||||
Type = "website",
|
||||
Url = "https://example.com/ext",
|
||||
Comment = "home"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var document = CreateDocumentWithComponents("ext-doc", [component]);
|
||||
var result = _writer.Write(document);
|
||||
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
var graph = json.RootElement.GetProperty("@graph");
|
||||
var package = graph.EnumerateArray()
|
||||
.First(element => element.TryGetProperty("@type", out var type) &&
|
||||
type.GetString() == "software_Package" &&
|
||||
element.GetProperty("name").GetString() == "ext");
|
||||
|
||||
var externalRef = package.GetProperty("externalRef")[0];
|
||||
Assert.Equal("AltWebPage", externalRef.GetProperty("externalRefType").GetString());
|
||||
Assert.Equal("https://example.com/ext", externalRef.GetProperty("locator")[0].GetString());
|
||||
Assert.Equal("home", externalRef.GetProperty("comment").GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test Case 7: External identifiers include locator and issuing authority.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExternalIdentifiers_AreSerialized()
|
||||
{
|
||||
var component = CreateComponent("pkg:npm/extid@1.0.0", "extid")
|
||||
with
|
||||
{
|
||||
ExternalIdentifiers =
|
||||
[
|
||||
new SbomExternalIdentifier
|
||||
{
|
||||
Type = "cve",
|
||||
Identifier = "CVE-2024-1234",
|
||||
Locator = "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1234",
|
||||
IssuingAuthority = "mitre",
|
||||
Comment = "primary"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var document = CreateDocumentWithComponents("extid-doc", [component]);
|
||||
var result = _writer.Write(document);
|
||||
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
var graph = json.RootElement.GetProperty("@graph");
|
||||
var package = graph.EnumerateArray()
|
||||
.First(element => element.TryGetProperty("@type", out var type) &&
|
||||
type.GetString() == "software_Package" &&
|
||||
element.GetProperty("name").GetString() == "extid");
|
||||
|
||||
var identifier = package.GetProperty("externalIdentifier")
|
||||
.EnumerateArray()
|
||||
.First(entry => entry.GetProperty("identifier").GetString() == "CVE-2024-1234");
|
||||
|
||||
Assert.Equal("Cve", identifier.GetProperty("externalIdentifierType").GetString());
|
||||
Assert.Equal("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1234",
|
||||
identifier.GetProperty("identifierLocator").GetString());
|
||||
Assert.Equal("mitre", identifier.GetProperty("issuingAuthority").GetString());
|
||||
Assert.Equal("primary", identifier.GetProperty("comment").GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test Case 8: verifiedUsing includes signature integrity methods.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void VerifiedUsing_IncludesSignature()
|
||||
{
|
||||
var component = CreateComponent("pkg:npm/signed@1.0.0", "signed")
|
||||
with
|
||||
{
|
||||
Signature = new SbomSignature
|
||||
{
|
||||
Algorithm = SbomSignatureAlgorithm.ES256,
|
||||
KeyId = "key-1",
|
||||
PublicKey = new SbomJsonWebKey
|
||||
{
|
||||
KeyType = "RSA",
|
||||
Modulus = "modulus",
|
||||
Exponent = "AQAB"
|
||||
},
|
||||
Value = "sigvalue"
|
||||
}
|
||||
};
|
||||
|
||||
var document = CreateDocumentWithComponents("signed-doc", [component]);
|
||||
var result = _writer.Write(document);
|
||||
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
var graph = json.RootElement.GetProperty("@graph");
|
||||
var package = graph.EnumerateArray()
|
||||
.First(element => element.TryGetProperty("@type", out var type) &&
|
||||
type.GetString() == "software_Package" &&
|
||||
element.GetProperty("name").GetString() == "signed");
|
||||
|
||||
var signature = package.GetProperty("verifiedUsing")
|
||||
.EnumerateArray()
|
||||
.First(entry => entry.GetProperty("@type").GetString() == "Signature");
|
||||
|
||||
Assert.Equal("ES256", signature.GetProperty("algorithm").GetString());
|
||||
Assert.Equal("sigvalue", signature.GetProperty("signature").GetString());
|
||||
Assert.Equal("key-1", signature.GetProperty("keyId").GetString());
|
||||
|
||||
var publicKey = signature.GetProperty("publicKey");
|
||||
Assert.Equal("RSA", publicKey.GetProperty("kty").GetString());
|
||||
Assert.Equal("modulus", publicKey.GetProperty("n").GetString());
|
||||
Assert.Equal("AQAB", publicKey.GetProperty("e").GetString());
|
||||
}
|
||||
|
||||
private static SbomDocument CreateTestDocument(string name, string version)
|
||||
{
|
||||
return new SbomDocument
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.StandardPredicates.Models;
|
||||
using StellaOps.Attestor.StandardPredicates.Writers;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests;
|
||||
|
||||
public sealed class SpdxWriterSecurityProfileTests
|
||||
{
|
||||
private const string SecurityProfileUri =
|
||||
"https://spdx.org/rdf/3.0.1/terms/Security/ProfileIdentifierType/security";
|
||||
|
||||
private readonly SpdxWriter _writer = new();
|
||||
|
||||
[Fact]
|
||||
public void VulnerabilityElements_AreSerialized()
|
||||
{
|
||||
var component = new SbomComponent
|
||||
{
|
||||
BomRef = "app",
|
||||
Name = "app",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
|
||||
var vulnerability = new SbomVulnerability
|
||||
{
|
||||
Id = "CVE-2026-0001",
|
||||
Source = "NVD",
|
||||
Summary = "Example vulnerability",
|
||||
Description = "Details",
|
||||
PublishedTime = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
ModifiedTime = new DateTimeOffset(2026, 1, 2, 0, 0, 0, TimeSpan.Zero),
|
||||
AffectedRefs = ["app"],
|
||||
Assessments =
|
||||
[
|
||||
new SbomVulnerabilityAssessment
|
||||
{
|
||||
Type = SbomVulnerabilityAssessmentType.CvssV3,
|
||||
TargetRef = "app",
|
||||
Score = 9.1,
|
||||
Vector = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
Comment = "critical"
|
||||
},
|
||||
new SbomVulnerabilityAssessment
|
||||
{
|
||||
Type = SbomVulnerabilityAssessmentType.Epss,
|
||||
TargetRef = "app",
|
||||
Score = 0.42,
|
||||
Comment = "epss"
|
||||
},
|
||||
new SbomVulnerabilityAssessment
|
||||
{
|
||||
Type = SbomVulnerabilityAssessmentType.VexAffected,
|
||||
TargetRef = "app",
|
||||
Comment = "affected"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var document = new SbomDocument
|
||||
{
|
||||
Name = "security-doc",
|
||||
Version = "1.0.0",
|
||||
Timestamp = new DateTimeOffset(2026, 1, 1, 8, 0, 0, TimeSpan.Zero),
|
||||
Components = [component],
|
||||
Vulnerabilities = [vulnerability]
|
||||
};
|
||||
|
||||
var result = _writer.Write(document);
|
||||
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
var graph = json.RootElement.GetProperty("@graph");
|
||||
|
||||
var docElement = graph.EnumerateArray()
|
||||
.First(element => element.GetProperty("@type").GetString() == "SpdxDocument");
|
||||
var profiles = docElement.GetProperty("creationInfo").GetProperty("profile")
|
||||
.EnumerateArray()
|
||||
.Select(value => value.GetString())
|
||||
.ToArray();
|
||||
Assert.Contains(SecurityProfileUri, profiles);
|
||||
|
||||
var vulnElement = graph.EnumerateArray()
|
||||
.First(element => element.GetProperty("@type").GetString() == "security_Vulnerability");
|
||||
Assert.Equal("CVE-2026-0001", vulnElement.GetProperty("name").GetString());
|
||||
|
||||
var vulnIdentifier = vulnElement.GetProperty("externalIdentifier")[0];
|
||||
Assert.Equal("Cve", vulnIdentifier.GetProperty("externalIdentifierType").GetString());
|
||||
Assert.Equal("CVE-2026-0001", vulnIdentifier.GetProperty("identifier").GetString());
|
||||
|
||||
var affectsRelationship = graph.EnumerateArray()
|
||||
.First(element => element.GetProperty("@type").GetString() == "Relationship" &&
|
||||
element.GetProperty("relationshipType").GetString() == "Affects");
|
||||
Assert.Equal(BuildElementId("vuln:CVE-2026-0001"), affectsRelationship.GetProperty("from").GetString());
|
||||
Assert.Equal(BuildElementId("app"), affectsRelationship.GetProperty("to")[0].GetString());
|
||||
|
||||
var cvssAssessment = graph.EnumerateArray()
|
||||
.First(element => element.GetProperty("@type").GetString() == "security_CvssV3VulnAssessmentRelationship");
|
||||
var cvssScore = cvssAssessment.GetProperty("security_score").GetDouble();
|
||||
Assert.InRange(cvssScore, 9.09, 9.11);
|
||||
Assert.Equal("Critical", cvssAssessment.GetProperty("security_severity").GetString());
|
||||
|
||||
var epssAssessment = graph.EnumerateArray()
|
||||
.First(element => element.GetProperty("@type").GetString() == "security_EpssVulnAssessmentRelationship");
|
||||
var epssScore = epssAssessment.GetProperty("security_probability").GetDouble();
|
||||
Assert.InRange(epssScore, 0.419, 0.421);
|
||||
|
||||
var vexAssessment = graph.EnumerateArray()
|
||||
.First(element => element.GetProperty("@type").GetString() == "security_VexAffectedVulnAssessmentRelationship");
|
||||
Assert.Equal("affected", vexAssessment.GetProperty("security_statusNotes").GetString());
|
||||
}
|
||||
|
||||
private static string BuildElementId(string reference)
|
||||
{
|
||||
return "urn:stellaops:sbom:element:" + Uri.EscapeDataString(reference);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.StandardPredicates.Models;
|
||||
using StellaOps.Attestor.StandardPredicates.Writers;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests;
|
||||
|
||||
public sealed class SpdxWriterSoftwareProfileTests
|
||||
{
|
||||
private readonly SpdxWriter _writer = new();
|
||||
|
||||
[Fact]
|
||||
public void PackageFields_AreSerialized()
|
||||
{
|
||||
var component = new SbomComponent
|
||||
{
|
||||
BomRef = "pkg",
|
||||
Name = "pkg",
|
||||
Version = "1.2.3",
|
||||
Purl = "pkg:npm/pkg@1.2.3",
|
||||
DownloadLocation = "https://example.com/pkg.tgz",
|
||||
HomePage = "https://example.com/pkg",
|
||||
SourceInfo = "git+https://example.com/pkg.git",
|
||||
PrimaryPurpose = "library",
|
||||
AdditionalPurposes = ["service", "documentation", "service"],
|
||||
ContentIdentifier = "sha256:abc123",
|
||||
CopyrightText = "(c) Example",
|
||||
AttributionText = ["Line B", "Line A"],
|
||||
OriginatedBy = "urn:stellaops:agent:person:dev",
|
||||
SuppliedBy = "urn:stellaops:agent:org:example",
|
||||
BuiltTime = new DateTimeOffset(2026, 1, 1, 10, 0, 0, TimeSpan.Zero),
|
||||
ReleaseTime = new DateTimeOffset(2026, 1, 2, 9, 0, 0, TimeSpan.Zero),
|
||||
ValidUntilTime = new DateTimeOffset(2027, 1, 1, 0, 0, 0, TimeSpan.Zero)
|
||||
};
|
||||
|
||||
var document = new SbomDocument
|
||||
{
|
||||
Name = "pkg-doc",
|
||||
Version = "1.0.0",
|
||||
Timestamp = new DateTimeOffset(2026, 1, 1, 8, 0, 0, TimeSpan.Zero),
|
||||
Components = [component]
|
||||
};
|
||||
|
||||
var result = _writer.Write(document);
|
||||
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
var graph = json.RootElement.GetProperty("@graph");
|
||||
var package = graph.EnumerateArray()
|
||||
.First(element => element.GetProperty("@type").GetString() == "software_Package" &&
|
||||
element.GetProperty("name").GetString() == "pkg");
|
||||
|
||||
Assert.Equal("https://example.com/pkg.tgz", package.GetProperty("downloadLocation").GetString());
|
||||
Assert.Equal("https://example.com/pkg", package.GetProperty("homePage").GetString());
|
||||
Assert.Equal("git+https://example.com/pkg.git", package.GetProperty("sourceInfo").GetString());
|
||||
Assert.Equal("library", package.GetProperty("primaryPurpose").GetString());
|
||||
Assert.Equal("sha256:abc123", package.GetProperty("contentIdentifier").GetString());
|
||||
Assert.Equal("2026-01-01T10:00:00Z", package.GetProperty("builtTime").GetString());
|
||||
|
||||
var additionalPurpose = package.GetProperty("additionalPurpose")
|
||||
.EnumerateArray()
|
||||
.Select(value => value.GetString())
|
||||
.ToArray();
|
||||
Assert.Equal(new[] { "documentation", "service" }, additionalPurpose);
|
||||
|
||||
var attribution = package.GetProperty("attributionText")
|
||||
.EnumerateArray()
|
||||
.Select(value => value.GetString())
|
||||
.ToArray();
|
||||
Assert.Equal(new[] { "Line A", "Line B" }, attribution);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileAndSnippetElements_AreSerialized()
|
||||
{
|
||||
var fileComponent = new SbomComponent
|
||||
{
|
||||
BomRef = "file1",
|
||||
Name = "app.cs",
|
||||
Type = SbomComponentType.File,
|
||||
FileName = "app.cs",
|
||||
FileKind = "text",
|
||||
ContentType = "text/plain"
|
||||
};
|
||||
|
||||
var snippet = new SbomSnippet
|
||||
{
|
||||
BomRef = "snippet1",
|
||||
FromFileRef = "file1",
|
||||
Name = "snippet",
|
||||
Description = "example snippet",
|
||||
ByteRange = new SbomRange { Start = 10, End = 20 },
|
||||
LineRange = new SbomRange { Start = 1, End = 2 }
|
||||
};
|
||||
|
||||
var document = new SbomDocument
|
||||
{
|
||||
Name = "snippet-doc",
|
||||
Version = "1.0.0",
|
||||
Timestamp = new DateTimeOffset(2026, 1, 1, 8, 0, 0, TimeSpan.Zero),
|
||||
Components = [fileComponent],
|
||||
Snippets = [snippet]
|
||||
};
|
||||
|
||||
var result = _writer.Write(document);
|
||||
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
var graph = json.RootElement.GetProperty("@graph");
|
||||
|
||||
var file = graph.EnumerateArray()
|
||||
.First(element => element.GetProperty("@type").GetString() == "software_File");
|
||||
Assert.Equal("app.cs", file.GetProperty("fileName").GetString());
|
||||
Assert.Equal("text", file.GetProperty("fileKind").GetString());
|
||||
Assert.Equal("text/plain", file.GetProperty("contentType").GetString());
|
||||
|
||||
var snippetElement = graph.EnumerateArray()
|
||||
.First(element => element.GetProperty("@type").GetString() == "software_Snippet");
|
||||
Assert.Equal(BuildElementId("file1"), snippetElement.GetProperty("snippetFromFile").GetString());
|
||||
Assert.Equal(10, snippetElement.GetProperty("byteRange").GetProperty("start").GetInt32());
|
||||
Assert.Equal(20, snippetElement.GetProperty("byteRange").GetProperty("end").GetInt32());
|
||||
Assert.Equal(1, snippetElement.GetProperty("lineRange").GetProperty("start").GetInt32());
|
||||
Assert.Equal(2, snippetElement.GetProperty("lineRange").GetProperty("end").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildProfileElements_AreSerialized()
|
||||
{
|
||||
var component = new SbomComponent
|
||||
{
|
||||
BomRef = "app",
|
||||
Name = "app",
|
||||
Version = "2.0.0"
|
||||
};
|
||||
|
||||
var build = new SbomBuild
|
||||
{
|
||||
BomRef = "build-123",
|
||||
BuildId = "build-123",
|
||||
BuildType = "ci",
|
||||
BuildStartTime = new DateTimeOffset(2026, 1, 2, 12, 0, 0, TimeSpan.Zero),
|
||||
BuildEndTime = new DateTimeOffset(2026, 1, 2, 12, 30, 0, TimeSpan.Zero),
|
||||
ConfigSourceEntrypoint = "Dockerfile",
|
||||
ConfigSourceDigest = "sha256:deadbeef",
|
||||
ConfigSourceUri = "https://example.com/build/Dockerfile",
|
||||
Environment = ImmutableDictionary.CreateRange(new Dictionary<string, string>
|
||||
{
|
||||
["CI"] = "true",
|
||||
["OS"] = "linux"
|
||||
}),
|
||||
Parameters = ImmutableDictionary.CreateRange(new Dictionary<string, string>
|
||||
{
|
||||
["configuration"] = "Release"
|
||||
}),
|
||||
ProducedRefs = ["app"]
|
||||
};
|
||||
|
||||
var document = new SbomDocument
|
||||
{
|
||||
Name = "build-doc",
|
||||
Version = "1.0.0",
|
||||
Timestamp = new DateTimeOffset(2026, 1, 2, 8, 0, 0, TimeSpan.Zero),
|
||||
Components = [component],
|
||||
Builds = [build]
|
||||
};
|
||||
|
||||
var result = _writer.Write(document);
|
||||
|
||||
using var json = JsonDocument.Parse(result.CanonicalBytes);
|
||||
var graph = json.RootElement.GetProperty("@graph");
|
||||
|
||||
var buildElement = graph.EnumerateArray()
|
||||
.First(element => element.GetProperty("@type").GetString() == "build_Build");
|
||||
Assert.Equal("build-123", buildElement.GetProperty("buildId").GetString());
|
||||
Assert.Equal("ci", buildElement.GetProperty("buildType").GetString());
|
||||
Assert.Equal("2026-01-02T12:00:00Z", buildElement.GetProperty("buildStartTime").GetString());
|
||||
Assert.Equal("Dockerfile", buildElement.GetProperty("configSourceEntrypoint").GetString());
|
||||
Assert.Equal("true", buildElement.GetProperty("environment").GetProperty("CI").GetString());
|
||||
|
||||
var relationship = graph.EnumerateArray()
|
||||
.First(element => element.GetProperty("@type").GetString() == "Relationship" &&
|
||||
element.GetProperty("relationshipType").GetString() == "OutputOf");
|
||||
Assert.Equal(BuildElementId("build:build-123"), relationship.GetProperty("from").GetString());
|
||||
Assert.Equal(BuildElementId("app"), relationship.GetProperty("to")[0].GetString());
|
||||
}
|
||||
|
||||
private static string BuildElementId(string reference)
|
||||
{
|
||||
return "urn:stellaops:sbom:element:" + Uri.EscapeDataString(reference);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# Attestor StandardPredicates Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260113_001_002_ATTESTOR_binary_diff_predicate.md`.
|
||||
Source of truth: `docs/implplan/SPRINT_20260119_013_Attestor_cyclonedx_1.7_generation.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
@@ -9,3 +9,6 @@ Source of truth: `docs/implplan/SPRINT_20260113_001_002_ATTESTOR_binary_diff_pre
|
||||
| AUDIT-0065-T | DONE | Revalidated 2026-01-06. |
|
||||
| AUDIT-0065-A | DONE | Waived after revalidation 2026-01-06. |
|
||||
| BINARYDIFF-TESTS-0001 | DONE | Add unit tests for BinaryDiff predicate, serializer, signer, and verifier. |
|
||||
| ATT-004 | DONE | Timestamp extension roundtrip tests for CycloneDX/SPDX predicates. |
|
||||
| TASK-013-009 | DONE | Added CycloneDX 1.7 feature, determinism, and round-trip tests. |
|
||||
| TASK-013-010 | DONE | Added CycloneDX 1.7 schema validation test. |
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.StandardPredicates;
|
||||
using StellaOps.Attestor.StandardPredicates.Writers;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests;
|
||||
|
||||
public sealed class TimestampExtensionTests
|
||||
{
|
||||
[Fact]
|
||||
public void CycloneDxTimestampExtension_RoundTripsMetadata()
|
||||
{
|
||||
var baseDoc = new
|
||||
{
|
||||
bomFormat = "CycloneDX",
|
||||
specVersion = "1.6",
|
||||
metadata = new { timestamp = "2026-01-19T12:00:00Z" }
|
||||
};
|
||||
var input = JsonSerializer.SerializeToUtf8Bytes(baseDoc);
|
||||
var metadata = CreateMetadata();
|
||||
|
||||
var updated = CycloneDxTimestampExtension.AddTimestampMetadata(input, metadata);
|
||||
var extracted = CycloneDxTimestampExtension.ExtractTimestampMetadata(updated);
|
||||
|
||||
extracted.Should().NotBeNull();
|
||||
extracted!.TsaUrl.Should().Be(metadata.TsaUrl);
|
||||
extracted.TokenDigest.Should().Be(metadata.TokenDigest);
|
||||
extracted.DigestAlgorithm.Should().Be(metadata.DigestAlgorithm);
|
||||
extracted.GenerationTime.Should().Be(metadata.GenerationTime);
|
||||
extracted.PolicyOid.Should().Be(metadata.PolicyOid);
|
||||
extracted.SerialNumber.Should().Be(metadata.SerialNumber);
|
||||
extracted.TsaName.Should().Be(metadata.TsaName);
|
||||
extracted.HasStapledRevocation.Should().BeTrue();
|
||||
extracted.IsQualified.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpdxTimestampExtension_RoundTripsMetadata()
|
||||
{
|
||||
var baseDoc = new
|
||||
{
|
||||
spdxVersion = "SPDX-3.0",
|
||||
dataLicense = "CC0-1.0",
|
||||
SPDXID = "SPDXRef-DOCUMENT"
|
||||
};
|
||||
var input = JsonSerializer.SerializeToUtf8Bytes(baseDoc);
|
||||
var metadata = CreateMetadata();
|
||||
|
||||
var updated = SpdxTimestampExtension.AddTimestampAnnotation(input, metadata);
|
||||
var extracted = SpdxTimestampExtension.ExtractTimestampMetadata(updated);
|
||||
|
||||
extracted.Should().NotBeNull();
|
||||
extracted!.TsaUrl.Should().Be(metadata.TsaUrl);
|
||||
extracted.TokenDigest.Should().Be(metadata.TokenDigest);
|
||||
extracted.DigestAlgorithm.Should().Be(metadata.DigestAlgorithm);
|
||||
extracted.GenerationTime.Should().Be(metadata.GenerationTime);
|
||||
extracted.PolicyOid.Should().Be(metadata.PolicyOid);
|
||||
extracted.TsaName.Should().Be(metadata.TsaName);
|
||||
extracted.HasStapledRevocation.Should().BeTrue();
|
||||
extracted.IsQualified.Should().BeTrue();
|
||||
}
|
||||
|
||||
private static Rfc3161TimestampMetadata CreateMetadata()
|
||||
{
|
||||
return new Rfc3161TimestampMetadata
|
||||
{
|
||||
TsaUrl = "https://tsa.example.test",
|
||||
TokenDigest = "abc123",
|
||||
DigestAlgorithm = "SHA256",
|
||||
GenerationTime = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero),
|
||||
PolicyOid = "1.2.3.4",
|
||||
SerialNumber = "01",
|
||||
TsaName = "Example TSA",
|
||||
HasStapledRevocation = true,
|
||||
IsQualified = true
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user