save progress
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"subject": [
|
||||
{
|
||||
"name": "file://dist/app.tar.gz",
|
||||
"digest": {
|
||||
"sha256": "b2c3d4e5f6789012345678901234567890123456789012345678901234abcdef"
|
||||
}
|
||||
}
|
||||
],
|
||||
"predicateType": "https://in-toto.io/Link/v1",
|
||||
"predicate": {
|
||||
"name": "build",
|
||||
"command": [
|
||||
"make",
|
||||
"release",
|
||||
"VERSION=1.0.0"
|
||||
],
|
||||
"materials": [
|
||||
{
|
||||
"uri": "git://github.com/example/repo@abc123def456",
|
||||
"digest": {
|
||||
"sha256": "abc123def4567890123456789012345678901234567890123456789012345678"
|
||||
}
|
||||
},
|
||||
{
|
||||
"uri": "file://Cargo.lock",
|
||||
"digest": {
|
||||
"sha256": "def456789012345678901234567890123456789012345678901234567890abcd"
|
||||
}
|
||||
}
|
||||
],
|
||||
"products": [
|
||||
{
|
||||
"uri": "file://dist/app.tar.gz",
|
||||
"digest": {
|
||||
"sha256": "b2c3d4e5f6789012345678901234567890123456789012345678901234abcdef"
|
||||
}
|
||||
},
|
||||
{
|
||||
"uri": "file://dist/app.tar.gz.sha256",
|
||||
"digest": {
|
||||
"sha256": "c3d4e5f6789012345678901234567890123456789012345678901234abcdef12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"byproducts": {
|
||||
"return-value": 0,
|
||||
"stdout": "Building release v1.0.0...\nBuild complete.",
|
||||
"stderr": ""
|
||||
},
|
||||
"environment": {
|
||||
"GITHUB_SHA": "abc123def456",
|
||||
"GITHUB_RUN_ID": "12345",
|
||||
"RUST_VERSION": "1.75.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"name": "build",
|
||||
"expectedMaterials": ["git://*"],
|
||||
"expectedProducts": ["file://dist/*"],
|
||||
"threshold": 1
|
||||
},
|
||||
{
|
||||
"name": "scan",
|
||||
"expectedMaterials": ["oci://*", "file://dist/*"],
|
||||
"expectedProducts": ["file://sbom.*", "file://vulns.*"],
|
||||
"threshold": 1
|
||||
},
|
||||
{
|
||||
"name": "sign",
|
||||
"expectedMaterials": ["file://dist/*"],
|
||||
"expectedProducts": ["file://dist/*.sig"],
|
||||
"threshold": 2
|
||||
}
|
||||
],
|
||||
"keys": {
|
||||
"builder-key-001": {
|
||||
"keyType": "ecdsa-p256",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...\n-----END PUBLIC KEY-----",
|
||||
"allowedSteps": ["build"]
|
||||
},
|
||||
"scanner-key-001": {
|
||||
"keyType": "ecdsa-p256",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...\n-----END PUBLIC KEY-----",
|
||||
"allowedSteps": ["scan"]
|
||||
},
|
||||
"signer-key-001": {
|
||||
"keyType": "ecdsa-p256",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...\n-----END PUBLIC KEY-----",
|
||||
"allowedSteps": ["sign"]
|
||||
},
|
||||
"signer-key-002": {
|
||||
"keyType": "ecdsa-p256",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...\n-----END PUBLIC KEY-----",
|
||||
"allowedSteps": ["sign"]
|
||||
}
|
||||
},
|
||||
"rootLayoutId": "layout-v1-20260102",
|
||||
"expires": "2027-01-01T00:00:00Z"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"subject": [
|
||||
{
|
||||
"name": "file://sbom.cdx.json",
|
||||
"digest": {
|
||||
"sha256": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"
|
||||
}
|
||||
}
|
||||
],
|
||||
"predicateType": "https://in-toto.io/Link/v1",
|
||||
"predicate": {
|
||||
"name": "scan",
|
||||
"command": [
|
||||
"stella",
|
||||
"scan",
|
||||
"--image",
|
||||
"nginx:1.25"
|
||||
],
|
||||
"materials": [
|
||||
{
|
||||
"uri": "oci://docker.io/library/nginx@sha256:abc123456789",
|
||||
"digest": {
|
||||
"sha256": "abc123456789012345678901234567890123456789012345678901234567890a"
|
||||
}
|
||||
}
|
||||
],
|
||||
"products": [
|
||||
{
|
||||
"uri": "file://sbom.cdx.json",
|
||||
"digest": {
|
||||
"sha256": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"
|
||||
}
|
||||
}
|
||||
],
|
||||
"byproducts": {
|
||||
"return-value": 0
|
||||
},
|
||||
"environment": {
|
||||
"STELLAOPS_VERSION": "2026.01",
|
||||
"CI": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.Core.InToto;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Core.Tests.InToto;
|
||||
|
||||
/// <summary>
|
||||
/// Golden tests that verify in-toto link parsing and serialization against reference fixtures.
|
||||
/// These tests ensure compatibility with the in-toto specification.
|
||||
/// </summary>
|
||||
public class InTotoGoldenTests
|
||||
{
|
||||
private static readonly string FixturesPath = Path.Combine(
|
||||
AppContext.BaseDirectory, "Fixtures", "InToto");
|
||||
|
||||
/// <summary>
|
||||
/// Test that we can parse the golden scan link fixture.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseGoldenScanLink_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var json = File.ReadAllText(Path.Combine(FixturesPath, "golden_scan_link.json"));
|
||||
|
||||
// Act
|
||||
var link = InTotoLink.FromJson(json);
|
||||
|
||||
// Assert
|
||||
link.Should().NotBeNull();
|
||||
link.Subjects.Should().HaveCount(1);
|
||||
link.Subjects[0].Name.Should().Be("file://sbom.cdx.json");
|
||||
link.Subjects[0].Digest.Sha256.Should().Be("a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd");
|
||||
|
||||
link.Predicate.Name.Should().Be("scan");
|
||||
link.Predicate.Command.Should().BeEquivalentTo(new[] { "stella", "scan", "--image", "nginx:1.25" });
|
||||
link.Predicate.Materials.Should().HaveCount(1);
|
||||
link.Predicate.Materials[0].Uri.Should().Be("oci://docker.io/library/nginx@sha256:abc123456789");
|
||||
link.Predicate.Products.Should().HaveCount(1);
|
||||
link.Predicate.Products[0].Uri.Should().Be("file://sbom.cdx.json");
|
||||
link.Predicate.ByProducts.ReturnValue.Should().Be(0);
|
||||
link.Predicate.Environment.Should().ContainKey("STELLAOPS_VERSION");
|
||||
link.Predicate.Environment["STELLAOPS_VERSION"].Should().Be("2026.01");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that we can parse the golden build link fixture.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseGoldenBuildLink_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var json = File.ReadAllText(Path.Combine(FixturesPath, "golden_build_link.json"));
|
||||
|
||||
// Act
|
||||
var link = InTotoLink.FromJson(json);
|
||||
|
||||
// Assert
|
||||
link.Should().NotBeNull();
|
||||
link.Predicate.Name.Should().Be("build");
|
||||
link.Predicate.Command.Should().Contain("make");
|
||||
link.Predicate.Materials.Should().HaveCount(2);
|
||||
link.Predicate.Products.Should().HaveCount(2);
|
||||
link.Predicate.ByProducts.ReturnValue.Should().Be(0);
|
||||
link.Predicate.ByProducts.Stdout.Should().Contain("Build complete");
|
||||
link.Predicate.Environment.Should().ContainKey("GITHUB_SHA");
|
||||
link.Predicate.Environment.Should().ContainKey("RUST_VERSION");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test round-trip serialization of the golden scan link.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RoundTripGoldenScanLink_ShouldPreserveContent()
|
||||
{
|
||||
// Arrange
|
||||
var originalJson = File.ReadAllText(Path.Combine(FixturesPath, "golden_scan_link.json"));
|
||||
var link = InTotoLink.FromJson(originalJson);
|
||||
|
||||
// Act
|
||||
var serializedJson = link.ToJson(indented: true);
|
||||
var reparsedLink = InTotoLink.FromJson(serializedJson);
|
||||
|
||||
// Assert
|
||||
reparsedLink.Predicate.Name.Should().Be(link.Predicate.Name);
|
||||
reparsedLink.Predicate.Command.Should().BeEquivalentTo(link.Predicate.Command);
|
||||
reparsedLink.Predicate.Materials.Should().HaveCount(link.Predicate.Materials.Length);
|
||||
reparsedLink.Predicate.Products.Should().HaveCount(link.Predicate.Products.Length);
|
||||
reparsedLink.Subjects.Should().HaveCount(link.Subjects.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test round-trip serialization of the golden build link.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RoundTripGoldenBuildLink_ShouldPreserveContent()
|
||||
{
|
||||
// Arrange
|
||||
var originalJson = File.ReadAllText(Path.Combine(FixturesPath, "golden_build_link.json"));
|
||||
var link = InTotoLink.FromJson(originalJson);
|
||||
|
||||
// Act
|
||||
var serializedJson = link.ToJson(indented: true);
|
||||
var reparsedLink = InTotoLink.FromJson(serializedJson);
|
||||
|
||||
// Assert
|
||||
reparsedLink.Predicate.Name.Should().Be(link.Predicate.Name);
|
||||
reparsedLink.Predicate.Environment.Should().BeEquivalentTo(link.Predicate.Environment);
|
||||
reparsedLink.Predicate.ByProducts.Stdout.Should().Be(link.Predicate.ByProducts.Stdout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that golden links have the correct in-toto statement type.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("golden_scan_link.json")]
|
||||
[InlineData("golden_build_link.json")]
|
||||
public void GoldenLinks_ShouldHaveCorrectStatementType(string filename)
|
||||
{
|
||||
// Arrange
|
||||
var json = File.ReadAllText(Path.Combine(FixturesPath, filename));
|
||||
|
||||
// Act
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Assert
|
||||
root.GetProperty("_type").GetString().Should().Be("https://in-toto.io/Statement/v1");
|
||||
root.GetProperty("predicateType").GetString().Should().Be("https://in-toto.io/Link/v1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that golden links have required predicate fields per in-toto spec.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("golden_scan_link.json")]
|
||||
[InlineData("golden_build_link.json")]
|
||||
public void GoldenLinks_ShouldHaveRequiredPredicateFields(string filename)
|
||||
{
|
||||
// Arrange
|
||||
var json = File.ReadAllText(Path.Combine(FixturesPath, filename));
|
||||
|
||||
// Act
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var predicate = doc.RootElement.GetProperty("predicate");
|
||||
|
||||
// Assert - Required fields per in-toto Link predicate spec
|
||||
predicate.TryGetProperty("name", out _).Should().BeTrue("name is required");
|
||||
predicate.TryGetProperty("command", out _).Should().BeTrue("command is required");
|
||||
predicate.TryGetProperty("materials", out _).Should().BeTrue("materials is required");
|
||||
predicate.TryGetProperty("products", out _).Should().BeTrue("products is required");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that subjects match products (per in-toto link semantics).
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("golden_scan_link.json")]
|
||||
[InlineData("golden_build_link.json")]
|
||||
public void GoldenLinks_SubjectsShouldMatchProducts(string filename)
|
||||
{
|
||||
// Arrange
|
||||
var json = File.ReadAllText(Path.Combine(FixturesPath, filename));
|
||||
var link = InTotoLink.FromJson(json);
|
||||
|
||||
// Act & Assert
|
||||
// In in-toto, subjects are the products - they should have matching digests
|
||||
foreach (var subject in link.Subjects)
|
||||
{
|
||||
var matchingProduct = link.Predicate.Products
|
||||
.FirstOrDefault(p => p.Uri == subject.Name);
|
||||
|
||||
matchingProduct.Should().NotBeNull(
|
||||
$"Subject '{subject.Name}' should have a matching product");
|
||||
|
||||
if (matchingProduct is not null)
|
||||
{
|
||||
matchingProduct.Digest.Sha256.Should().Be(subject.Digest.Sha256,
|
||||
"Subject and product digests should match");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that all artifacts have valid digests.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("golden_scan_link.json")]
|
||||
[InlineData("golden_build_link.json")]
|
||||
public void GoldenLinks_AllArtifactsShouldHaveValidDigests(string filename)
|
||||
{
|
||||
// Arrange
|
||||
var json = File.ReadAllText(Path.Combine(FixturesPath, filename));
|
||||
var link = InTotoLink.FromJson(json);
|
||||
|
||||
// Act & Assert
|
||||
foreach (var material in link.Predicate.Materials)
|
||||
{
|
||||
material.Digest.HasDigest.Should().BeTrue(
|
||||
$"Material '{material.Uri}' should have a digest");
|
||||
material.Digest.Sha256.Should().MatchRegex("^[a-f0-9]{64}$",
|
||||
"SHA-256 digest should be 64 hex characters");
|
||||
}
|
||||
|
||||
foreach (var product in link.Predicate.Products)
|
||||
{
|
||||
product.Digest.HasDigest.Should().BeTrue(
|
||||
$"Product '{product.Uri}' should have a digest");
|
||||
product.Digest.Sha256.Should().MatchRegex("^[a-f0-9]{64}$",
|
||||
"SHA-256 digest should be 64 hex characters");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that byproducts have a return value.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("golden_scan_link.json", 0)]
|
||||
[InlineData("golden_build_link.json", 0)]
|
||||
public void GoldenLinks_ByProductsShouldHaveReturnValue(string filename, int expectedReturnValue)
|
||||
{
|
||||
// Arrange
|
||||
var json = File.ReadAllText(Path.Combine(FixturesPath, filename));
|
||||
var link = InTotoLink.FromJson(json);
|
||||
|
||||
// Act & Assert
|
||||
link.Predicate.ByProducts.ReturnValue.Should().Be(expectedReturnValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test golden layout fixture parsing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseGoldenLayout_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var json = File.ReadAllText(Path.Combine(FixturesPath, "golden_layout.json"));
|
||||
|
||||
// Act
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Assert
|
||||
root.GetProperty("steps").GetArrayLength().Should().Be(3);
|
||||
root.GetProperty("keys").EnumerateObject().Count().Should().Be(4);
|
||||
|
||||
var steps = root.GetProperty("steps").EnumerateArray().ToList();
|
||||
steps[0].GetProperty("name").GetString().Should().Be("build");
|
||||
steps[1].GetProperty("name").GetString().Should().Be("scan");
|
||||
steps[2].GetProperty("name").GetString().Should().Be("sign");
|
||||
|
||||
// Sign step should require threshold of 2
|
||||
steps[2].GetProperty("threshold").GetInt32().Should().Be(2);
|
||||
}
|
||||
}
|
||||
@@ -27,4 +27,11 @@
|
||||
<ProjectReference Include="..\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Copy fixture files to output directory -->
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*.*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user