save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View File

@@ -34,7 +34,7 @@ public sealed class BuildAttestationMapper : IBuildAttestationMapper
ArgumentException.ThrowIfNullOrWhiteSpace(spdxIdPrefix);
var configSourceUris = ImmutableArray<string>.Empty;
var configSourceDigests = ImmutableArray<Spdx3Hash>.Empty;
var configSourceDigests = ImmutableArray<Spdx3BuildHash>.Empty;
var configSourceEntrypoints = ImmutableArray<string>.Empty;
if (attestation.Invocation?.ConfigSource is { } configSource)
@@ -47,7 +47,7 @@ public sealed class BuildAttestationMapper : IBuildAttestationMapper
if (configSource.Digest.Count > 0)
{
configSourceDigests = configSource.Digest
.Select(kvp => new Spdx3Hash { Algorithm = kvp.Key, HashValue = kvp.Value })
.Select(kvp => new Spdx3BuildHash { Algorithm = kvp.Key, HashValue = kvp.Value })
.ToImmutableArray();
}

View File

@@ -35,7 +35,7 @@ public sealed class BuildRelationshipBuilder
public BuildRelationshipBuilder AddBuildToolOf(string toolSpdxId, string artifactSpdxId)
{
_relationships.Add(CreateRelationship(
"BUILD_TOOL_OF",
Spdx3RelationshipType.BuildToolOf,
toolSpdxId,
artifactSpdxId));
return this;
@@ -49,7 +49,7 @@ public sealed class BuildRelationshipBuilder
public BuildRelationshipBuilder AddGenerates(string buildSpdxId, string artifactSpdxId)
{
_relationships.Add(CreateRelationship(
"GENERATES",
Spdx3RelationshipType.Generates,
buildSpdxId,
artifactSpdxId));
return this;
@@ -63,7 +63,7 @@ public sealed class BuildRelationshipBuilder
public BuildRelationshipBuilder AddGeneratedFrom(string artifactSpdxId, string sourceSpdxId)
{
_relationships.Add(CreateRelationship(
"GENERATED_FROM",
Spdx3RelationshipType.GeneratedFrom,
artifactSpdxId,
sourceSpdxId));
return this;
@@ -77,7 +77,7 @@ public sealed class BuildRelationshipBuilder
public BuildRelationshipBuilder AddHasPrerequisite(string buildSpdxId, string prerequisiteSpdxId)
{
_relationships.Add(CreateRelationship(
"HAS_PREREQUISITE",
Spdx3RelationshipType.HasPrerequisite,
buildSpdxId,
prerequisiteSpdxId));
return this;
@@ -133,11 +133,11 @@ public sealed class BuildRelationshipBuilder
}
private Spdx3Relationship CreateRelationship(
string relationshipType,
Spdx3RelationshipType relationshipType,
string fromSpdxId,
string toSpdxId)
{
var relId = $"{_spdxIdPrefix}/relationship/{relationshipType.ToLowerInvariant()}/{_relationships.Count + 1}";
var relId = $"{_spdxIdPrefix}/relationship/{relationshipType.ToString().ToLowerInvariant()}/{_relationships.Count + 1}";
return new Spdx3Relationship
{

View File

@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\__Libraries\StellaOps.Spdx3\StellaOps.Spdx3.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Spdx3\StellaOps.Spdx3.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,387 @@
// -----------------------------------------------------------------------------
// BuildProfileIntegrationTests.cs
// Sprint: SPRINT_20260107_004_003_BE_spdx3_build_profile
// Task: BP-011 - Integration tests for SPDX 3.0.1 Build profile
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using Moq;
using StellaOps.Spdx3.Model;
using StellaOps.Spdx3.Model.Build;
using Xunit;
namespace StellaOps.Attestor.Spdx3.Tests.Integration;
/// <summary>
/// Integration tests for SPDX 3.0.1 Build profile end-to-end flows.
/// These tests verify the complete attestation-to-SPDX 3.0.1 pipeline.
/// </summary>
[Trait("Category", "Integration")]
public sealed class BuildProfileIntegrationTests
{
private static readonly DateTimeOffset FixedTimestamp =
new(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
[Fact]
public async Task EndToEnd_AttestationToSpdx3_ProducesValidBuildProfile()
{
// Arrange: Create a realistic build attestation payload
var attestation = new BuildAttestationPayload
{
Type = "https://in-toto.io/Statement/v1",
PredicateType = "https://slsa.dev/provenance/v1",
Subject = ImmutableArray.Create(new AttestationSubject
{
Name = "pkg:oci/myapp@sha256:abc123",
Digest = new Dictionary<string, string>
{
["sha256"] = "abc123def456"
}.ToImmutableDictionary()
}),
Predicate = new BuildPredicate
{
BuildDefinition = new BuildDefinitionInfo
{
BuildType = "https://stellaops.org/build/container-scan/v1",
ExternalParameters = new Dictionary<string, object>
{
["imageReference"] = "registry.io/myapp:latest"
}.ToImmutableDictionary(),
InternalParameters = ImmutableDictionary<string, object>.Empty,
ResolvedDependencies = ImmutableArray.Create(new ResourceDescriptor
{
Name = "base-image",
Uri = "pkg:oci/alpine@sha256:def789",
Digest = new Dictionary<string, string>
{
["sha256"] = "def789"
}.ToImmutableDictionary()
})
},
RunDetails = new RunDetailsInfo
{
Builder = new BuilderInfo
{
Id = "https://stellaops.org/scanner/v1.0.0",
Version = new Dictionary<string, string>
{
["stellaops"] = "1.0.0"
}.ToImmutableDictionary()
},
Metadata = new BuildMetadata
{
InvocationId = "scan-12345",
StartedOn = FixedTimestamp.AddMinutes(-5),
FinishedOn = FixedTimestamp
}
}
}
};
var mapper = new BuildAttestationMapper();
// Act: Map attestation to SPDX 3.0.1 Build element
var buildElement = mapper.MapToSpdx3(attestation);
// Assert: Verify all fields are correctly mapped
buildElement.Should().NotBeNull();
buildElement.BuildType.Should().Be("https://stellaops.org/build/container-scan/v1");
buildElement.BuildId.Should().Be("scan-12345");
buildElement.BuildStartTime.Should().Be(FixedTimestamp.AddMinutes(-5));
buildElement.BuildEndTime.Should().Be(FixedTimestamp);
buildElement.ConfigSourceUri.Should().NotBeNullOrEmpty();
}
[Fact]
public void SignatureVerification_ValidSignedDocument_Succeeds()
{
// Arrange: Create document and sign it
var timeProvider = new FakeTimeProvider(FixedTimestamp);
var serializer = new Spdx3JsonSerializer();
var signingProvider = new TestDsseSigningProvider();
var signer = new DsseSpdx3Signer(serializer, signingProvider, timeProvider);
var document = CreateTestSpdx3Document();
var options = new DsseSpdx3SigningOptions { PrimaryKeyId = "test-key-1" };
// Act: Sign the document
var envelope = signer.SignAsync(document, options).Result;
// Assert: Signature should be present
envelope.Should().NotBeNull();
envelope.Signatures.Should().HaveCount(1);
envelope.PayloadType.Should().Be("application/vnd.spdx3+json");
// Verify: Extract and verify the document
var verifier = new DsseSpdx3Verifier(serializer, signingProvider);
var verificationResult = verifier.VerifyAsync(envelope, CancellationToken.None).Result;
verificationResult.IsValid.Should().BeTrue();
verificationResult.ExtractedDocument.Should().NotBeNull();
}
[Fact]
public void ImportExternalBuildProfile_ValidDocument_ParsesCorrectly()
{
// Arrange: External SPDX 3.0.1 Build profile JSON
var externalJson = """
{
"@context": "https://spdx.org/rdf/3.0.1/terms/",
"@graph": [
{
"@type": "Build",
"spdxId": "urn:external:build:ext-build-001",
"build_buildType": "https://example.com/build/maven/v1",
"build_buildId": "maven-build-789",
"build_buildStartTime": "2026-01-09T10:00:00Z",
"build_buildEndTime": "2026-01-09T10:15:00Z",
"build_configSourceUri": ["https://github.com/example/repo"],
"build_configSourceDigest": [
{
"algorithm": "sha256",
"hashValue": "feedfacecafe"
}
],
"build_environment": {
"JAVA_VERSION": "21",
"MAVEN_VERSION": "3.9.6"
}
}
]
}
""";
// Act: Parse the external document
var parser = new Spdx3Parser();
var parseResult = parser.Parse(externalJson);
// Assert: Build element should be correctly parsed
parseResult.IsSuccess.Should().BeTrue();
parseResult.Document.Should().NotBeNull();
var buildElements = parseResult.Document!.Elements
.OfType<Spdx3Build>()
.ToList();
buildElements.Should().HaveCount(1);
var build = buildElements[0];
build.SpdxId.Should().Be("urn:external:build:ext-build-001");
build.BuildType.Should().Be("https://example.com/build/maven/v1");
build.BuildId.Should().Be("maven-build-789");
build.BuildStartTime.Should().Be(new DateTimeOffset(2026, 1, 9, 10, 0, 0, TimeSpan.Zero));
build.BuildEndTime.Should().Be(new DateTimeOffset(2026, 1, 9, 10, 15, 0, TimeSpan.Zero));
build.ConfigSourceUri.Should().Contain("https://github.com/example/repo");
build.Environment.Should().ContainKey("JAVA_VERSION");
build.Environment!["JAVA_VERSION"].Should().Be("21");
}
[Fact]
public void CombinedDocument_SoftwareAndBuildProfiles_MergesCorrectly()
{
// Arrange: Create Software profile SBOM
var sbomDocument = new Spdx3Document
{
SpdxId = "urn:stellaops:sbom:sbom-001",
Name = "MyApp SBOM",
Namespaces = ImmutableArray.Create("https://stellaops.org/spdx/"),
ProfileConformance = ImmutableArray.Create(Spdx3Profile.Software),
Elements = ImmutableArray.Create<Spdx3Element>(
new Spdx3Package
{
SpdxId = "urn:stellaops:pkg:myapp-1.0.0",
Name = "MyApp",
PackageVersion = "1.0.0",
PackageUrl = "pkg:npm/myapp@1.0.0"
}
)
};
// Arrange: Create Build profile element
var buildElement = new Spdx3Build
{
SpdxId = "urn:stellaops:build:build-001",
BuildType = "https://stellaops.org/build/scanner/v1",
BuildId = "scan-12345",
BuildStartTime = FixedTimestamp.AddMinutes(-5),
BuildEndTime = FixedTimestamp
};
// Act: Combine using CombinedDocumentBuilder
var builder = new CombinedDocumentBuilder();
var combinedDoc = builder
.WithSoftwareDocument(sbomDocument)
.WithBuildProvenance(buildElement)
.Build();
// Assert: Combined document has both profiles
combinedDoc.Should().NotBeNull();
combinedDoc.ProfileConformance.Should().Contain(Spdx3Profile.Software);
combinedDoc.ProfileConformance.Should().Contain(Spdx3Profile.Build);
// Assert: Contains both package and build elements
combinedDoc.Elements.OfType<Spdx3Package>().Should().HaveCount(1);
combinedDoc.Elements.OfType<Spdx3Build>().Should().HaveCount(1);
// Assert: GENERATES relationship exists
var relationships = combinedDoc.Elements.OfType<Spdx3Relationship>().ToList();
var generatesRel = relationships.FirstOrDefault(r =>
r.RelationshipType == Spdx3RelationshipType.Generates);
generatesRel.Should().NotBeNull();
generatesRel!.From.Should().Be(buildElement.SpdxId);
}
[Fact]
public async Task RoundTrip_SignedCombinedDocument_PreservesAllData()
{
// Arrange: Create combined document
var timeProvider = new FakeTimeProvider(FixedTimestamp);
var serializer = new Spdx3JsonSerializer();
var signingProvider = new TestDsseSigningProvider();
var signer = new DsseSpdx3Signer(serializer, signingProvider, timeProvider);
var verifier = new DsseSpdx3Verifier(serializer, signingProvider);
var originalDoc = CreateCombinedTestDocument();
var options = new DsseSpdx3SigningOptions { PrimaryKeyId = "test-key-1" };
// Act: Sign, then verify and extract
var envelope = await signer.SignAsync(originalDoc, options);
var verifyResult = await verifier.VerifyAsync(envelope, CancellationToken.None);
// Assert: Extracted document matches original
verifyResult.IsValid.Should().BeTrue();
var extractedDoc = verifyResult.ExtractedDocument;
extractedDoc.Should().NotBeNull();
extractedDoc!.SpdxId.Should().Be(originalDoc.SpdxId);
extractedDoc.Name.Should().Be(originalDoc.Name);
extractedDoc.ProfileConformance.Should().BeEquivalentTo(originalDoc.ProfileConformance);
// Verify elements preserved
extractedDoc.Elements.OfType<Spdx3Package>().Count()
.Should().Be(originalDoc.Elements.OfType<Spdx3Package>().Count());
extractedDoc.Elements.OfType<Spdx3Build>().Count()
.Should().Be(originalDoc.Elements.OfType<Spdx3Build>().Count());
}
#region Test Helpers
private static Spdx3Document CreateTestSpdx3Document()
{
return new Spdx3Document
{
SpdxId = "urn:stellaops:sbom:test-001",
Name = "Test SBOM",
Namespaces = ImmutableArray.Create("https://stellaops.org/spdx/"),
ProfileConformance = ImmutableArray.Create(Spdx3Profile.Software),
Elements = ImmutableArray.Create<Spdx3Element>(
new Spdx3Package
{
SpdxId = "urn:stellaops:pkg:test-pkg",
Name = "TestPackage",
PackageVersion = "1.0.0"
}
)
};
}
private static Spdx3Document CreateCombinedTestDocument()
{
return new Spdx3Document
{
SpdxId = "urn:stellaops:combined:test-001",
Name = "Combined Test Document",
Namespaces = ImmutableArray.Create("https://stellaops.org/spdx/"),
ProfileConformance = ImmutableArray.Create(Spdx3Profile.Software, Spdx3Profile.Build),
Elements = ImmutableArray.Create<Spdx3Element>(
new Spdx3Package
{
SpdxId = "urn:stellaops:pkg:combined-pkg",
Name = "CombinedPackage",
PackageVersion = "2.0.0"
},
new Spdx3Build
{
SpdxId = "urn:stellaops:build:combined-build",
BuildType = "https://stellaops.org/build/test/v1",
BuildId = "combined-build-001",
BuildStartTime = FixedTimestamp.AddMinutes(-10),
BuildEndTime = FixedTimestamp
},
new Spdx3Relationship
{
SpdxId = "urn:stellaops:rel:generates-001",
RelationshipType = Spdx3RelationshipType.Generates,
From = "urn:stellaops:build:combined-build",
To = ImmutableArray.Create("urn:stellaops:pkg:combined-pkg")
}
)
};
}
/// <summary>
/// Test signing provider that uses a simple HMAC for testing purposes.
/// </summary>
private sealed class TestDsseSigningProvider : IDsseSigningProvider
{
private static readonly byte[] TestKey = Encoding.UTF8.GetBytes("test-signing-key-32-bytes-long!!");
public Task<DsseSignatureResult> SignAsync(
byte[] payload,
string keyId,
string? algorithm,
CancellationToken cancellationToken)
{
using var hmac = new System.Security.Cryptography.HMACSHA256(TestKey);
var signature = hmac.ComputeHash(payload);
return Task.FromResult(new DsseSignatureResult
{
KeyId = keyId,
Algorithm = algorithm ?? "HMAC-SHA256",
SignatureBytes = signature
});
}
public Task<bool> VerifyAsync(
byte[] payload,
byte[] signature,
string keyId,
CancellationToken cancellationToken)
{
using var hmac = new System.Security.Cryptography.HMACSHA256(TestKey);
var expectedSignature = hmac.ComputeHash(payload);
return Task.FromResult(signature.SequenceEqual(expectedSignature));
}
}
#endregion
}
/// <summary>
/// Simple JSON serializer for SPDX 3.0.1 documents (test implementation).
/// </summary>
file sealed class Spdx3JsonSerializer : ISpdx3Serializer
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
public byte[] SerializeToBytes(Spdx3Document document)
{
return JsonSerializer.SerializeToUtf8Bytes(document, Options);
}
public Spdx3Document? DeserializeFromBytes(byte[] bytes)
{
return JsonSerializer.Deserialize<Spdx3Document>(bytes, Options);
}
}