audit, advisories and doctors/setup work
This commit is contained in:
@@ -32,12 +32,12 @@ public sealed class FixChainAttestationIntegrationTests
|
||||
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddLogging();
|
||||
services.Configure<FixChainOptions>(opts =>
|
||||
services.AddSingleton(Options.Create(new FixChainOptions
|
||||
{
|
||||
opts.AnalyzerName = "TestAnalyzer";
|
||||
opts.AnalyzerVersion = "1.0.0";
|
||||
opts.AnalyzerSourceDigest = "sha256:integrationtest";
|
||||
});
|
||||
AnalyzerName = "TestAnalyzer",
|
||||
AnalyzerVersion = "1.0.0",
|
||||
AnalyzerSourceDigest = "sha256:integrationtest"
|
||||
}));
|
||||
|
||||
services.AddFixChainAttestation();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Time.Testing" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -415,7 +415,7 @@ public sealed class FixChainValidatorTests
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().HaveCountGreaterOrEqualTo(3);
|
||||
result.Errors.Should().HaveCountGreaterThanOrEqualTo(3);
|
||||
}
|
||||
|
||||
private static FixChainPredicate CreateValidPredicate()
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests.BinaryDiff;
|
||||
|
||||
public sealed class BinaryDiffDsseSignerTests
|
||||
{
|
||||
private readonly EnvelopeSignatureService _signatureService = new();
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAndVerify_Succeeds()
|
||||
{
|
||||
var predicate = BinaryDiffTestData.CreatePredicate();
|
||||
var serializer = new BinaryDiffPredicateSerializer();
|
||||
var signer = new BinaryDiffDsseSigner(_signatureService, serializer);
|
||||
var verifier = new BinaryDiffDsseVerifier(_signatureService, serializer);
|
||||
var keys = BinaryDiffTestData.CreateDeterministicKeyPair("binarydiff-key");
|
||||
|
||||
var signResult = await signer.SignAsync(predicate, keys.Signer);
|
||||
var envelope = new DsseEnvelope(signResult.PayloadType, signResult.Payload, signResult.Signatures);
|
||||
|
||||
var verifyResult = verifier.Verify(envelope, keys.Verifier);
|
||||
|
||||
verifyResult.IsValid.Should().BeTrue();
|
||||
verifyResult.Predicate.Should().NotBeNull();
|
||||
verifyResult.VerifiedKeyId.Should().Be(keys.Signer.KeyId);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_Fails_OnTamperedPayload()
|
||||
{
|
||||
var predicate = BinaryDiffTestData.CreatePredicate();
|
||||
var serializer = new BinaryDiffPredicateSerializer();
|
||||
var signer = new BinaryDiffDsseSigner(_signatureService, serializer);
|
||||
var verifier = new BinaryDiffDsseVerifier(_signatureService, serializer);
|
||||
var keys = BinaryDiffTestData.CreateDeterministicKeyPair("binarydiff-key");
|
||||
|
||||
var signResult = await signer.SignAsync(predicate, keys.Signer);
|
||||
var tamperedPayload = signResult.Payload.ToArray();
|
||||
tamperedPayload[^1] ^= 0xFF;
|
||||
var envelope = new DsseEnvelope(signResult.PayloadType, tamperedPayload, signResult.Signatures);
|
||||
|
||||
var verifyResult = verifier.Verify(envelope, keys.Verifier);
|
||||
|
||||
verifyResult.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Verify_Fails_OnSchemaViolation()
|
||||
{
|
||||
var invalidJson = "{\"predicateType\":\"stellaops.binarydiff.v1\"}";
|
||||
var payload = System.Text.Encoding.UTF8.GetBytes(invalidJson);
|
||||
var keys = BinaryDiffTestData.CreateDeterministicKeyPair("binarydiff-key");
|
||||
var signResult = _signatureService.SignDsse(BinaryDiffPredicate.PredicateType, payload, keys.Signer);
|
||||
signResult.IsSuccess.Should().BeTrue();
|
||||
|
||||
var signature = DsseSignature.FromBytes(signResult.Value!.Value.Span, signResult.Value.KeyId);
|
||||
var envelope = new DsseEnvelope(BinaryDiffPredicate.PredicateType, payload, new[] { signature });
|
||||
var verifier = new BinaryDiffDsseVerifier(_signatureService, new BinaryDiffPredicateSerializer());
|
||||
|
||||
var verifyResult = verifier.Verify(envelope, keys.Verifier);
|
||||
|
||||
verifyResult.IsValid.Should().BeFalse();
|
||||
verifyResult.SchemaErrors.Should().NotBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests.BinaryDiff;
|
||||
|
||||
public sealed class BinaryDiffPredicateBuilderTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_RequiresSubject()
|
||||
{
|
||||
var options = Options.Create(new BinaryDiffOptions { ToolVersion = "1.0.0" });
|
||||
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.FixedTimeProvider);
|
||||
|
||||
builder.WithInputs(
|
||||
new BinaryDiffImageReference { Digest = "sha256:base" },
|
||||
new BinaryDiffImageReference { Digest = "sha256:target" });
|
||||
|
||||
Action act = () => builder.Build();
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*subject*");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_RequiresInputs()
|
||||
{
|
||||
var options = Options.Create(new BinaryDiffOptions { ToolVersion = "1.0.0" });
|
||||
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.FixedTimeProvider);
|
||||
|
||||
builder.WithSubject("docker://example/app@sha256:base", "sha256:aaaa");
|
||||
|
||||
Action act = () => builder.Build();
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*Inputs*");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_SortsFindingsAndSections()
|
||||
{
|
||||
var options = Options.Create(new BinaryDiffOptions { ToolVersion = "1.0.0" });
|
||||
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.FixedTimeProvider);
|
||||
|
||||
builder.WithSubject("docker://example/app@sha256:base", "sha256:aaaa")
|
||||
.WithInputs(
|
||||
new BinaryDiffImageReference { Digest = "sha256:base" },
|
||||
new BinaryDiffImageReference { Digest = "sha256:target" })
|
||||
.AddFinding(new BinaryDiffFinding
|
||||
{
|
||||
Path = "/z/libz.so",
|
||||
ChangeType = ChangeType.Modified,
|
||||
BinaryFormat = BinaryFormat.Elf,
|
||||
SectionDeltas = ImmutableArray.Create(
|
||||
new SectionDelta
|
||||
{
|
||||
Section = ".text",
|
||||
Status = SectionStatus.Modified
|
||||
},
|
||||
new SectionDelta
|
||||
{
|
||||
Section = ".bss",
|
||||
Status = SectionStatus.Added
|
||||
})
|
||||
})
|
||||
.AddFinding(new BinaryDiffFinding
|
||||
{
|
||||
Path = "/a/liba.so",
|
||||
ChangeType = ChangeType.Added,
|
||||
BinaryFormat = BinaryFormat.Elf,
|
||||
SectionDeltas = ImmutableArray.Create(
|
||||
new SectionDelta
|
||||
{
|
||||
Section = ".zlast",
|
||||
Status = SectionStatus.Added
|
||||
},
|
||||
new SectionDelta
|
||||
{
|
||||
Section = ".afirst",
|
||||
Status = SectionStatus.Added
|
||||
})
|
||||
})
|
||||
.WithMetadata(metadata => metadata.WithTotals(2, 1));
|
||||
|
||||
var predicate = builder.Build();
|
||||
|
||||
predicate.Findings[0].Path.Should().Be("/a/liba.so");
|
||||
predicate.Findings[1].Path.Should().Be("/z/libz.so");
|
||||
|
||||
predicate.Findings[0].SectionDeltas[0].Section.Should().Be(".afirst");
|
||||
predicate.Findings[0].SectionDeltas[1].Section.Should().Be(".zlast");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_UsesOptionsDefaults()
|
||||
{
|
||||
var options = Options.Create(new BinaryDiffOptions
|
||||
{
|
||||
ToolVersion = "2.0.0",
|
||||
ConfigDigest = "sha256:cfg",
|
||||
AnalyzedSections = [".z", ".a"]
|
||||
});
|
||||
|
||||
var builder = new BinaryDiffPredicateBuilder(options, BinaryDiffTestData.FixedTimeProvider);
|
||||
builder.WithSubject("docker://example/app@sha256:base", "sha256:aaaa")
|
||||
.WithInputs(
|
||||
new BinaryDiffImageReference { Digest = "sha256:base" },
|
||||
new BinaryDiffImageReference { Digest = "sha256:target" });
|
||||
|
||||
var predicate = builder.Build();
|
||||
|
||||
predicate.Metadata.ToolVersion.Should().Be("2.0.0");
|
||||
predicate.Metadata.ConfigDigest.Should().Be("sha256:cfg");
|
||||
predicate.Metadata.AnalysisTimestamp.Should().Be(BinaryDiffTestData.FixedTimeProvider.GetUtcNow());
|
||||
predicate.Metadata.AnalyzedSections.Should().Equal(".a", ".z");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests.BinaryDiff;
|
||||
|
||||
public sealed class BinaryDiffPredicateSerializerTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_IsDeterministic()
|
||||
{
|
||||
var predicate = BinaryDiffTestData.CreatePredicate();
|
||||
var serializer = new BinaryDiffPredicateSerializer();
|
||||
|
||||
var jsonA = serializer.Serialize(predicate);
|
||||
var jsonB = serializer.Serialize(predicate);
|
||||
|
||||
jsonA.Should().Be(jsonB);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_RoundTrip_ProducesEquivalentPredicate()
|
||||
{
|
||||
var predicate = BinaryDiffTestData.CreatePredicate();
|
||||
var serializer = new BinaryDiffPredicateSerializer();
|
||||
|
||||
var json = serializer.Serialize(predicate);
|
||||
var roundTrip = serializer.Deserialize(json);
|
||||
|
||||
roundTrip.Should().BeEquivalentTo(predicate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Json.Schema;
|
||||
using StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests.BinaryDiff;
|
||||
|
||||
public sealed class BinaryDiffSchemaValidationTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SchemaFile_ValidatesSamplePredicate()
|
||||
{
|
||||
var schema = LoadSchemaFromDocs();
|
||||
var predicate = BinaryDiffTestData.CreatePredicate();
|
||||
var serializer = new BinaryDiffPredicateSerializer();
|
||||
var json = serializer.Serialize(predicate);
|
||||
using var document = JsonDocument.Parse(json);
|
||||
|
||||
var result = schema.Evaluate(document.RootElement, new EvaluationOptions
|
||||
{
|
||||
OutputFormat = OutputFormat.List,
|
||||
RequireFormatValidation = true
|
||||
});
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InlineSchema_RejectsMissingRequiredFields()
|
||||
{
|
||||
using var document = JsonDocument.Parse("{\"predicateType\":\"stellaops.binarydiff.v1\"}");
|
||||
var result = BinaryDiffSchema.Validate(document.RootElement);
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
private static JsonSchema LoadSchemaFromDocs()
|
||||
{
|
||||
var root = FindRepoRoot();
|
||||
var schemaPath = Path.Combine(root, "docs", "schemas", "binarydiff-v1.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,138 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests.BinaryDiff;
|
||||
|
||||
internal static class BinaryDiffTestData
|
||||
{
|
||||
internal static readonly TimeProvider FixedTimeProvider =
|
||||
new FixedTimeProvider(new DateTimeOffset(2026, 1, 13, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
internal static BinaryDiffPredicate CreatePredicate()
|
||||
{
|
||||
var options = Options.Create(new BinaryDiffOptions
|
||||
{
|
||||
ToolVersion = "1.0.0",
|
||||
ConfigDigest = "sha256:config",
|
||||
AnalyzedSections = [".text", ".rodata", ".data"]
|
||||
});
|
||||
|
||||
var builder = new BinaryDiffPredicateBuilder(options, FixedTimeProvider);
|
||||
builder.WithSubject("docker://example/app@sha256:base", "sha256:aaaaaaaa")
|
||||
.WithInputs(
|
||||
new BinaryDiffImageReference
|
||||
{
|
||||
Digest = "sha256:base",
|
||||
Reference = "docker://example/app:base"
|
||||
},
|
||||
new BinaryDiffImageReference
|
||||
{
|
||||
Digest = "sha256:target",
|
||||
Reference = "docker://example/app:target"
|
||||
})
|
||||
.AddFinding(new BinaryDiffFinding
|
||||
{
|
||||
Path = "/usr/lib/libssl.so.3",
|
||||
ChangeType = ChangeType.Modified,
|
||||
BinaryFormat = BinaryFormat.Elf,
|
||||
LayerDigest = "sha256:layer1",
|
||||
BaseHashes = new SectionHashSet
|
||||
{
|
||||
BuildId = "buildid-base",
|
||||
FileHash = "sha256:file-base",
|
||||
Sections = ImmutableDictionary.CreateRange(
|
||||
StringComparer.Ordinal,
|
||||
new[]
|
||||
{
|
||||
new KeyValuePair<string, SectionInfo>(".text", new SectionInfo
|
||||
{
|
||||
Sha256 = "sha256:text-base",
|
||||
Size = 1024
|
||||
}),
|
||||
new KeyValuePair<string, SectionInfo>(".rodata", new SectionInfo
|
||||
{
|
||||
Sha256 = "sha256:rodata-base",
|
||||
Size = 512
|
||||
})
|
||||
})
|
||||
},
|
||||
TargetHashes = new SectionHashSet
|
||||
{
|
||||
BuildId = "buildid-target",
|
||||
FileHash = "sha256:file-target",
|
||||
Sections = ImmutableDictionary.CreateRange(
|
||||
StringComparer.Ordinal,
|
||||
new[]
|
||||
{
|
||||
new KeyValuePair<string, SectionInfo>(".text", new SectionInfo
|
||||
{
|
||||
Sha256 = "sha256:text-target",
|
||||
Size = 1200
|
||||
}),
|
||||
new KeyValuePair<string, SectionInfo>(".rodata", new SectionInfo
|
||||
{
|
||||
Sha256 = "sha256:rodata-target",
|
||||
Size = 512
|
||||
})
|
||||
})
|
||||
},
|
||||
SectionDeltas = ImmutableArray.Create(
|
||||
new SectionDelta
|
||||
{
|
||||
Section = ".text",
|
||||
Status = SectionStatus.Modified,
|
||||
BaseSha256 = "sha256:text-base",
|
||||
TargetSha256 = "sha256:text-target",
|
||||
SizeDelta = 176
|
||||
},
|
||||
new SectionDelta
|
||||
{
|
||||
Section = ".rodata",
|
||||
Status = SectionStatus.Identical,
|
||||
BaseSha256 = "sha256:rodata-base",
|
||||
TargetSha256 = "sha256:rodata-target",
|
||||
SizeDelta = 0
|
||||
}),
|
||||
Confidence = 0.9,
|
||||
Verdict = Verdict.Patched
|
||||
})
|
||||
.WithMetadata(metadata => metadata.WithTotals(1, 1));
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
internal static BinaryDiffKeyPair CreateDeterministicKeyPair(string keyId)
|
||||
{
|
||||
var seed = new byte[32];
|
||||
for (var i = 0; i < seed.Length; i++)
|
||||
{
|
||||
seed[i] = (byte)(i + 1);
|
||||
}
|
||||
|
||||
var privateKeyParameters = new Ed25519PrivateKeyParameters(seed, 0);
|
||||
var publicKeyParameters = privateKeyParameters.GeneratePublicKey();
|
||||
var publicKey = publicKeyParameters.GetEncoded();
|
||||
var privateKey = privateKeyParameters.GetEncoded();
|
||||
|
||||
var signer = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey, keyId);
|
||||
var verifier = EnvelopeKey.CreateEd25519Verifier(publicKey, keyId);
|
||||
return new BinaryDiffKeyPair(signer, verifier);
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixedTime;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset fixedTime)
|
||||
{
|
||||
_fixedTime = fixedTime;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record BinaryDiffKeyPair(EnvelopeKey Signer, EnvelopeKey Verifier);
|
||||
@@ -1,10 +1,11 @@
|
||||
# Attestor StandardPredicates Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
Source of truth: `docs/implplan/SPRINT_20260113_001_002_ATTESTOR_binary_diff_predicate.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0065-M | DONE | Revalidated 2026-01-06. |
|
||||
| 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. |
|
||||
|
||||
Reference in New Issue
Block a user