license switch agpl -> busl1, sprints work, new product advisories
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// <copyright file="DsseVerifierTests.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Security.Cryptography;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="DsseVerifier.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="IDsseVerifier.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"@types/node": "^22.7.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
},
|
||||
"license": "BUSL-1.1"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// StellaOps Attestor - Distributed Verification Provider (Resilient, Multi-Node)
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
#if STELLAOPS_EXPERIMENTAL_DISTRIBUTED_VERIFY
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.Core.InToto;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.Core.InToto;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.Core.Predicates;
|
||||
using StellaOps.Attestor.Core.Signing;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Attestor.Serialization;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Core.Tests.Signing;
|
||||
|
||||
public sealed class VerificationReportSignerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SignAsync_ProducesDsseEnvelopeWithVerifiableSignature()
|
||||
{
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var parameters = ecdsa.ExportParameters(true);
|
||||
var signingKey = EnvelopeKey.CreateEcdsaSigner(SignatureAlgorithms.Es256, parameters, "test-key");
|
||||
var verificationKey = EnvelopeKey.CreateEcdsaVerifier(
|
||||
SignatureAlgorithms.Es256,
|
||||
new ECParameters { Curve = parameters.Curve, Q = parameters.Q },
|
||||
"test-key");
|
||||
|
||||
var signedAt = new DateTimeOffset(2026, 1, 20, 12, 0, 0, TimeSpan.Zero);
|
||||
var certificatePem = "-----BEGIN CERTIFICATE-----\nTESTCERT\n-----END CERTIFICATE-----";
|
||||
|
||||
var report = BuildReport(new DateTimeOffset(2026, 1, 20, 11, 59, 0, TimeSpan.Zero));
|
||||
var signer = new DsseVerificationReportSigner(new EnvelopeSignatureService());
|
||||
var result = await signer.SignAsync(new VerificationReportSigningRequest(
|
||||
report,
|
||||
signingKey,
|
||||
VerifierCertificatePem: certificatePem,
|
||||
SignedAt: signedAt));
|
||||
|
||||
Assert.Equal(VerificationReportPredicate.PredicateType, result.PayloadType);
|
||||
Assert.NotNull(result.Report.Verifier);
|
||||
Assert.Equal(signingKey.AlgorithmId, result.Report.Verifier!.Algo);
|
||||
Assert.Equal(certificatePem, result.Report.Verifier.Cert);
|
||||
Assert.Equal(signedAt, result.Report.Verifier.SignedAt);
|
||||
|
||||
using var document = JsonDocument.Parse(result.EnvelopeJson);
|
||||
var payloadBase64 = document.RootElement.GetProperty("payload").GetString();
|
||||
Assert.False(string.IsNullOrWhiteSpace(payloadBase64));
|
||||
|
||||
var payloadBytes = Convert.FromBase64String(payloadBase64!);
|
||||
var expectedPayload = CanonicalJsonSerializer.SerializeToBytes(result.Report, prettify: false);
|
||||
Assert.Equal(expectedPayload, payloadBytes);
|
||||
|
||||
var signatureElement = document.RootElement.GetProperty("signatures")[0];
|
||||
var signatureBase64 = signatureElement.GetProperty("sig").GetString();
|
||||
Assert.False(string.IsNullOrWhiteSpace(signatureBase64));
|
||||
var signatureBytes = Convert.FromBase64String(signatureBase64!);
|
||||
|
||||
var envelopeSignature = new EnvelopeSignature(
|
||||
signatureElement.GetProperty("keyid").GetString() ?? "test-key",
|
||||
signingKey.AlgorithmId,
|
||||
signatureBytes);
|
||||
|
||||
var verifier = new EnvelopeSignatureService();
|
||||
var verifyResult = verifier.VerifyDsse(
|
||||
VerificationReportPredicate.PredicateType,
|
||||
payloadBytes,
|
||||
envelopeSignature,
|
||||
verificationKey);
|
||||
|
||||
Assert.True(verifyResult.IsSuccess);
|
||||
Assert.True(verifyResult.Value);
|
||||
}
|
||||
|
||||
private static VerificationReportPredicate BuildReport(DateTimeOffset generatedAt)
|
||||
{
|
||||
return new VerificationReportPredicate
|
||||
{
|
||||
ReportId = "report-001",
|
||||
GeneratedAt = generatedAt,
|
||||
Generator = new GeneratorInfo
|
||||
{
|
||||
Tool = "stella bundle verify",
|
||||
Version = "test"
|
||||
},
|
||||
Subject = new VerificationSubject
|
||||
{
|
||||
BundleId = "bundle-001",
|
||||
BundleDigest = "sha256:bundle",
|
||||
ArtifactDigest = "sha256:artifact",
|
||||
ArtifactName = "registry.example.com/app@sha256:deadbeef"
|
||||
},
|
||||
VerificationSteps =
|
||||
[
|
||||
new VerificationStep
|
||||
{
|
||||
Step = 1,
|
||||
Name = "manifest",
|
||||
Status = VerificationStepStatus.Passed,
|
||||
DurationMs = 1,
|
||||
Details = "manifest parsed",
|
||||
Issues = Array.Empty<VerificationIssue>()
|
||||
}
|
||||
],
|
||||
OverallResult = new OverallVerificationResult
|
||||
{
|
||||
Status = VerificationStepStatus.Passed,
|
||||
Summary = "PASSED",
|
||||
TotalDurationMs = 1,
|
||||
PassedSteps = 1,
|
||||
FailedSteps = 0,
|
||||
WarningSteps = 0,
|
||||
SkippedSteps = 0
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
|
||||
|
||||
// Models are now in the same namespace
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
namespace StellaOps.Attestor.Core.InToto;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
namespace StellaOps.Attestor.Core.InToto;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
namespace StellaOps.Attestor.Core.InToto;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
namespace StellaOps.Attestor.Core.InToto;
|
||||
|
||||
|
||||
@@ -123,9 +123,9 @@ public sealed class AttestorOptions
|
||||
public int MaxAttempts { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Log version to use: Auto, V1, or V2.
|
||||
/// Log version to use: Auto or V2.
|
||||
/// V2 uses tile-based (Sunlight) log structure.
|
||||
/// Default: Auto (backward compatible).
|
||||
/// Default: Auto (v2 tiles).
|
||||
/// </summary>
|
||||
public string Version { get; set; } = "Auto";
|
||||
|
||||
@@ -141,10 +141,6 @@ public sealed class AttestorOptions
|
||||
/// </summary>
|
||||
public string? LogId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true and Version is Auto, prefer tile-based proofs over v1 proofs.
|
||||
/// </summary>
|
||||
public bool PreferTileProofs { get; set; } = false;
|
||||
}
|
||||
|
||||
public sealed class RekorMirrorOptions : RekorBackendOptions
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="PathWitnessPredicateTypes.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Sprint: SPRINT_20260112_006_ATTESTOR_path_witness_predicate (PW-ATT-003)
|
||||
// </copyright>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
||||
@@ -8,15 +8,10 @@ namespace StellaOps.Attestor.Core.Rekor;
|
||||
public enum RekorLogVersion
|
||||
{
|
||||
/// <summary>
|
||||
/// Automatically detect log version from server capabilities.
|
||||
/// Automatically select the supported log format (defaults to v2 tiles).
|
||||
/// </summary>
|
||||
Auto = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Rekor v1 with Trillian-backed Merkle tree.
|
||||
/// </summary>
|
||||
V1 = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Rekor v2 with tile-based (Sunlight) log structure.
|
||||
/// Provides cheaper operation and simpler verification.
|
||||
@@ -31,7 +26,7 @@ public sealed class RekorBackend
|
||||
public required Uri Url { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Log version to use. Default is Auto for backward compatibility.
|
||||
/// Log version to use. Default is Auto (v2 tiles).
|
||||
/// Set to V2 to explicitly opt into tile-based verification.
|
||||
/// </summary>
|
||||
public RekorLogVersion Version { get; init; } = RekorLogVersion.Auto;
|
||||
@@ -50,12 +45,6 @@ public sealed class RekorBackend
|
||||
/// </summary>
|
||||
public string? LogId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to prefer tile-based proofs when available.
|
||||
/// When true and Version is Auto, will attempt tile fetching first.
|
||||
/// </summary>
|
||||
public bool PreferTileProofs { get; init; } = false;
|
||||
|
||||
public TimeSpan ProofTimeout { get; init; } = TimeSpan.FromSeconds(15);
|
||||
|
||||
public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds(250);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="RekorEntryEvent.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-001, ATT-REKOR-002)
|
||||
// </copyright>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
|
||||
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
|
||||
using System.Text;
|
||||
using StellaOps.Attestor.Core.Predicates;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Attestor.Serialization;
|
||||
|
||||
namespace StellaOps.Attestor.Core.Signing;
|
||||
|
||||
public sealed class DsseVerificationReportSigner : IVerificationReportSigner
|
||||
{
|
||||
private readonly EnvelopeSignatureService _signatureService;
|
||||
|
||||
public DsseVerificationReportSigner(EnvelopeSignatureService signatureService)
|
||||
{
|
||||
_signatureService = signatureService ?? throw new ArgumentNullException(nameof(signatureService));
|
||||
}
|
||||
|
||||
public Task<VerificationReportSigningResult> SignAsync(
|
||||
VerificationReportSigningRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(request.Report);
|
||||
ArgumentNullException.ThrowIfNull(request.SigningKey);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var signedAt = request.SignedAt ?? DateTimeOffset.UtcNow;
|
||||
var report = request.Report with
|
||||
{
|
||||
Verifier = new VerifierInfo
|
||||
{
|
||||
Algo = request.SigningKey.AlgorithmId,
|
||||
Cert = NormalizePem(request.VerifierCertificatePem),
|
||||
SignedAt = signedAt
|
||||
}
|
||||
};
|
||||
|
||||
var payloadBytes = CanonicalJsonSerializer.SerializeToBytes(report, prettify: false);
|
||||
var signResult = _signatureService.SignDsse(
|
||||
VerificationReportPredicate.PredicateType,
|
||||
payloadBytes,
|
||||
request.SigningKey,
|
||||
cancellationToken);
|
||||
|
||||
if (!signResult.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Verification report DSSE signing failed: {signResult.Error.Message}");
|
||||
}
|
||||
|
||||
var signature = DsseSignature.FromBytes(signResult.Value.Value.Span, signResult.Value.KeyId);
|
||||
var envelope = new DsseEnvelope(
|
||||
VerificationReportPredicate.PredicateType,
|
||||
payloadBytes,
|
||||
new[] { signature },
|
||||
payloadContentType: "application/json");
|
||||
|
||||
var serialization = DsseEnvelopeSerializer.Serialize(envelope);
|
||||
var envelopeJson = serialization.CompactJson is null
|
||||
? string.Empty
|
||||
: Encoding.UTF8.GetString(serialization.CompactJson);
|
||||
|
||||
return Task.FromResult(new VerificationReportSigningResult
|
||||
{
|
||||
PayloadType = envelope.PayloadType,
|
||||
Payload = payloadBytes,
|
||||
Signatures = envelope.Signatures,
|
||||
EnvelopeJson = envelopeJson,
|
||||
Report = report
|
||||
});
|
||||
}
|
||||
|
||||
private static string? NormalizePem(string? pem)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pem))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return pem.Replace("\r\n", "\n").Trim();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
|
||||
using StellaOps.Attestor.Core.Predicates;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
|
||||
namespace StellaOps.Attestor.Core.Signing;
|
||||
|
||||
public interface IVerificationReportSigner
|
||||
{
|
||||
Task<VerificationReportSigningResult> SignAsync(
|
||||
VerificationReportSigningRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed record VerificationReportSigningRequest(
|
||||
VerificationReportPredicate Report,
|
||||
EnvelopeKey SigningKey,
|
||||
string? VerifierCertificatePem = null,
|
||||
DateTimeOffset? SignedAt = null);
|
||||
|
||||
public sealed record VerificationReportSigningResult
|
||||
{
|
||||
public required string PayloadType { get; init; }
|
||||
public required byte[] Payload { get; init; }
|
||||
public required IReadOnlyList<DsseSignature> Signatures { get; init; }
|
||||
public required string EnvelopeJson { get; init; }
|
||||
public required VerificationReportPredicate Report { get; init; }
|
||||
}
|
||||
@@ -18,6 +18,10 @@
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Schemas\*.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\__Libraries\StellaOps.Attestor.Core\Predicates\VerificationReportPredicate.cs"
|
||||
Link="Predicates\VerificationReportPredicate.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Kms\StellaOps.Cryptography.Kms.csproj" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -52,7 +52,6 @@ internal static class RekorBackendResolver
|
||||
? null
|
||||
: new Uri(options.TileBaseUrl, UriKind.Absolute),
|
||||
LogId = options.LogId,
|
||||
PreferTileProofs = options.PreferTileProofs,
|
||||
ProofTimeout = TimeSpan.FromMilliseconds(options.ProofTimeoutMs),
|
||||
PollInterval = TimeSpan.FromMilliseconds(options.PollIntervalMs),
|
||||
MaxAttempts = options.MaxAttempts
|
||||
@@ -72,9 +71,11 @@ internal static class RekorBackendResolver
|
||||
return version.Trim().ToUpperInvariant() switch
|
||||
{
|
||||
"AUTO" => RekorLogVersion.Auto,
|
||||
"V1" or "1" => RekorLogVersion.V1,
|
||||
"V2" or "2" => RekorLogVersion.V2,
|
||||
_ => RekorLogVersion.Auto
|
||||
"V1" or "1" => throw new InvalidOperationException(
|
||||
"Rekor v1 is no longer supported. Use Auto or V2."),
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Unsupported Rekor version '{version}'. Use Auto or V2.")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,6 +85,6 @@ internal static class RekorBackendResolver
|
||||
public static bool ShouldUseTileProofs(RekorBackend backend)
|
||||
{
|
||||
return backend.Version == RekorLogVersion.V2 ||
|
||||
(backend.Version == RekorLogVersion.Auto && backend.PreferTileProofs);
|
||||
backend.Version == RekorLogVersion.Auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Attestor.Verify\StellaOps.Attestor.Verify.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.Timestamping\StellaOps.Attestor.Timestamping.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Plugin.SmSoft\StellaOps.Cryptography.Plugin.SmSoft.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -8,3 +8,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0066-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
|
||||
| AUDIT-0066-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
|
||||
| AUDIT-0066-A | DONE | Waived (test project; revalidated 2026-01-06). |
|
||||
| ATT-001 | DONE | Timestamping service unit tests for envelope digest handling. |
|
||||
| ATT-002 | DONE | Timestamp verification scenarios covered. |
|
||||
| ATT-003 | DONE | Timestamp policy evaluator scenarios covered. |
|
||||
| ATT-006 | DONE | Time correlation validator unit tests added. |
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.Timestamping;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests.Timestamping;
|
||||
|
||||
public sealed class AttestationTimestampPolicyTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Evaluate_WhenRfc3161RequiredAndMissing_Fails()
|
||||
{
|
||||
var evaluator = new TimestampPolicyEvaluator();
|
||||
var context = new AttestationTimestampPolicyContext { HasValidTst = false };
|
||||
var policy = new TimestampPolicy { RequireRfc3161 = true };
|
||||
|
||||
var result = evaluator.Evaluate(context, policy);
|
||||
|
||||
result.IsCompliant.Should().BeFalse();
|
||||
result.Violations.Should().Contain(v => v.RuleId == "require-rfc3161");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Evaluate_WhenSkewExceedsThreshold_Fails()
|
||||
{
|
||||
var evaluator = new TimestampPolicyEvaluator();
|
||||
var context = new AttestationTimestampPolicyContext
|
||||
{
|
||||
HasValidTst = true,
|
||||
TimeSkew = TimeSpan.FromMinutes(10)
|
||||
};
|
||||
var policy = new TimestampPolicy
|
||||
{
|
||||
RequireRfc3161 = true,
|
||||
MaxTimeSkew = TimeSpan.FromMinutes(5)
|
||||
};
|
||||
|
||||
var result = evaluator.Evaluate(context, policy);
|
||||
|
||||
result.IsCompliant.Should().BeFalse();
|
||||
result.Violations.Should().Contain(v => v.RuleId == "time-skew");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Evaluate_WhenRevocationStapleMissing_Fails()
|
||||
{
|
||||
var evaluator = new TimestampPolicyEvaluator();
|
||||
var context = new AttestationTimestampPolicyContext
|
||||
{
|
||||
HasValidTst = true,
|
||||
OcspStatus = null,
|
||||
CrlChecked = false
|
||||
};
|
||||
var policy = new TimestampPolicy
|
||||
{
|
||||
RequireRfc3161 = true,
|
||||
RequireRevocationStapling = true
|
||||
};
|
||||
|
||||
var result = evaluator.Evaluate(context, policy);
|
||||
|
||||
result.IsCompliant.Should().BeFalse();
|
||||
result.Violations.Should().Contain(v => v.RuleId == "revocation-staple");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Evaluate_WhenTsaNotTrusted_Fails()
|
||||
{
|
||||
var evaluator = new TimestampPolicyEvaluator();
|
||||
var context = new AttestationTimestampPolicyContext
|
||||
{
|
||||
HasValidTst = true,
|
||||
TsaName = "Untrusted TSA",
|
||||
OcspStatus = "Good",
|
||||
CrlChecked = true
|
||||
};
|
||||
var policy = new TimestampPolicy
|
||||
{
|
||||
RequireRfc3161 = true,
|
||||
RequireRevocationStapling = true,
|
||||
TrustedTsas = new[] { "Trusted TSA" }
|
||||
};
|
||||
|
||||
var result = evaluator.Evaluate(context, policy);
|
||||
|
||||
result.IsCompliant.Should().BeFalse();
|
||||
result.Violations.Should().Contain(v => v.RuleId == "trusted-tsa");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Evaluate_WhenAllRequirementsMet_Passes()
|
||||
{
|
||||
var evaluator = new TimestampPolicyEvaluator();
|
||||
var context = new AttestationTimestampPolicyContext
|
||||
{
|
||||
HasValidTst = true,
|
||||
TimeSkew = TimeSpan.FromMinutes(1),
|
||||
TsaName = "Trusted TSA",
|
||||
OcspStatus = "Good",
|
||||
CrlChecked = true,
|
||||
TsaCertificateExpires = DateTimeOffset.UtcNow.AddDays(365)
|
||||
};
|
||||
var policy = new TimestampPolicy
|
||||
{
|
||||
RequireRfc3161 = true,
|
||||
RequireRevocationStapling = true,
|
||||
MaxTimeSkew = TimeSpan.FromMinutes(5),
|
||||
MinCertificateFreshness = TimeSpan.FromDays(180),
|
||||
TrustedTsas = new[] { "Trusted TSA" }
|
||||
};
|
||||
|
||||
var result = evaluator.Evaluate(context, policy);
|
||||
|
||||
result.IsCompliant.Should().BeTrue();
|
||||
result.Violations.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor.Timestamping;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests.Timestamping;
|
||||
|
||||
public sealed class AttestationTimestampServiceTests
|
||||
{
|
||||
private static AttestationTimestampService CreateService()
|
||||
{
|
||||
var options = Options.Create(new AttestationTimestampServiceOptions());
|
||||
return new AttestationTimestampService(options, NullLogger<AttestationTimestampService>.Instance);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TimestampAsync_ComputesEnvelopeDigest()
|
||||
{
|
||||
var service = CreateService();
|
||||
var envelope = Encoding.UTF8.GetBytes("{\"payload\":\"test\"}");
|
||||
|
||||
var result = await service.TimestampAsync(
|
||||
envelope,
|
||||
new AttestationTimestampOptions { HashAlgorithm = "SHA256" });
|
||||
|
||||
var expectedHash = Convert.ToHexString(SHA256.HashData(envelope)).ToLowerInvariant();
|
||||
result.EnvelopeDigest.Should().Be($"sha256:{expectedHash}");
|
||||
result.Envelope.Should().Equal(envelope);
|
||||
result.TsaName.Should().NotBeNullOrWhiteSpace();
|
||||
result.TsaPolicyOid.Should().NotBeNullOrWhiteSpace();
|
||||
result.TimestampTime.Should().NotBe(default);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithConsistentRekorTime_ReturnsSuccess()
|
||||
{
|
||||
var service = CreateService();
|
||||
var envelope = Encoding.UTF8.GetBytes("{\"payload\":\"test\"}");
|
||||
var digest = "sha256:" + Convert.ToHexString(SHA256.HashData(envelope)).ToLowerInvariant();
|
||||
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
var attestation = new TimestampedAttestation
|
||||
{
|
||||
Envelope = envelope,
|
||||
EnvelopeDigest = digest,
|
||||
TimeStampToken = Array.Empty<byte>(),
|
||||
TimestampTime = tstTime,
|
||||
TsaName = "Test TSA",
|
||||
TsaPolicyOid = "1.2.3.4",
|
||||
RekorReceipt = new RekorReceipt
|
||||
{
|
||||
LogId = "rekor",
|
||||
LogIndex = 42,
|
||||
IntegratedTime = tstTime.AddMinutes(1)
|
||||
}
|
||||
};
|
||||
|
||||
var options = new AttestationTimestampVerificationOptions
|
||||
{
|
||||
RequireRekorConsistency = true,
|
||||
MaxTimeSkew = TimeSpan.FromMinutes(5),
|
||||
VerifyTsaRevocation = false
|
||||
};
|
||||
|
||||
var result = await service.VerifyAsync(attestation, options);
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.TimeConsistency.Should().NotBeNull();
|
||||
result.TimeConsistency!.WithinTolerance.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithRekorInconsistency_ReturnsFailure()
|
||||
{
|
||||
var service = CreateService();
|
||||
var envelope = Encoding.UTF8.GetBytes("{\"payload\":\"test\"}");
|
||||
var digest = "sha256:" + Convert.ToHexString(SHA256.HashData(envelope)).ToLowerInvariant();
|
||||
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
var attestation = new TimestampedAttestation
|
||||
{
|
||||
Envelope = envelope,
|
||||
EnvelopeDigest = digest,
|
||||
TimeStampToken = Array.Empty<byte>(),
|
||||
TimestampTime = tstTime,
|
||||
TsaName = "Test TSA",
|
||||
TsaPolicyOid = "1.2.3.4",
|
||||
RekorReceipt = new RekorReceipt
|
||||
{
|
||||
LogId = "rekor",
|
||||
LogIndex = 42,
|
||||
IntegratedTime = tstTime.AddMinutes(-10)
|
||||
}
|
||||
};
|
||||
|
||||
var options = new AttestationTimestampVerificationOptions
|
||||
{
|
||||
RequireRekorConsistency = true,
|
||||
MaxTimeSkew = TimeSpan.FromMinutes(5),
|
||||
VerifyTsaRevocation = false
|
||||
};
|
||||
|
||||
var result = await service.VerifyAsync(attestation, options);
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.TstStatus.Should().Be(TstVerificationStatus.TimeInconsistency);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Attestor.Timestamping;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests.Timestamping;
|
||||
|
||||
public sealed class TimeCorrelationValidatorTests
|
||||
{
|
||||
private static TimeCorrelationValidator CreateValidator()
|
||||
=> new(NullLogger<TimeCorrelationValidator>.Instance);
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_WhenGapWithinLimits_ReturnsValid()
|
||||
{
|
||||
var validator = CreateValidator();
|
||||
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero);
|
||||
var rekorTime = tstTime.AddSeconds(30);
|
||||
|
||||
var result = validator.Validate(tstTime, rekorTime);
|
||||
|
||||
result.Valid.Should().BeTrue();
|
||||
result.Status.Should().Be(TimeCorrelationStatus.Valid);
|
||||
result.Suspicious.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_WhenGapExceedsMaximum_ReturnsGapExceeded()
|
||||
{
|
||||
var validator = CreateValidator();
|
||||
var policy = new TimeCorrelationPolicy
|
||||
{
|
||||
MaximumGap = TimeSpan.FromMinutes(2),
|
||||
SuspiciousGap = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero);
|
||||
var rekorTime = tstTime.AddMinutes(5);
|
||||
|
||||
var result = validator.Validate(tstTime, rekorTime, policy);
|
||||
|
||||
result.Valid.Should().BeFalse();
|
||||
result.Status.Should().Be(TimeCorrelationStatus.GapExceeded);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_WhenTstAfterRekor_ReturnsInvalid()
|
||||
{
|
||||
var validator = CreateValidator();
|
||||
var policy = new TimeCorrelationPolicy
|
||||
{
|
||||
ClockSkewTolerance = TimeSpan.FromSeconds(5)
|
||||
};
|
||||
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 10, TimeSpan.Zero);
|
||||
var rekorTime = tstTime.AddSeconds(-30);
|
||||
|
||||
var result = validator.Validate(tstTime, rekorTime, policy);
|
||||
|
||||
result.Valid.Should().BeFalse();
|
||||
result.Status.Should().Be(TimeCorrelationStatus.TstAfterRekor);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_WhenSuspiciousAndFailOnSuspicious_ReturnsFailure()
|
||||
{
|
||||
var validator = CreateValidator();
|
||||
var policy = new TimeCorrelationPolicy
|
||||
{
|
||||
MaximumGap = TimeSpan.FromMinutes(5),
|
||||
SuspiciousGap = TimeSpan.FromSeconds(10),
|
||||
FailOnSuspicious = true
|
||||
};
|
||||
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero);
|
||||
var rekorTime = tstTime.AddSeconds(30);
|
||||
|
||||
var result = validator.Validate(tstTime, rekorTime, policy);
|
||||
|
||||
result.Valid.Should().BeFalse();
|
||||
result.Status.Should().Be(TimeCorrelationStatus.SuspiciousGapFailed);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Attestor.Core.InToto;
|
||||
|
||||
@@ -39,6 +39,12 @@ public sealed record VerificationReportPredicate
|
||||
[JsonPropertyName("generator")]
|
||||
public required GeneratorInfo Generator { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verifier metadata for the signed report.
|
||||
/// </summary>
|
||||
[JsonPropertyName("verifier")]
|
||||
public VerifierInfo? Verifier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject being verified.
|
||||
/// </summary>
|
||||
@@ -88,6 +94,25 @@ public sealed record GeneratorInfo
|
||||
public HostInfo? HostInfo { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifier metadata captured when a report is signed.
|
||||
/// </summary>
|
||||
public sealed record VerifierInfo
|
||||
{
|
||||
/// <summary>Signature algorithm identifier.</summary>
|
||||
[JsonPropertyName("algo")]
|
||||
public string? Algo { get; init; }
|
||||
|
||||
/// <summary>Signing certificate or chain (PEM).</summary>
|
||||
[JsonPropertyName("cert")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public string? Cert { get; init; }
|
||||
|
||||
/// <summary>When the report was signed (UTC).</summary>
|
||||
[JsonPropertyName("signed_at")]
|
||||
public DateTimeOffset? SignedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Host information.
|
||||
/// </summary>
|
||||
|
||||
@@ -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.Security.Cryptography;
|
||||
|
||||
@@ -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.Serialization;
|
||||
|
||||
@@ -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.Serialization;
|
||||
|
||||
@@ -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.Globalization;
|
||||
|
||||
@@ -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 Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="GraphType.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Attestor.GraphRoot;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="BuildAttestationMapper.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="BuildRelationshipBuilder.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="CombinedDocumentBuilder.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="DsseSpdx3Signer.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="IBuildAttestationMapper.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using StellaOps.Spdx3.Model.Build;
|
||||
|
||||
@@ -72,6 +72,25 @@ public sealed class BinaryDiffDsseVerifier : IBinaryDiffDsseVerifier
|
||||
return BinaryDiffVerificationResult.Failure("DSSE signature verification failed.");
|
||||
}
|
||||
|
||||
JsonDocument document;
|
||||
try
|
||||
{
|
||||
document = JsonDocument.Parse(envelope.Payload);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return BinaryDiffVerificationResult.Failure($"Failed to parse predicate JSON: {ex.Message}");
|
||||
}
|
||||
|
||||
using (document)
|
||||
{
|
||||
var schemaResult = BinaryDiffSchema.Validate(document.RootElement);
|
||||
if (!schemaResult.IsValid)
|
||||
{
|
||||
return BinaryDiffVerificationResult.Failure("Schema validation failed.", schemaResult.Errors);
|
||||
}
|
||||
}
|
||||
|
||||
BinaryDiffPredicate predicate;
|
||||
try
|
||||
{
|
||||
@@ -87,13 +106,6 @@ public sealed class BinaryDiffDsseVerifier : IBinaryDiffDsseVerifier
|
||||
return BinaryDiffVerificationResult.Failure("Predicate type does not match BinaryDiffV1.");
|
||||
}
|
||||
|
||||
using var document = JsonDocument.Parse(envelope.Payload);
|
||||
var schemaResult = BinaryDiffSchema.Validate(document.RootElement);
|
||||
if (!schemaResult.IsValid)
|
||||
{
|
||||
return BinaryDiffVerificationResult.Failure("Schema validation failed.", schemaResult.Errors);
|
||||
}
|
||||
|
||||
if (!HasDeterministicOrdering(predicate))
|
||||
{
|
||||
return BinaryDiffVerificationResult.Failure("Predicate ordering is not deterministic.");
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace StellaOps.Attestor.StandardPredicates.Licensing;
|
||||
|
||||
public abstract record SpdxLicenseExpression;
|
||||
|
||||
public sealed record SpdxSimpleLicense(string LicenseId) : SpdxLicenseExpression;
|
||||
|
||||
public sealed record SpdxConjunctiveLicense(
|
||||
SpdxLicenseExpression Left,
|
||||
SpdxLicenseExpression Right) : SpdxLicenseExpression;
|
||||
|
||||
public sealed record SpdxDisjunctiveLicense(
|
||||
SpdxLicenseExpression Left,
|
||||
SpdxLicenseExpression Right) : SpdxLicenseExpression;
|
||||
|
||||
public sealed record SpdxWithException(
|
||||
SpdxLicenseExpression License,
|
||||
string Exception) : SpdxLicenseExpression;
|
||||
|
||||
public sealed record SpdxNoneLicense : SpdxLicenseExpression
|
||||
{
|
||||
public static SpdxNoneLicense Instance { get; } = new();
|
||||
|
||||
private SpdxNoneLicense()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record SpdxNoAssertionLicense : SpdxLicenseExpression
|
||||
{
|
||||
public static SpdxNoAssertionLicense Instance { get; } = new();
|
||||
|
||||
private SpdxNoAssertionLicense()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.Licensing;
|
||||
|
||||
public enum SpdxLicenseListVersion
|
||||
{
|
||||
V3_21
|
||||
}
|
||||
|
||||
public sealed record SpdxLicenseList
|
||||
{
|
||||
public required string Version { get; init; }
|
||||
|
||||
public required ImmutableHashSet<string> LicenseIds { get; init; }
|
||||
|
||||
public required ImmutableHashSet<string> ExceptionIds { get; init; }
|
||||
}
|
||||
|
||||
public static class SpdxLicenseListProvider
|
||||
{
|
||||
private const string LicenseResource = "StellaOps.Attestor.StandardPredicates.Resources.spdx-license-list-3.21.json";
|
||||
private const string ExceptionResource = "StellaOps.Attestor.StandardPredicates.Resources.spdx-license-exceptions-3.21.json";
|
||||
|
||||
private static readonly Lazy<SpdxLicenseList> LicenseListV321 = new(LoadV321);
|
||||
|
||||
public static SpdxLicenseList Get(SpdxLicenseListVersion version)
|
||||
=> version switch
|
||||
{
|
||||
SpdxLicenseListVersion.V3_21 => LicenseListV321.Value,
|
||||
_ => LicenseListV321.Value,
|
||||
};
|
||||
|
||||
private static SpdxLicenseList LoadV321()
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var licenseIds = LoadLicenseIds(assembly, LicenseResource, "licenses", "licenseId");
|
||||
var exceptionIds = LoadLicenseIds(assembly, ExceptionResource, "exceptions", "licenseExceptionId");
|
||||
|
||||
return new SpdxLicenseList
|
||||
{
|
||||
Version = "3.21",
|
||||
LicenseIds = licenseIds,
|
||||
ExceptionIds = exceptionIds,
|
||||
};
|
||||
}
|
||||
|
||||
private static ImmutableHashSet<string> LoadLicenseIds(
|
||||
Assembly assembly,
|
||||
string resourceName,
|
||||
string arrayProperty,
|
||||
string idProperty)
|
||||
{
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||
?? throw new InvalidOperationException($"Missing embedded resource: {resourceName}");
|
||||
using var document = JsonDocument.Parse(stream);
|
||||
|
||||
if (!document.RootElement.TryGetProperty(arrayProperty, out var array) ||
|
||||
array.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return ImmutableHashSet<string>.Empty;
|
||||
}
|
||||
|
||||
var builder = ImmutableHashSet.CreateBuilder<string>(StringComparer.Ordinal);
|
||||
foreach (var entry in array.EnumerateArray())
|
||||
{
|
||||
if (entry.TryGetProperty(idProperty, out var idElement) &&
|
||||
idElement.ValueKind == JsonValueKind.String &&
|
||||
idElement.GetString() is { Length: > 0 } id)
|
||||
{
|
||||
builder.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SpdxLicenseExpressionParser
|
||||
{
|
||||
public static bool TryParse(string expression, out SpdxLicenseExpression? result, SpdxLicenseList? licenseList = null)
|
||||
{
|
||||
result = null;
|
||||
if (string.IsNullOrWhiteSpace(expression))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result = Parse(expression, licenseList);
|
||||
return true;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static SpdxLicenseExpression Parse(string expression, SpdxLicenseList? licenseList = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(expression))
|
||||
{
|
||||
throw new FormatException("License expression is empty.");
|
||||
}
|
||||
|
||||
var tokens = Tokenize(expression);
|
||||
var parser = new Parser(tokens);
|
||||
var parsed = parser.ParseExpression();
|
||||
|
||||
if (parser.HasMoreTokens)
|
||||
{
|
||||
throw new FormatException("Unexpected trailing tokens in license expression.");
|
||||
}
|
||||
|
||||
if (licenseList is not null)
|
||||
{
|
||||
Validate(parsed, licenseList);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private static void Validate(SpdxLicenseExpression expression, SpdxLicenseList list)
|
||||
{
|
||||
switch (expression)
|
||||
{
|
||||
case SpdxSimpleLicense simple:
|
||||
if (IsSpecial(simple.LicenseId) || IsLicenseRef(simple.LicenseId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!list.LicenseIds.Contains(simple.LicenseId))
|
||||
{
|
||||
throw new FormatException($"Unknown SPDX license identifier: {simple.LicenseId}");
|
||||
}
|
||||
break;
|
||||
case SpdxWithException withException:
|
||||
Validate(withException.License, list);
|
||||
if (!list.ExceptionIds.Contains(withException.Exception))
|
||||
{
|
||||
throw new FormatException($"Unknown SPDX license exception: {withException.Exception}");
|
||||
}
|
||||
break;
|
||||
case SpdxConjunctiveLicense conjunctive:
|
||||
Validate(conjunctive.Left, list);
|
||||
Validate(conjunctive.Right, list);
|
||||
break;
|
||||
case SpdxDisjunctiveLicense disjunctive:
|
||||
Validate(disjunctive.Left, list);
|
||||
Validate(disjunctive.Right, list);
|
||||
break;
|
||||
case SpdxNoneLicense:
|
||||
case SpdxNoAssertionLicense:
|
||||
break;
|
||||
default:
|
||||
throw new FormatException("Unsupported SPDX license expression node.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSpecial(string licenseId)
|
||||
=> string.Equals(licenseId, "NONE", StringComparison.Ordinal)
|
||||
|| string.Equals(licenseId, "NOASSERTION", StringComparison.Ordinal);
|
||||
|
||||
private static bool IsLicenseRef(string licenseId)
|
||||
=> licenseId.StartsWith("LicenseRef-", StringComparison.Ordinal)
|
||||
|| licenseId.StartsWith("DocumentRef-", StringComparison.Ordinal);
|
||||
|
||||
private static List<Token> Tokenize(string expression)
|
||||
{
|
||||
var tokens = new List<Token>();
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
void Flush()
|
||||
{
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var value = buffer.ToString();
|
||||
buffer.Clear();
|
||||
tokens.Add(Token.From(value));
|
||||
}
|
||||
|
||||
foreach (var ch in expression)
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
case '(':
|
||||
Flush();
|
||||
tokens.Add(new Token(TokenType.OpenParen, "("));
|
||||
break;
|
||||
case ')':
|
||||
Flush();
|
||||
tokens.Add(new Token(TokenType.CloseParen, ")"));
|
||||
break;
|
||||
default:
|
||||
if (char.IsWhiteSpace(ch))
|
||||
{
|
||||
Flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Append(ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Flush();
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private sealed class Parser
|
||||
{
|
||||
private readonly IReadOnlyList<Token> _tokens;
|
||||
private int _index;
|
||||
|
||||
public Parser(IReadOnlyList<Token> tokens)
|
||||
{
|
||||
_tokens = tokens;
|
||||
}
|
||||
|
||||
public bool HasMoreTokens => _index < _tokens.Count;
|
||||
|
||||
public SpdxLicenseExpression ParseExpression()
|
||||
{
|
||||
var left = ParseWith();
|
||||
while (TryMatch(TokenType.And, out var op) || TryMatch(TokenType.Or, out op))
|
||||
{
|
||||
var right = ParseWith();
|
||||
left = op!.Type == TokenType.And
|
||||
? new SpdxConjunctiveLicense(left, right)
|
||||
: new SpdxDisjunctiveLicense(left, right);
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private SpdxLicenseExpression ParseWith()
|
||||
{
|
||||
var left = ParsePrimary();
|
||||
if (TryMatch(TokenType.With, out var withToken))
|
||||
{
|
||||
var exception = Expect(TokenType.Identifier);
|
||||
left = new SpdxWithException(left, exception.Value);
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private SpdxLicenseExpression ParsePrimary()
|
||||
{
|
||||
if (TryMatch(TokenType.OpenParen, out _))
|
||||
{
|
||||
var inner = ParseExpression();
|
||||
Expect(TokenType.CloseParen);
|
||||
return inner;
|
||||
}
|
||||
|
||||
var token = Expect(TokenType.Identifier);
|
||||
if (string.Equals(token.Value, "NONE", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SpdxNoneLicense.Instance;
|
||||
}
|
||||
|
||||
if (string.Equals(token.Value, "NOASSERTION", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SpdxNoAssertionLicense.Instance;
|
||||
}
|
||||
|
||||
return new SpdxSimpleLicense(token.Value);
|
||||
}
|
||||
|
||||
private bool TryMatch(TokenType type, out Token? token)
|
||||
{
|
||||
token = null;
|
||||
if (_index >= _tokens.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidate = _tokens[_index];
|
||||
if (candidate.Type != type)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_index++;
|
||||
token = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
private Token Expect(TokenType type)
|
||||
{
|
||||
if (_index >= _tokens.Count)
|
||||
{
|
||||
throw new FormatException($"Expected {type} but reached end of expression.");
|
||||
}
|
||||
|
||||
var token = _tokens[_index++];
|
||||
if (token.Type != type)
|
||||
{
|
||||
throw new FormatException($"Expected {type} but found {token.Type}.");
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record Token(TokenType Type, string Value)
|
||||
{
|
||||
public static Token From(string value)
|
||||
{
|
||||
var normalized = value.Trim();
|
||||
if (string.Equals(normalized, "AND", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Token(TokenType.And, "AND");
|
||||
}
|
||||
|
||||
if (string.Equals(normalized, "OR", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Token(TokenType.Or, "OR");
|
||||
}
|
||||
|
||||
if (string.Equals(normalized, "WITH", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Token(TokenType.With, "WITH");
|
||||
}
|
||||
|
||||
return new Token(TokenType.Identifier, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
private enum TokenType
|
||||
{
|
||||
Identifier,
|
||||
And,
|
||||
Or,
|
||||
With,
|
||||
OpenParen,
|
||||
CloseParen
|
||||
}
|
||||
}
|
||||
|
||||
public static class SpdxLicenseExpressionRenderer
|
||||
{
|
||||
public static string Render(SpdxLicenseExpression expression)
|
||||
{
|
||||
return RenderInternal(expression, parentOperator: null);
|
||||
}
|
||||
|
||||
private static string RenderInternal(SpdxLicenseExpression expression, SpdxBinaryOperator? parentOperator)
|
||||
{
|
||||
switch (expression)
|
||||
{
|
||||
case SpdxSimpleLicense simple:
|
||||
return simple.LicenseId;
|
||||
case SpdxNoneLicense:
|
||||
return "NONE";
|
||||
case SpdxNoAssertionLicense:
|
||||
return "NOASSERTION";
|
||||
case SpdxWithException withException:
|
||||
var licenseText = RenderInternal(withException.License, parentOperator: null);
|
||||
return $"{licenseText} WITH {withException.Exception}";
|
||||
case SpdxConjunctiveLicense conjunctive:
|
||||
return RenderBinary(conjunctive.Left, conjunctive.Right, "AND", SpdxBinaryOperator.And, parentOperator);
|
||||
case SpdxDisjunctiveLicense disjunctive:
|
||||
return RenderBinary(disjunctive.Left, disjunctive.Right, "OR", SpdxBinaryOperator.Or, parentOperator);
|
||||
default:
|
||||
throw new InvalidOperationException("Unsupported SPDX license expression node.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string RenderBinary(
|
||||
SpdxLicenseExpression left,
|
||||
SpdxLicenseExpression right,
|
||||
string op,
|
||||
SpdxBinaryOperator current,
|
||||
SpdxBinaryOperator? parent)
|
||||
{
|
||||
var leftText = RenderInternal(left, current);
|
||||
var rightText = RenderInternal(right, current);
|
||||
var text = $"{leftText} {op} {rightText}";
|
||||
|
||||
if (parent.HasValue && parent.Value != current)
|
||||
{
|
||||
return $"({text})";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private enum SpdxBinaryOperator
|
||||
{
|
||||
And,
|
||||
Or
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,643 @@
|
||||
{
|
||||
"licenseListVersion": "3.21",
|
||||
"exceptions": [
|
||||
{
|
||||
"reference": "./389-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./389-exception.html",
|
||||
"referenceNumber": 48,
|
||||
"name": "389 Directory Server Exception",
|
||||
"licenseExceptionId": "389-exception",
|
||||
"seeAlso": [
|
||||
"http://directory.fedoraproject.org/wiki/GPL_Exception_License_Text",
|
||||
"https://web.archive.org/web/20080828121337/http://directory.fedoraproject.org/wiki/GPL_Exception_License_Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Asterisk-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Asterisk-exception.html",
|
||||
"referenceNumber": 33,
|
||||
"name": "Asterisk exception",
|
||||
"licenseExceptionId": "Asterisk-exception",
|
||||
"seeAlso": [
|
||||
"https://github.com/asterisk/libpri/blob/7f91151e6bd10957c746c031c1f4a030e8146e9a/pri.c#L22",
|
||||
"https://github.com/asterisk/libss7/blob/03e81bcd0d28ff25d4c77c78351ddadc82ff5c3f/ss7.c#L24"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Autoconf-exception-2.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Autoconf-exception-2.0.html",
|
||||
"referenceNumber": 42,
|
||||
"name": "Autoconf exception 2.0",
|
||||
"licenseExceptionId": "Autoconf-exception-2.0",
|
||||
"seeAlso": [
|
||||
"http://ac-archive.sourceforge.net/doc/copyright.html",
|
||||
"http://ftp.gnu.org/gnu/autoconf/autoconf-2.59.tar.gz"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Autoconf-exception-3.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Autoconf-exception-3.0.html",
|
||||
"referenceNumber": 41,
|
||||
"name": "Autoconf exception 3.0",
|
||||
"licenseExceptionId": "Autoconf-exception-3.0",
|
||||
"seeAlso": [
|
||||
"http://www.gnu.org/licenses/autoconf-exception-3.0.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Autoconf-exception-generic.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Autoconf-exception-generic.html",
|
||||
"referenceNumber": 4,
|
||||
"name": "Autoconf generic exception",
|
||||
"licenseExceptionId": "Autoconf-exception-generic",
|
||||
"seeAlso": [
|
||||
"https://launchpad.net/ubuntu/precise/+source/xmltooling/+copyright",
|
||||
"https://tracker.debian.org/media/packages/s/sipwitch/copyright-1.9.15-3",
|
||||
"https://opensource.apple.com/source/launchd/launchd-258.1/launchd/compile.auto.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Autoconf-exception-macro.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Autoconf-exception-macro.html",
|
||||
"referenceNumber": 19,
|
||||
"name": "Autoconf macro exception",
|
||||
"licenseExceptionId": "Autoconf-exception-macro",
|
||||
"seeAlso": [
|
||||
"https://github.com/freedesktop/xorg-macros/blob/39f07f7db58ebbf3dcb64a2bf9098ed5cf3d1223/xorg-macros.m4.in",
|
||||
"https://www.gnu.org/software/autoconf-archive/ax_pthread.html",
|
||||
"https://launchpad.net/ubuntu/precise/+source/xmltooling/+copyright"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Bison-exception-2.2.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Bison-exception-2.2.html",
|
||||
"referenceNumber": 11,
|
||||
"name": "Bison exception 2.2",
|
||||
"licenseExceptionId": "Bison-exception-2.2",
|
||||
"seeAlso": [
|
||||
"http://git.savannah.gnu.org/cgit/bison.git/tree/data/yacc.c?id\u003d193d7c7054ba7197b0789e14965b739162319b5e#n141"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Bootloader-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Bootloader-exception.html",
|
||||
"referenceNumber": 50,
|
||||
"name": "Bootloader Distribution Exception",
|
||||
"licenseExceptionId": "Bootloader-exception",
|
||||
"seeAlso": [
|
||||
"https://github.com/pyinstaller/pyinstaller/blob/develop/COPYING.txt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Classpath-exception-2.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Classpath-exception-2.0.html",
|
||||
"referenceNumber": 36,
|
||||
"name": "Classpath exception 2.0",
|
||||
"licenseExceptionId": "Classpath-exception-2.0",
|
||||
"seeAlso": [
|
||||
"http://www.gnu.org/software/classpath/license.html",
|
||||
"https://fedoraproject.org/wiki/Licensing/GPL_Classpath_Exception"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./CLISP-exception-2.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./CLISP-exception-2.0.html",
|
||||
"referenceNumber": 9,
|
||||
"name": "CLISP exception 2.0",
|
||||
"licenseExceptionId": "CLISP-exception-2.0",
|
||||
"seeAlso": [
|
||||
"http://sourceforge.net/p/clisp/clisp/ci/default/tree/COPYRIGHT"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./cryptsetup-OpenSSL-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./cryptsetup-OpenSSL-exception.html",
|
||||
"referenceNumber": 39,
|
||||
"name": "cryptsetup OpenSSL exception",
|
||||
"licenseExceptionId": "cryptsetup-OpenSSL-exception",
|
||||
"seeAlso": [
|
||||
"https://gitlab.com/cryptsetup/cryptsetup/-/blob/main/COPYING",
|
||||
"https://gitlab.nic.cz/datovka/datovka/-/blob/develop/COPYING",
|
||||
"https://github.com/nbs-system/naxsi/blob/951123ad456bdf5ac94e8d8819342fe3d49bc002/naxsi_src/naxsi_raw.c",
|
||||
"http://web.mit.edu/jgross/arch/amd64_deb60/bin/mosh"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./DigiRule-FOSS-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./DigiRule-FOSS-exception.html",
|
||||
"referenceNumber": 20,
|
||||
"name": "DigiRule FOSS License Exception",
|
||||
"licenseExceptionId": "DigiRule-FOSS-exception",
|
||||
"seeAlso": [
|
||||
"http://www.digirulesolutions.com/drupal/foss"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./eCos-exception-2.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./eCos-exception-2.0.html",
|
||||
"referenceNumber": 38,
|
||||
"name": "eCos exception 2.0",
|
||||
"licenseExceptionId": "eCos-exception-2.0",
|
||||
"seeAlso": [
|
||||
"http://ecos.sourceware.org/license-overview.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Fawkes-Runtime-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Fawkes-Runtime-exception.html",
|
||||
"referenceNumber": 8,
|
||||
"name": "Fawkes Runtime Exception",
|
||||
"licenseExceptionId": "Fawkes-Runtime-exception",
|
||||
"seeAlso": [
|
||||
"http://www.fawkesrobotics.org/about/license/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./FLTK-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./FLTK-exception.html",
|
||||
"referenceNumber": 18,
|
||||
"name": "FLTK exception",
|
||||
"licenseExceptionId": "FLTK-exception",
|
||||
"seeAlso": [
|
||||
"http://www.fltk.org/COPYING.php"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Font-exception-2.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Font-exception-2.0.html",
|
||||
"referenceNumber": 7,
|
||||
"name": "Font exception 2.0",
|
||||
"licenseExceptionId": "Font-exception-2.0",
|
||||
"seeAlso": [
|
||||
"http://www.gnu.org/licenses/gpl-faq.html#FontException"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./freertos-exception-2.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./freertos-exception-2.0.html",
|
||||
"referenceNumber": 47,
|
||||
"name": "FreeRTOS Exception 2.0",
|
||||
"licenseExceptionId": "freertos-exception-2.0",
|
||||
"seeAlso": [
|
||||
"https://web.archive.org/web/20060809182744/http://www.freertos.org/a00114.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./GCC-exception-2.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./GCC-exception-2.0.html",
|
||||
"referenceNumber": 54,
|
||||
"name": "GCC Runtime Library exception 2.0",
|
||||
"licenseExceptionId": "GCC-exception-2.0",
|
||||
"seeAlso": [
|
||||
"https://gcc.gnu.org/git/?p\u003dgcc.git;a\u003dblob;f\u003dgcc/libgcc1.c;h\u003d762f5143fc6eed57b6797c82710f3538aa52b40b;hb\u003dcb143a3ce4fb417c68f5fa2691a1b1b1053dfba9#l10"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./GCC-exception-3.1.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./GCC-exception-3.1.html",
|
||||
"referenceNumber": 27,
|
||||
"name": "GCC Runtime Library exception 3.1",
|
||||
"licenseExceptionId": "GCC-exception-3.1",
|
||||
"seeAlso": [
|
||||
"http://www.gnu.org/licenses/gcc-exception-3.1.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./GNAT-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./GNAT-exception.html",
|
||||
"referenceNumber": 13,
|
||||
"name": "GNAT exception",
|
||||
"licenseExceptionId": "GNAT-exception",
|
||||
"seeAlso": [
|
||||
"https://github.com/AdaCore/florist/blob/master/libsrc/posix-configurable_file_limits.adb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./gnu-javamail-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./gnu-javamail-exception.html",
|
||||
"referenceNumber": 34,
|
||||
"name": "GNU JavaMail exception",
|
||||
"licenseExceptionId": "gnu-javamail-exception",
|
||||
"seeAlso": [
|
||||
"http://www.gnu.org/software/classpathx/javamail/javamail.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./GPL-3.0-interface-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./GPL-3.0-interface-exception.html",
|
||||
"referenceNumber": 21,
|
||||
"name": "GPL-3.0 Interface Exception",
|
||||
"licenseExceptionId": "GPL-3.0-interface-exception",
|
||||
"seeAlso": [
|
||||
"https://www.gnu.org/licenses/gpl-faq.en.html#LinkingOverControlledInterface"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./GPL-3.0-linking-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./GPL-3.0-linking-exception.html",
|
||||
"referenceNumber": 1,
|
||||
"name": "GPL-3.0 Linking Exception",
|
||||
"licenseExceptionId": "GPL-3.0-linking-exception",
|
||||
"seeAlso": [
|
||||
"https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./GPL-3.0-linking-source-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./GPL-3.0-linking-source-exception.html",
|
||||
"referenceNumber": 37,
|
||||
"name": "GPL-3.0 Linking Exception (with Corresponding Source)",
|
||||
"licenseExceptionId": "GPL-3.0-linking-source-exception",
|
||||
"seeAlso": [
|
||||
"https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs",
|
||||
"https://github.com/mirror/wget/blob/master/src/http.c#L20"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./GPL-CC-1.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./GPL-CC-1.0.html",
|
||||
"referenceNumber": 52,
|
||||
"name": "GPL Cooperation Commitment 1.0",
|
||||
"licenseExceptionId": "GPL-CC-1.0",
|
||||
"seeAlso": [
|
||||
"https://github.com/gplcc/gplcc/blob/master/Project/COMMITMENT",
|
||||
"https://gplcc.github.io/gplcc/Project/README-PROJECT.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./GStreamer-exception-2005.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./GStreamer-exception-2005.html",
|
||||
"referenceNumber": 35,
|
||||
"name": "GStreamer Exception (2005)",
|
||||
"licenseExceptionId": "GStreamer-exception-2005",
|
||||
"seeAlso": [
|
||||
"https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/licensing.html?gi-language\u003dc#licensing-of-applications-using-gstreamer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./GStreamer-exception-2008.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./GStreamer-exception-2008.html",
|
||||
"referenceNumber": 30,
|
||||
"name": "GStreamer Exception (2008)",
|
||||
"licenseExceptionId": "GStreamer-exception-2008",
|
||||
"seeAlso": [
|
||||
"https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/licensing.html?gi-language\u003dc#licensing-of-applications-using-gstreamer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./i2p-gpl-java-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./i2p-gpl-java-exception.html",
|
||||
"referenceNumber": 40,
|
||||
"name": "i2p GPL+Java Exception",
|
||||
"licenseExceptionId": "i2p-gpl-java-exception",
|
||||
"seeAlso": [
|
||||
"http://geti2p.net/en/get-involved/develop/licenses#java_exception"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./KiCad-libraries-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./KiCad-libraries-exception.html",
|
||||
"referenceNumber": 28,
|
||||
"name": "KiCad Libraries Exception",
|
||||
"licenseExceptionId": "KiCad-libraries-exception",
|
||||
"seeAlso": [
|
||||
"https://www.kicad.org/libraries/license/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./LGPL-3.0-linking-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./LGPL-3.0-linking-exception.html",
|
||||
"referenceNumber": 2,
|
||||
"name": "LGPL-3.0 Linking Exception",
|
||||
"licenseExceptionId": "LGPL-3.0-linking-exception",
|
||||
"seeAlso": [
|
||||
"https://raw.githubusercontent.com/go-xmlpath/xmlpath/v2/LICENSE",
|
||||
"https://github.com/goamz/goamz/blob/master/LICENSE",
|
||||
"https://github.com/juju/errors/blob/master/LICENSE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./libpri-OpenH323-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./libpri-OpenH323-exception.html",
|
||||
"referenceNumber": 32,
|
||||
"name": "libpri OpenH323 exception",
|
||||
"licenseExceptionId": "libpri-OpenH323-exception",
|
||||
"seeAlso": [
|
||||
"https://github.com/asterisk/libpri/blob/1.6.0/README#L19-L22"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Libtool-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Libtool-exception.html",
|
||||
"referenceNumber": 17,
|
||||
"name": "Libtool Exception",
|
||||
"licenseExceptionId": "Libtool-exception",
|
||||
"seeAlso": [
|
||||
"http://git.savannah.gnu.org/cgit/libtool.git/tree/m4/libtool.m4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Linux-syscall-note.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Linux-syscall-note.html",
|
||||
"referenceNumber": 49,
|
||||
"name": "Linux Syscall Note",
|
||||
"licenseExceptionId": "Linux-syscall-note",
|
||||
"seeAlso": [
|
||||
"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/COPYING"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./LLGPL.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./LLGPL.html",
|
||||
"referenceNumber": 3,
|
||||
"name": "LLGPL Preamble",
|
||||
"licenseExceptionId": "LLGPL",
|
||||
"seeAlso": [
|
||||
"http://opensource.franz.com/preamble.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./LLVM-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./LLVM-exception.html",
|
||||
"referenceNumber": 14,
|
||||
"name": "LLVM Exception",
|
||||
"licenseExceptionId": "LLVM-exception",
|
||||
"seeAlso": [
|
||||
"http://llvm.org/foundation/relicensing/LICENSE.txt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./LZMA-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./LZMA-exception.html",
|
||||
"referenceNumber": 55,
|
||||
"name": "LZMA exception",
|
||||
"licenseExceptionId": "LZMA-exception",
|
||||
"seeAlso": [
|
||||
"http://nsis.sourceforge.net/Docs/AppendixI.html#I.6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./mif-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./mif-exception.html",
|
||||
"referenceNumber": 53,
|
||||
"name": "Macros and Inline Functions Exception",
|
||||
"licenseExceptionId": "mif-exception",
|
||||
"seeAlso": [
|
||||
"http://www.scs.stanford.edu/histar/src/lib/cppsup/exception",
|
||||
"http://dev.bertos.org/doxygen/",
|
||||
"https://www.threadingbuildingblocks.org/licensing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Nokia-Qt-exception-1.1.json",
|
||||
"isDeprecatedLicenseId": true,
|
||||
"detailsUrl": "./Nokia-Qt-exception-1.1.html",
|
||||
"referenceNumber": 31,
|
||||
"name": "Nokia Qt LGPL exception 1.1",
|
||||
"licenseExceptionId": "Nokia-Qt-exception-1.1",
|
||||
"seeAlso": [
|
||||
"https://www.keepassx.org/dev/projects/keepassx/repository/revisions/b8dfb9cc4d5133e0f09cd7533d15a4f1c19a40f2/entry/LICENSE.NOKIA-LGPL-EXCEPTION"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./OCaml-LGPL-linking-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./OCaml-LGPL-linking-exception.html",
|
||||
"referenceNumber": 29,
|
||||
"name": "OCaml LGPL Linking Exception",
|
||||
"licenseExceptionId": "OCaml-LGPL-linking-exception",
|
||||
"seeAlso": [
|
||||
"https://caml.inria.fr/ocaml/license.en.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./OCCT-exception-1.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./OCCT-exception-1.0.html",
|
||||
"referenceNumber": 15,
|
||||
"name": "Open CASCADE Exception 1.0",
|
||||
"licenseExceptionId": "OCCT-exception-1.0",
|
||||
"seeAlso": [
|
||||
"http://www.opencascade.com/content/licensing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./OpenJDK-assembly-exception-1.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./OpenJDK-assembly-exception-1.0.html",
|
||||
"referenceNumber": 24,
|
||||
"name": "OpenJDK Assembly exception 1.0",
|
||||
"licenseExceptionId": "OpenJDK-assembly-exception-1.0",
|
||||
"seeAlso": [
|
||||
"http://openjdk.java.net/legal/assembly-exception.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./openvpn-openssl-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./openvpn-openssl-exception.html",
|
||||
"referenceNumber": 43,
|
||||
"name": "OpenVPN OpenSSL Exception",
|
||||
"licenseExceptionId": "openvpn-openssl-exception",
|
||||
"seeAlso": [
|
||||
"http://openvpn.net/index.php/license.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./PS-or-PDF-font-exception-20170817.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./PS-or-PDF-font-exception-20170817.html",
|
||||
"referenceNumber": 45,
|
||||
"name": "PS/PDF font exception (2017-08-17)",
|
||||
"licenseExceptionId": "PS-or-PDF-font-exception-20170817",
|
||||
"seeAlso": [
|
||||
"https://github.com/ArtifexSoftware/urw-base35-fonts/blob/65962e27febc3883a17e651cdb23e783668c996f/LICENSE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./QPL-1.0-INRIA-2004-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./QPL-1.0-INRIA-2004-exception.html",
|
||||
"referenceNumber": 44,
|
||||
"name": "INRIA QPL 1.0 2004 variant exception",
|
||||
"licenseExceptionId": "QPL-1.0-INRIA-2004-exception",
|
||||
"seeAlso": [
|
||||
"https://git.frama-c.com/pub/frama-c/-/blob/master/licenses/Q_MODIFIED_LICENSE",
|
||||
"https://github.com/maranget/hevea/blob/master/LICENSE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Qt-GPL-exception-1.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Qt-GPL-exception-1.0.html",
|
||||
"referenceNumber": 10,
|
||||
"name": "Qt GPL exception 1.0",
|
||||
"licenseExceptionId": "Qt-GPL-exception-1.0",
|
||||
"seeAlso": [
|
||||
"http://code.qt.io/cgit/qt/qtbase.git/tree/LICENSE.GPL3-EXCEPT"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Qt-LGPL-exception-1.1.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Qt-LGPL-exception-1.1.html",
|
||||
"referenceNumber": 16,
|
||||
"name": "Qt LGPL exception 1.1",
|
||||
"licenseExceptionId": "Qt-LGPL-exception-1.1",
|
||||
"seeAlso": [
|
||||
"http://code.qt.io/cgit/qt/qtbase.git/tree/LGPL_EXCEPTION.txt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Qwt-exception-1.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Qwt-exception-1.0.html",
|
||||
"referenceNumber": 51,
|
||||
"name": "Qwt exception 1.0",
|
||||
"licenseExceptionId": "Qwt-exception-1.0",
|
||||
"seeAlso": [
|
||||
"http://qwt.sourceforge.net/qwtlicense.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./SHL-2.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./SHL-2.0.html",
|
||||
"referenceNumber": 26,
|
||||
"name": "Solderpad Hardware License v2.0",
|
||||
"licenseExceptionId": "SHL-2.0",
|
||||
"seeAlso": [
|
||||
"https://solderpad.org/licenses/SHL-2.0/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./SHL-2.1.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./SHL-2.1.html",
|
||||
"referenceNumber": 23,
|
||||
"name": "Solderpad Hardware License v2.1",
|
||||
"licenseExceptionId": "SHL-2.1",
|
||||
"seeAlso": [
|
||||
"https://solderpad.org/licenses/SHL-2.1/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./SWI-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./SWI-exception.html",
|
||||
"referenceNumber": 22,
|
||||
"name": "SWI exception",
|
||||
"licenseExceptionId": "SWI-exception",
|
||||
"seeAlso": [
|
||||
"https://github.com/SWI-Prolog/packages-clpqr/blob/bfa80b9270274f0800120d5b8e6fef42ac2dc6a5/clpqr/class.pl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Swift-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Swift-exception.html",
|
||||
"referenceNumber": 46,
|
||||
"name": "Swift Exception",
|
||||
"licenseExceptionId": "Swift-exception",
|
||||
"seeAlso": [
|
||||
"https://swift.org/LICENSE.txt",
|
||||
"https://github.com/apple/swift-package-manager/blob/7ab2275f447a5eb37497ed63a9340f8a6d1e488b/LICENSE.txt#L205"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./u-boot-exception-2.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./u-boot-exception-2.0.html",
|
||||
"referenceNumber": 5,
|
||||
"name": "U-Boot exception 2.0",
|
||||
"licenseExceptionId": "u-boot-exception-2.0",
|
||||
"seeAlso": [
|
||||
"http://git.denx.de/?p\u003du-boot.git;a\u003dblob;f\u003dLicenses/Exceptions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./Universal-FOSS-exception-1.0.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./Universal-FOSS-exception-1.0.html",
|
||||
"referenceNumber": 12,
|
||||
"name": "Universal FOSS Exception, Version 1.0",
|
||||
"licenseExceptionId": "Universal-FOSS-exception-1.0",
|
||||
"seeAlso": [
|
||||
"https://oss.oracle.com/licenses/universal-foss-exception/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./vsftpd-openssl-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./vsftpd-openssl-exception.html",
|
||||
"referenceNumber": 56,
|
||||
"name": "vsftpd OpenSSL exception",
|
||||
"licenseExceptionId": "vsftpd-openssl-exception",
|
||||
"seeAlso": [
|
||||
"https://git.stg.centos.org/source-git/vsftpd/blob/f727873674d9c9cd7afcae6677aa782eb54c8362/f/LICENSE",
|
||||
"https://launchpad.net/debian/squeeze/+source/vsftpd/+copyright",
|
||||
"https://github.com/richardcochran/vsftpd/blob/master/COPYING"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./WxWindows-exception-3.1.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./WxWindows-exception-3.1.html",
|
||||
"referenceNumber": 25,
|
||||
"name": "WxWindows Library Exception 3.1",
|
||||
"licenseExceptionId": "WxWindows-exception-3.1",
|
||||
"seeAlso": [
|
||||
"http://www.opensource.org/licenses/WXwindows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"reference": "./x11vnc-openssl-exception.json",
|
||||
"isDeprecatedLicenseId": false,
|
||||
"detailsUrl": "./x11vnc-openssl-exception.html",
|
||||
"referenceNumber": 6,
|
||||
"name": "x11vnc OpenSSL Exception",
|
||||
"licenseExceptionId": "x11vnc-openssl-exception",
|
||||
"seeAlso": [
|
||||
"https://github.com/LibVNC/x11vnc/blob/master/src/8to24.c#L22"
|
||||
]
|
||||
}
|
||||
],
|
||||
"releaseDate": "2023-06-18"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,10 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="JsonSchema.Net" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\\spdx-license-list-3.21.json" />
|
||||
<EmbeddedResource Include="Resources\\spdx-license-exceptions-3.21.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Attestor StandardPredicates 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`,
|
||||
`docs/implplan/SPRINT_20260119_014_Attestor_spdx_3.0.1_generation.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
@@ -15,3 +16,14 @@ Source of truth: `docs/implplan/SPRINT_20260113_001_002_ATTESTOR_binary_diff_pre
|
||||
| BINARYDIFF-SIGNER-0001 | DONE | Implement DSSE signer for binary diff predicates. |
|
||||
| BINARYDIFF-VERIFIER-0001 | DONE | Implement DSSE verifier for binary diff predicates. |
|
||||
| BINARYDIFF-DI-0001 | DONE | Register BinaryDiff services and options in DI. |
|
||||
| TASK-013-001 | DONE | Extended SbomDocument with CycloneDX 1.7 concepts. |
|
||||
| TASK-013-002 | DONE | CycloneDxWriter upgraded to spec 1.7 and new sections. |
|
||||
| TASK-013-003 | DONE | Component-level 1.7 fields and evidence support. |
|
||||
| TASK-013-004 | DONE | Services and formulation serialization implemented. |
|
||||
| TASK-013-005 | DONE | ModelCard generation for ML components. |
|
||||
| TASK-013-006 | DONE | CryptoProperties and CBOM fields supported. |
|
||||
| TASK-013-007 | DONE | Annotations, compositions, declarations, definitions serialized. |
|
||||
| TASK-013-008 | DONE | Signature mapping and JWK validation added. |
|
||||
| TASK-014-001 | DOING | SPDX 3.0.1 context/spec version and writer baseline. |
|
||||
| TASK-014-002 | DOING | Core profile elements in progress. |
|
||||
| TASK-014-011 | DOING | Integrity methods and external references/identifiers in progress. |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,8 @@
|
||||
// Description: Interface for deterministic SBOM writing
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Attestor.StandardPredicates.Models;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.Writers;
|
||||
|
||||
/// <summary>
|
||||
@@ -58,167 +60,3 @@ public interface ISbomWriter
|
||||
/// <returns>Write result containing canonical bytes and hash.</returns>
|
||||
Task<SbomWriteResult> WriteAsync(SbomDocument document, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified SBOM document model for Attestor operations.
|
||||
/// </summary>
|
||||
public sealed record SbomDocument
|
||||
{
|
||||
/// <summary>
|
||||
/// Document name/identifier.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Document version.
|
||||
/// </summary>
|
||||
public string? Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creation timestamp (UTC).
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 digest of the artifact this SBOM describes (e.g., container image digest).
|
||||
/// Used to derive deterministic serialNumber: urn:sha256:<artifact-digest>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sprint: SPRINT_20260118_025_ReleaseOrchestrator_sbom_release_association (TASK-025-004)
|
||||
/// If provided, CycloneDxWriter will generate serialNumber as urn:sha256:<artifact-digest>
|
||||
/// instead of using a deterministic UUID. This enables reproducible SBOMs where the
|
||||
/// serialNumber directly references the artifact being described.
|
||||
/// Format: lowercase hex string, 64 characters (no prefix).
|
||||
/// </remarks>
|
||||
public string? ArtifactDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Components in the SBOM.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SbomComponent> Components { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Dependencies between components.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SbomDependency> Dependencies { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Tool information.
|
||||
/// </summary>
|
||||
public SbomTool? Tool { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// External references.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SbomExternalReference> ExternalReferences { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A component in the SBOM.
|
||||
/// </summary>
|
||||
public sealed record SbomComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique reference ID.
|
||||
/// </summary>
|
||||
public required string BomRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Component name.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Component version.
|
||||
/// </summary>
|
||||
public string? Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Package URL (purl).
|
||||
/// </summary>
|
||||
public string? Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Component type.
|
||||
/// </summary>
|
||||
public string Type { get; init; } = "library";
|
||||
|
||||
/// <summary>
|
||||
/// Hashes for the component.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SbomHash> Hashes { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// License identifiers.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Licenses { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A hash in the SBOM.
|
||||
/// </summary>
|
||||
public sealed record SbomHash
|
||||
{
|
||||
/// <summary>
|
||||
/// Hash algorithm (e.g., SHA-256, SHA-512).
|
||||
/// </summary>
|
||||
public required string Algorithm { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash value in hex format.
|
||||
/// </summary>
|
||||
public required string Value { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A dependency relationship.
|
||||
/// </summary>
|
||||
public sealed record SbomDependency
|
||||
{
|
||||
/// <summary>
|
||||
/// The component that has the dependency.
|
||||
/// </summary>
|
||||
public required string Ref { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Components this component depends on.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> DependsOn { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tool information.
|
||||
/// </summary>
|
||||
public sealed record SbomTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Tool vendor.
|
||||
/// </summary>
|
||||
public string? Vendor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tool name.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tool version.
|
||||
/// </summary>
|
||||
public string? Version { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An external reference.
|
||||
/// </summary>
|
||||
public sealed record SbomExternalReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference type.
|
||||
/// </summary>
|
||||
public required string Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference URL.
|
||||
/// </summary>
|
||||
public required string Url { get; init; }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,8 @@
|
||||
// Description: Service implementation for timestamping attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -18,16 +20,31 @@ public sealed class AttestationTimestampService : IAttestationTimestampService
|
||||
{
|
||||
private readonly AttestationTimestampServiceOptions _options;
|
||||
private readonly ILogger<AttestationTimestampService> _logger;
|
||||
private readonly Histogram<double>? _timestampDurationSeconds;
|
||||
private readonly Counter<long>? _timestampAttempts;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AttestationTimestampService"/> class.
|
||||
/// </summary>
|
||||
public AttestationTimestampService(
|
||||
IOptions<AttestationTimestampServiceOptions> options,
|
||||
ILogger<AttestationTimestampService> logger)
|
||||
ILogger<AttestationTimestampService> logger,
|
||||
IMeterFactory? meterFactory = null)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
|
||||
if (meterFactory is not null)
|
||||
{
|
||||
var meter = meterFactory.Create("StellaOps.Attestor.Timestamping");
|
||||
_timestampDurationSeconds = meter.CreateHistogram<double>(
|
||||
"attestation_timestamp_duration_seconds",
|
||||
unit: "s",
|
||||
description: "Duration of RFC-3161 timestamp requests.");
|
||||
_timestampAttempts = meter.CreateCounter<long>(
|
||||
"attestation_timestamp_attempts_total",
|
||||
description: "Total RFC-3161 timestamp attempts grouped by result.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -37,42 +54,57 @@ public sealed class AttestationTimestampService : IAttestationTimestampService
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
options ??= AttestationTimestampOptions.Default;
|
||||
var startTimestamp = Stopwatch.GetTimestamp();
|
||||
var success = false;
|
||||
|
||||
// Hash the envelope
|
||||
var algorithm = options.HashAlgorithm switch
|
||||
try
|
||||
{
|
||||
"SHA256" => HashAlgorithmName.SHA256,
|
||||
"SHA384" => HashAlgorithmName.SHA384,
|
||||
"SHA512" => HashAlgorithmName.SHA512,
|
||||
_ => HashAlgorithmName.SHA256
|
||||
};
|
||||
// Hash the envelope
|
||||
var algorithm = options.HashAlgorithm switch
|
||||
{
|
||||
"SHA256" => HashAlgorithmName.SHA256,
|
||||
"SHA384" => HashAlgorithmName.SHA384,
|
||||
"SHA512" => HashAlgorithmName.SHA512,
|
||||
_ => HashAlgorithmName.SHA256
|
||||
};
|
||||
|
||||
var hash = ComputeHash(envelope.Span, algorithm);
|
||||
var digestHex = Convert.ToHexString(hash).ToLowerInvariant();
|
||||
var hash = ComputeHash(envelope.Span, algorithm);
|
||||
var digestHex = Convert.ToHexString(hash).ToLowerInvariant();
|
||||
|
||||
_logger.LogDebug(
|
||||
"Timestamping attestation envelope with {Algorithm} digest: {Digest}",
|
||||
options.HashAlgorithm,
|
||||
digestHex);
|
||||
_logger.LogDebug(
|
||||
"Timestamping attestation envelope with {Algorithm} digest: {Digest}",
|
||||
options.HashAlgorithm,
|
||||
digestHex);
|
||||
|
||||
// Call TSA client (placeholder - would integrate with ITimeStampAuthorityClient)
|
||||
var tstBytes = await RequestTimestampAsync(hash, options, cancellationToken);
|
||||
var (genTime, tsaName, policyOid) = ParseTstInfo(tstBytes);
|
||||
// Call TSA client (placeholder - would integrate with ITimeStampAuthorityClient)
|
||||
var tstBytes = await RequestTimestampAsync(hash, options, cancellationToken);
|
||||
var (genTime, tsaName, policyOid) = ParseTstInfo(tstBytes);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Attestation timestamped at {Time} by {TSA}",
|
||||
genTime,
|
||||
tsaName);
|
||||
_logger.LogInformation(
|
||||
"Attestation timestamped at {Time} by {TSA}",
|
||||
genTime,
|
||||
tsaName);
|
||||
|
||||
return new TimestampedAttestation
|
||||
var result = new TimestampedAttestation
|
||||
{
|
||||
Envelope = envelope.ToArray(),
|
||||
EnvelopeDigest = $"{options.HashAlgorithm.ToLowerInvariant()}:{digestHex}",
|
||||
TimeStampToken = tstBytes,
|
||||
TimestampTime = genTime,
|
||||
TsaName = tsaName,
|
||||
TsaPolicyOid = policyOid
|
||||
};
|
||||
|
||||
success = true;
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Envelope = envelope.ToArray(),
|
||||
EnvelopeDigest = $"{options.HashAlgorithm.ToLowerInvariant()}:{digestHex}",
|
||||
TimeStampToken = tstBytes,
|
||||
TimestampTime = genTime,
|
||||
TsaName = tsaName,
|
||||
TsaPolicyOid = policyOid
|
||||
};
|
||||
var elapsed = Stopwatch.GetElapsedTime(startTimestamp).TotalSeconds;
|
||||
var tags = new TagList { { "result", success ? "success" : "failure" } };
|
||||
_timestampDurationSeconds?.Record(elapsed, tags);
|
||||
_timestampAttempts?.Add(1, tags);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Attestor Timestamping Task Board
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260119_010_Attestor_tst_integration.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| ATT-001 | DONE | Added timestamping metrics and unit coverage for envelope hashing. |
|
||||
| ATT-002 | DONE | Covered timestamp verification scenarios and Rekor consistency checks. |
|
||||
| ATT-003 | DONE | Added policy context docs and evaluator tests for timestamp assertions. |
|
||||
| ATT-006 | DONE | Added time correlation validator unit tests. |
|
||||
| TASK-029-002 | DONE | Bundle TSA chain + revocation data for offline verification. |
|
||||
@@ -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 FluentAssertions;
|
||||
|
||||
@@ -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 FluentAssertions;
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
// <copyright file="BuildAttestationMapperTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="BuildProfileValidatorTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="CombinedDocumentBuilderTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// <copyright file="DsseSpdx3SignerTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
@@ -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