292 lines
6.8 KiB
Markdown
292 lines
6.8 KiB
Markdown
# Determinism Gates
|
|
|
|
Determinism is a core principle of StellaOps - all artifact generation (SBOM, VEX, attestations) must be reproducible. This document describes how to test for determinism.
|
|
|
|
## Why Determinism Matters
|
|
|
|
- **Reproducible builds**: Same input → same output, always
|
|
- **Cryptographic verification**: Hash-based integrity depends on byte-for-byte reproducibility
|
|
- **Audit trails**: Deterministic timestamps and ordering for compliance
|
|
- **Offline operation**: No reliance on external randomness or timestamps
|
|
|
|
## Using Determinism Gates
|
|
|
|
Add `StellaOps.TestKit` to your test project and use the `DeterminismGate` class:
|
|
|
|
```csharp
|
|
using StellaOps.TestKit.Determinism;
|
|
using StellaOps.TestKit.Traits;
|
|
using Xunit;
|
|
|
|
public class SbomGeneratorTests
|
|
{
|
|
[Fact]
|
|
[UnitTest]
|
|
[DeterminismTest]
|
|
public void SbomGenerationIsDeterministic()
|
|
{
|
|
// Verify that calling the function 3 times produces identical output
|
|
DeterminismGate.AssertDeterministic(() =>
|
|
{
|
|
return GenerateSbom();
|
|
}, iterations: 3);
|
|
}
|
|
|
|
[Fact]
|
|
[UnitTest]
|
|
[DeterminismTest]
|
|
public void SbomBinaryIsDeterministic()
|
|
{
|
|
// Verify binary reproducibility
|
|
DeterminismGate.AssertDeterministic(() =>
|
|
{
|
|
return GenerateSbomBytes();
|
|
}, iterations: 3);
|
|
}
|
|
}
|
|
```
|
|
|
|
## JSON Determinism
|
|
|
|
JSON output must have:
|
|
- Stable property ordering (alphabetical or schema-defined)
|
|
- Consistent whitespace/formatting
|
|
- No random IDs or timestamps (unless explicitly from deterministic clock)
|
|
|
|
```csharp
|
|
[Fact]
|
|
[UnitTest]
|
|
[DeterminismTest]
|
|
public void VexDocumentJsonIsDeterministic()
|
|
{
|
|
// Verifies JSON canonicalization and property ordering
|
|
DeterminismGate.AssertJsonDeterministic(() =>
|
|
{
|
|
var vex = GenerateVexDocument();
|
|
return JsonSerializer.Serialize(vex);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
[UnitTest]
|
|
[DeterminismTest]
|
|
public void VerdictObjectIsDeterministic()
|
|
{
|
|
// Verifies object serialization is deterministic
|
|
DeterminismGate.AssertJsonDeterministic(() =>
|
|
{
|
|
return GenerateVerdict();
|
|
});
|
|
}
|
|
```
|
|
|
|
## Canonical Equality
|
|
|
|
Compare two objects for canonical equivalence:
|
|
|
|
```csharp
|
|
[Fact]
|
|
[UnitTest]
|
|
public void VerdictFromDifferentPathsAreCanonicallyEqual()
|
|
{
|
|
var verdict1 = GenerateVerdictFromSbom();
|
|
var verdict2 = GenerateVerdictFromCache();
|
|
|
|
// Asserts that both produce identical canonical JSON
|
|
DeterminismGate.AssertCanonicallyEqual(verdict1, verdict2);
|
|
}
|
|
```
|
|
|
|
## Hash-Based Regression Testing
|
|
|
|
Compute stable hashes for regression detection:
|
|
|
|
```csharp
|
|
[Fact]
|
|
[UnitTest]
|
|
[DeterminismTest]
|
|
public void SbomHashMatchesBaseline()
|
|
{
|
|
var sbom = GenerateSbom();
|
|
var hash = DeterminismGate.ComputeHash(sbom);
|
|
|
|
// This hash should NEVER change unless SBOM format changes intentionally
|
|
const string expectedHash = "abc123...";
|
|
Assert.Equal(expectedHash, hash);
|
|
}
|
|
```
|
|
|
|
## Path Ordering
|
|
|
|
File paths in manifests must be sorted:
|
|
|
|
```csharp
|
|
[Fact]
|
|
[UnitTest]
|
|
[DeterminismTest]
|
|
public void SbomFilePathsAreSorted()
|
|
{
|
|
var sbom = GenerateSbom();
|
|
var filePaths = ExtractFilePaths(sbom);
|
|
|
|
// Asserts paths are in deterministic (lexicographic) order
|
|
DeterminismGate.AssertSortedPaths(filePaths);
|
|
}
|
|
```
|
|
|
|
## Timestamp Validation
|
|
|
|
All timestamps must be UTC ISO 8601:
|
|
|
|
```csharp
|
|
[Fact]
|
|
[UnitTest]
|
|
[DeterminismTest]
|
|
public void AttestationTimestampIsUtcIso8601()
|
|
{
|
|
var attestation = GenerateAttestation();
|
|
|
|
// Asserts timestamp is UTC with 'Z' suffix
|
|
DeterminismGate.AssertUtcIso8601(attestation.Timestamp);
|
|
}
|
|
```
|
|
|
|
## Determin
|
|
|
|
istic Time in Tests
|
|
|
|
Use `DeterministicClock` for reproducible timestamps:
|
|
|
|
```csharp
|
|
using StellaOps.TestKit.Time;
|
|
|
|
[Fact]
|
|
[UnitTest]
|
|
[DeterminismTest]
|
|
public void AttestationWithDeterministicTime()
|
|
{
|
|
var clock = new DeterministicClock();
|
|
|
|
// All operations using this clock will get the same time
|
|
var attestation1 = GenerateAttestation(clock);
|
|
var attestation2 = GenerateAttestation(clock);
|
|
|
|
Assert.Equal(attestation1.Timestamp, attestation2.Timestamp);
|
|
}
|
|
```
|
|
|
|
## Deterministic Random in Tests
|
|
|
|
Use `DeterministicRandom` for reproducible randomness:
|
|
|
|
```csharp
|
|
using StellaOps.TestKit.Random;
|
|
|
|
[Fact]
|
|
[UnitTest]
|
|
[DeterminismTest]
|
|
public void GeneratedIdsAreReproducible()
|
|
{
|
|
var rng1 = DeterministicRandomExtensions.WithTestSeed();
|
|
var id1 = GenerateId(rng1);
|
|
|
|
var rng2 = DeterministicRandomExtensions.WithTestSeed();
|
|
var id2 = GenerateId(rng2);
|
|
|
|
// Same seed → same output
|
|
Assert.Equal(id1, id2);
|
|
}
|
|
```
|
|
|
|
## Module-Specific Gates
|
|
|
|
### Scanner Determinism
|
|
- SBOM file path ordering
|
|
- Component hash stability
|
|
- Dependency graph ordering
|
|
|
|
### Concelier Determinism
|
|
- Advisory normalization (same advisory → same canonical form)
|
|
- Vulnerability merge determinism
|
|
- No lattice ordering dependencies
|
|
|
|
### Excititor Determinism
|
|
- VEX document format stability
|
|
- Preserve/prune decision ordering
|
|
- No lattice dependencies
|
|
|
|
### Policy Determinism
|
|
- Verdict reproducibility (same inputs → same verdict)
|
|
- Policy evaluation ordering
|
|
- Unknown budget calculations
|
|
|
|
### Attestor Determinism
|
|
- DSSE envelope canonical bytes
|
|
- Signature ordering (multiple signers)
|
|
- Rekor receipt stability
|
|
|
|
## Common Determinism Violations
|
|
|
|
❌ **Timestamps from system clock**
|
|
```csharp
|
|
// Bad: Uses system time
|
|
var timestamp = DateTimeOffset.UtcNow.ToString("o");
|
|
|
|
// Good: Uses injected clock
|
|
var timestamp = clock.UtcNow.ToString("o");
|
|
```
|
|
|
|
❌ **Random GUIDs**
|
|
```csharp
|
|
// Bad: Non-deterministic
|
|
var id = Guid.NewGuid().ToString();
|
|
|
|
// Good: Deterministic or content-addressed
|
|
var id = ComputeContentHash(data);
|
|
```
|
|
|
|
❌ **Unordered collections**
|
|
```csharp
|
|
// Bad: Dictionary iteration order is undefined
|
|
foreach (var (key, value) in dict) { ... }
|
|
|
|
// Good: Explicit ordering
|
|
foreach (var (key, value) in dict.OrderBy(x => x.Key)) { ... }
|
|
```
|
|
|
|
❌ **Floating-point comparisons**
|
|
```csharp
|
|
// Bad: Floating-point can differ across platforms
|
|
var score = 0.1 + 0.2; // Might not equal 0.3 exactly
|
|
|
|
// Good: Use fixed-point or integers
|
|
var scoreInt = (int)((0.1 + 0.2) * 1000);
|
|
```
|
|
|
|
❌ **Non-UTC timestamps**
|
|
```csharp
|
|
// Bad: Timezone-dependent
|
|
var timestamp = DateTime.Now.ToString();
|
|
|
|
// Good: Always UTC with 'Z'
|
|
var timestamp = DateTimeOffset.UtcNow.ToString("o");
|
|
```
|
|
|
|
## Determinism Test Checklist
|
|
|
|
When writing determinism tests, verify:
|
|
|
|
- [ ] Multiple invocations produce identical output
|
|
- [ ] JSON has stable property ordering
|
|
- [ ] File paths are sorted lexicographically
|
|
- [ ] Timestamps are UTC ISO 8601 with 'Z' suffix
|
|
- [ ] No random GUIDs (use content-addressing)
|
|
- [ ] Collections are explicitly ordered
|
|
- [ ] No system time/random usage (use DeterministicClock/DeterministicRandom)
|
|
|
|
## Related Documentation
|
|
|
|
- TestKit README: `src/__Libraries/StellaOps.TestKit/README.md`
|
|
- Testing Strategy: `docs/technical/testing/testing-strategy-models.md`
|
|
- Test Catalog: `docs/technical/testing/TEST_CATALOG.yml`
|