up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-14 15:50:38 +02:00
parent f1a39c4ce3
commit 233873f620
249 changed files with 29746 additions and 154 deletions

View File

@@ -0,0 +1,606 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Engine.Gates;
using StellaOps.Policy.Engine.ReachabilityFacts;
using StellaOps.Policy.Engine.Vex;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Vex;
public class VexDecisionEmitterTests
{
private const string TestTenantId = "test-tenant";
private const string TestVulnId = "CVE-2021-44228";
private const string TestPurl = "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1";
[Fact]
public async Task EmitAsync_WithUnreachableFact_EmitsNotAffected()
{
// Arrange
var fact = CreateFact(ReachabilityState.Unreachable, hasRuntime: true, confidence: 0.95m);
var factsService = CreateMockFactsService(fact);
var gateEvaluator = CreateMockGateEvaluator(PolicyGateDecisionType.Allow);
var emitter = CreateEmitter(factsService, gateEvaluator);
var request = new VexDecisionEmitRequest
{
TenantId = TestTenantId,
Author = "test@example.com",
Findings = new[]
{
new VexFindingInput { VulnId = TestVulnId, Purl = TestPurl }
}
};
// Act
var result = await emitter.EmitAsync(request);
// Assert
Assert.NotNull(result.Document);
Assert.Single(result.Document.Statements);
var statement = result.Document.Statements[0];
Assert.Equal("not_affected", statement.Status);
Assert.Equal(VexJustification.VulnerableCodeNotInExecutePath, statement.Justification);
Assert.Empty(result.Blocked);
}
[Fact]
public async Task EmitAsync_WithReachableFact_EmitsAffected()
{
// Arrange
var fact = CreateFact(ReachabilityState.Reachable, hasRuntime: true, confidence: 0.9m);
var factsService = CreateMockFactsService(fact);
var gateEvaluator = CreateMockGateEvaluator(PolicyGateDecisionType.Allow);
var emitter = CreateEmitter(factsService, gateEvaluator);
var request = new VexDecisionEmitRequest
{
TenantId = TestTenantId,
Author = "test@example.com",
Findings = new[]
{
new VexFindingInput { VulnId = TestVulnId, Purl = TestPurl }
}
};
// Act
var result = await emitter.EmitAsync(request);
// Assert
Assert.NotNull(result.Document);
Assert.Single(result.Document.Statements);
var statement = result.Document.Statements[0];
Assert.Equal("affected", statement.Status);
Assert.Null(statement.Justification);
Assert.Empty(result.Blocked);
}
[Fact]
public async Task EmitAsync_WithUnknownFact_EmitsUnderInvestigation()
{
// Arrange
var fact = CreateFact(ReachabilityState.Unknown, hasRuntime: false, confidence: 0.0m);
var factsService = CreateMockFactsService(fact);
var gateEvaluator = CreateMockGateEvaluator(PolicyGateDecisionType.Allow);
var emitter = CreateEmitter(factsService, gateEvaluator);
var request = new VexDecisionEmitRequest
{
TenantId = TestTenantId,
Author = "test@example.com",
Findings = new[]
{
new VexFindingInput { VulnId = TestVulnId, Purl = TestPurl }
}
};
// Act
var result = await emitter.EmitAsync(request);
// Assert
Assert.NotNull(result.Document);
Assert.Single(result.Document.Statements);
var statement = result.Document.Statements[0];
Assert.Equal("under_investigation", statement.Status);
Assert.Empty(result.Blocked);
}
[Fact]
public async Task EmitAsync_WhenGateBlocks_FallsBackToUnderInvestigation()
{
// Arrange
var fact = CreateFact(ReachabilityState.Unreachable, hasRuntime: false, confidence: 0.5m);
var factsService = CreateMockFactsService(fact);
var gateEvaluator = CreateMockGateEvaluator(PolicyGateDecisionType.Block, blockedBy: "EvidenceCompleteness", reason: "graphHash required");
var emitter = CreateEmitter(factsService, gateEvaluator);
var request = new VexDecisionEmitRequest
{
TenantId = TestTenantId,
Author = "test@example.com",
Findings = new[]
{
new VexFindingInput { VulnId = TestVulnId, Purl = TestPurl }
}
};
// Act
var result = await emitter.EmitAsync(request);
// Assert
Assert.Single(result.Blocked);
Assert.Equal(TestVulnId, result.Blocked[0].VulnId);
Assert.Equal("EvidenceCompleteness", result.Blocked[0].BlockedBy);
// With FallbackToUnderInvestigation=true (default), still emits under_investigation
Assert.Single(result.Document.Statements);
Assert.Equal("under_investigation", result.Document.Statements[0].Status);
}
[Fact]
public async Task EmitAsync_WithOverride_UsesOverrideStatus()
{
// Arrange
var fact = CreateFact(ReachabilityState.Reachable, hasRuntime: true, confidence: 0.9m);
var factsService = CreateMockFactsService(fact);
var gateEvaluator = CreateMockGateEvaluator(PolicyGateDecisionType.Allow);
var emitter = CreateEmitter(factsService, gateEvaluator);
var request = new VexDecisionEmitRequest
{
TenantId = TestTenantId,
Author = "test@example.com",
Findings = new[]
{
new VexFindingInput
{
VulnId = TestVulnId,
Purl = TestPurl,
OverrideStatus = "not_affected",
OverrideJustification = "Manual review confirmed unreachable"
}
}
};
// Act
var result = await emitter.EmitAsync(request);
// Assert
Assert.Single(result.Document.Statements);
Assert.Equal("not_affected", result.Document.Statements[0].Status);
}
[Fact]
public async Task EmitAsync_IncludesEvidenceBlock()
{
// Arrange
var fact = CreateFact(ReachabilityState.Unreachable, hasRuntime: true, confidence: 0.95m);
fact = fact with
{
EvidenceHash = "blake3:abc123",
Metadata = new Dictionary<string, object?>
{
["call_path"] = new List<object> { "main", "svc", "target" },
["entry_points"] = new List<object> { "main" },
["runtime_hits"] = new List<object> { "main", "svc" },
["uncertainty_tier"] = "T3",
["risk_score"] = 0.25
}
};
var factsService = CreateMockFactsService(fact);
var gateEvaluator = CreateMockGateEvaluator(PolicyGateDecisionType.Allow);
var emitter = CreateEmitter(factsService, gateEvaluator);
var request = new VexDecisionEmitRequest
{
TenantId = TestTenantId,
Author = "test@example.com",
IncludeEvidence = true,
Findings = new[]
{
new VexFindingInput { VulnId = TestVulnId, Purl = TestPurl }
}
};
// Act
var result = await emitter.EmitAsync(request);
// Assert
var statement = result.Document.Statements[0];
Assert.NotNull(statement.Evidence);
Assert.Equal("CU", statement.Evidence.LatticeState);
Assert.Equal(0.95, statement.Evidence.Confidence);
Assert.Equal("blake3:abc123", statement.Evidence.GraphHash);
Assert.Equal("T3", statement.Evidence.UncertaintyTier);
Assert.Equal(0.25, statement.Evidence.RiskScore);
Assert.NotNull(statement.Evidence.CallPath);
Assert.Equal(new[] { "main", "svc", "target" }, statement.Evidence.CallPath.Value.ToArray());
}
[Fact]
public async Task EmitAsync_WithMultipleFindings_EmitsMultipleStatements()
{
// Arrange
var facts = new Dictionary<ReachabilityFactKey, ReachabilityFact>
{
[new(TestTenantId, TestPurl, "CVE-2021-44228")] = CreateFact(ReachabilityState.Unreachable, hasRuntime: true, confidence: 0.95m, vulnId: "CVE-2021-44228"),
[new(TestTenantId, "pkg:npm/lodash@4.17.20", "CVE-2021-23337")] = CreateFact(ReachabilityState.Reachable, hasRuntime: false, confidence: 0.8m, vulnId: "CVE-2021-23337", purl: "pkg:npm/lodash@4.17.20")
};
var factsService = CreateMockFactsService(facts);
var gateEvaluator = CreateMockGateEvaluator(PolicyGateDecisionType.Allow);
var emitter = CreateEmitter(factsService, gateEvaluator);
var request = new VexDecisionEmitRequest
{
TenantId = TestTenantId,
Author = "test@example.com",
Findings = new[]
{
new VexFindingInput { VulnId = "CVE-2021-44228", Purl = TestPurl },
new VexFindingInput { VulnId = "CVE-2021-23337", Purl = "pkg:npm/lodash@4.17.20" }
}
};
// Act
var result = await emitter.EmitAsync(request);
// Assert
Assert.Equal(2, result.Document.Statements.Length);
Assert.Contains(result.Document.Statements, s => s.Status == "not_affected");
Assert.Contains(result.Document.Statements, s => s.Status == "affected");
}
[Fact]
public async Task EmitAsync_DocumentHasCorrectMetadata()
{
// Arrange
var factsService = CreateMockFactsService((ReachabilityFact?)null);
var gateEvaluator = CreateMockGateEvaluator(PolicyGateDecisionType.Allow);
var emitter = CreateEmitter(factsService, gateEvaluator);
var request = new VexDecisionEmitRequest
{
TenantId = TestTenantId,
Author = "security-team@company.com",
Findings = new[]
{
new VexFindingInput { VulnId = TestVulnId, Purl = TestPurl }
}
};
// Act
var result = await emitter.EmitAsync(request);
// Assert
Assert.StartsWith("urn:uuid:", result.Document.Id);
Assert.Equal("https://openvex.dev/ns/v0.2.0", result.Document.Context);
Assert.Equal("security-team@company.com", result.Document.Author);
Assert.Equal("policy_engine", result.Document.Role);
Assert.Equal("stellaops/policy-engine", result.Document.Tooling);
Assert.Equal(1, result.Document.Version);
}
[Fact]
public async Task DetermineStatusAsync_ReturnsCorrectBucket()
{
// Arrange
var fact = CreateFact(ReachabilityState.Reachable, hasRuntime: true, confidence: 0.9m);
var factsService = CreateMockFactsService(fact);
var gateEvaluator = CreateMockGateEvaluator(PolicyGateDecisionType.Allow);
var emitter = CreateEmitter(factsService, gateEvaluator);
// Act
var determination = await emitter.DetermineStatusAsync(TestTenantId, TestVulnId, TestPurl);
// Assert
Assert.Equal("affected", determination.Status);
Assert.Equal("runtime", determination.Bucket);
Assert.Equal("CR", determination.LatticeState);
Assert.Equal(0.9, determination.Confidence);
Assert.NotNull(determination.Fact);
}
[Fact]
public async Task EmitAsync_WithSymbolId_IncludesSubcomponent()
{
// Arrange
var factsService = CreateMockFactsService((ReachabilityFact?)null);
var gateEvaluator = CreateMockGateEvaluator(PolicyGateDecisionType.Allow);
var emitter = CreateEmitter(factsService, gateEvaluator);
var request = new VexDecisionEmitRequest
{
TenantId = TestTenantId,
Author = "test@example.com",
Findings = new[]
{
new VexFindingInput
{
VulnId = TestVulnId,
Purl = TestPurl,
SymbolId = "org.apache.logging.log4j.core.lookup.JndiLookup.lookup"
}
}
};
// Act
var result = await emitter.EmitAsync(request);
// Assert
var statement = result.Document.Statements[0];
Assert.NotNull(statement.Products[0].Subcomponents);
var subcomponents = statement.Products[0].Subcomponents!.Value;
Assert.Single(subcomponents);
Assert.Equal("org.apache.logging.log4j.core.lookup.JndiLookup.lookup", subcomponents[0].Id);
}
private static ReachabilityFact CreateFact(
ReachabilityState state,
bool hasRuntime,
decimal confidence,
string? vulnId = null,
string? purl = null)
{
return new ReachabilityFact
{
Id = Guid.NewGuid().ToString("N"),
TenantId = TestTenantId,
ComponentPurl = purl ?? TestPurl,
AdvisoryId = vulnId ?? TestVulnId,
State = state,
Confidence = confidence,
Score = (decimal)(hasRuntime ? 0.9 : 0.5),
HasRuntimeEvidence = hasRuntime,
Source = "stellaops/signals",
Method = hasRuntime ? AnalysisMethod.Hybrid : AnalysisMethod.Static,
ComputedAt = DateTimeOffset.UtcNow,
EvidenceRef = "cas://reachability/graphs/test"
};
}
private static ReachabilityFactsJoiningService CreateMockFactsService(ReachabilityFact? fact)
{
var facts = new Dictionary<ReachabilityFactKey, ReachabilityFact>();
if (fact is not null)
{
facts[new(fact.TenantId, fact.ComponentPurl, fact.AdvisoryId)] = fact;
}
return CreateMockFactsService(facts);
}
private static ReachabilityFactsJoiningService CreateMockFactsService(Dictionary<ReachabilityFactKey, ReachabilityFact> facts)
{
var store = new InMemoryReachabilityFactsStore(facts);
var cache = new InMemoryReachabilityFactsOverlayCache();
return new ReachabilityFactsJoiningService(
store,
cache,
NullLogger<ReachabilityFactsJoiningService>.Instance,
TimeProvider.System);
}
private static IPolicyGateEvaluator CreateMockGateEvaluator(
PolicyGateDecisionType decision,
string? blockedBy = null,
string? reason = null)
{
return new MockPolicyGateEvaluator(decision, blockedBy, reason);
}
private static VexDecisionEmitter CreateEmitter(
ReachabilityFactsJoiningService factsService,
IPolicyGateEvaluator gateEvaluator)
{
var options = new TestOptionsMonitor<VexDecisionEmitterOptions>(new VexDecisionEmitterOptions());
return new VexDecisionEmitter(
factsService,
gateEvaluator,
options,
TimeProvider.System,
NullLogger<VexDecisionEmitter>.Instance);
}
private sealed class TestOptionsMonitor<T> : IOptionsMonitor<T>
{
public TestOptionsMonitor(T currentValue)
{
CurrentValue = currentValue;
}
public T CurrentValue { get; }
public T Get(string? name) => CurrentValue;
public IDisposable? OnChange(Action<T, string?> listener) => null;
}
private sealed class InMemoryReachabilityFactsStore : IReachabilityFactsStore
{
private readonly Dictionary<ReachabilityFactKey, ReachabilityFact> _facts;
public InMemoryReachabilityFactsStore(Dictionary<ReachabilityFactKey, ReachabilityFact> facts)
{
_facts = facts;
}
public Task<ReachabilityFact?> GetAsync(string tenantId, string componentPurl, string advisoryId, CancellationToken cancellationToken = default)
{
var key = new ReachabilityFactKey(tenantId, componentPurl, advisoryId);
_facts.TryGetValue(key, out var fact);
return Task.FromResult(fact);
}
public Task<IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact>> GetBatchAsync(IReadOnlyList<ReachabilityFactKey> keys, CancellationToken cancellationToken = default)
{
var result = new Dictionary<ReachabilityFactKey, ReachabilityFact>();
foreach (var key in keys)
{
if (_facts.TryGetValue(key, out var fact))
{
result[key] = fact;
}
}
return Task.FromResult<IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact>>(result);
}
public Task<IReadOnlyList<ReachabilityFact>> QueryAsync(ReachabilityFactsQuery query, CancellationToken cancellationToken = default)
{
var results = _facts.Values
.Where(f => f.TenantId == query.TenantId)
.Where(f => query.ComponentPurls == null || query.ComponentPurls.Contains(f.ComponentPurl))
.Where(f => query.AdvisoryIds == null || query.AdvisoryIds.Contains(f.AdvisoryId))
.Where(f => query.States == null || query.States.Contains(f.State))
.Where(f => !query.MinConfidence.HasValue || f.Confidence >= query.MinConfidence.Value)
.Skip(query.Skip)
.Take(query.Limit)
.ToList();
return Task.FromResult<IReadOnlyList<ReachabilityFact>>(results);
}
public Task SaveAsync(ReachabilityFact fact, CancellationToken cancellationToken = default)
{
var key = new ReachabilityFactKey(fact.TenantId, fact.ComponentPurl, fact.AdvisoryId);
_facts[key] = fact;
return Task.CompletedTask;
}
public Task SaveBatchAsync(IReadOnlyList<ReachabilityFact> facts, CancellationToken cancellationToken = default)
{
foreach (var fact in facts)
{
var key = new ReachabilityFactKey(fact.TenantId, fact.ComponentPurl, fact.AdvisoryId);
_facts[key] = fact;
}
return Task.CompletedTask;
}
public Task DeleteAsync(string tenantId, string componentPurl, string advisoryId, CancellationToken cancellationToken = default)
{
var key = new ReachabilityFactKey(tenantId, componentPurl, advisoryId);
_facts.Remove(key);
return Task.CompletedTask;
}
public Task<long> CountAsync(string tenantId, CancellationToken cancellationToken = default)
{
var count = _facts.Values.Count(f => f.TenantId == tenantId);
return Task.FromResult((long)count);
}
}
private sealed class InMemoryReachabilityFactsOverlayCache : IReachabilityFactsOverlayCache
{
private readonly Dictionary<ReachabilityFactKey, ReachabilityFact> _cache = new();
public Task<(ReachabilityFact? Fact, bool CacheHit)> GetAsync(ReachabilityFactKey key, CancellationToken cancellationToken = default)
{
if (_cache.TryGetValue(key, out var fact))
{
return Task.FromResult<(ReachabilityFact?, bool)>((fact, true));
}
return Task.FromResult<(ReachabilityFact?, bool)>((null, false));
}
public Task<ReachabilityFactsBatch> GetBatchAsync(IReadOnlyList<ReachabilityFactKey> keys, CancellationToken cancellationToken = default)
{
var found = new Dictionary<ReachabilityFactKey, ReachabilityFact>();
var notFound = new List<ReachabilityFactKey>();
foreach (var key in keys)
{
if (_cache.TryGetValue(key, out var fact))
{
found[key] = fact;
}
else
{
notFound.Add(key);
}
}
return Task.FromResult(new ReachabilityFactsBatch
{
Found = found,
NotFound = notFound,
CacheHits = found.Count,
CacheMisses = notFound.Count
});
}
public Task SetAsync(ReachabilityFactKey key, ReachabilityFact fact, CancellationToken cancellationToken = default)
{
_cache[key] = fact;
return Task.CompletedTask;
}
public Task SetBatchAsync(IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact> facts, CancellationToken cancellationToken = default)
{
foreach (var (key, fact) in facts)
{
_cache[key] = fact;
}
return Task.CompletedTask;
}
public Task InvalidateAsync(ReachabilityFactKey key, CancellationToken cancellationToken = default)
{
_cache.Remove(key);
return Task.CompletedTask;
}
public Task InvalidateTenantAsync(string tenantId, CancellationToken cancellationToken = default)
{
var keysToRemove = _cache.Keys.Where(k => k.TenantId == tenantId).ToList();
foreach (var key in keysToRemove)
{
_cache.Remove(key);
}
return Task.CompletedTask;
}
public ReachabilityFactsCacheStats GetStats()
{
return new ReachabilityFactsCacheStats { ItemCount = _cache.Count };
}
}
private sealed class MockPolicyGateEvaluator : IPolicyGateEvaluator
{
private readonly PolicyGateDecisionType _decision;
private readonly string? _blockedBy;
private readonly string? _reason;
public MockPolicyGateEvaluator(PolicyGateDecisionType decision, string? blockedBy, string? reason)
{
_decision = decision;
_blockedBy = blockedBy;
_reason = reason;
}
public Task<PolicyGateDecision> EvaluateAsync(PolicyGateRequest request, CancellationToken cancellationToken = default)
{
return Task.FromResult(new PolicyGateDecision
{
GateId = $"gate:vex:{request.RequestedStatus}:{DateTimeOffset.UtcNow:O}",
RequestedStatus = request.RequestedStatus,
Subject = new PolicyGateSubject
{
VulnId = request.VulnId,
Purl = request.Purl
},
Evidence = new PolicyGateEvidence
{
LatticeState = request.LatticeState,
Confidence = request.Confidence
},
Gates = ImmutableArray<PolicyGateResult>.Empty,
Decision = _decision,
BlockedBy = _decision == PolicyGateDecisionType.Block ? _blockedBy : null,
BlockReason = _decision == PolicyGateDecisionType.Block ? _reason : null,
DecidedAt = DateTimeOffset.UtcNow
});
}
}
}

View File

@@ -0,0 +1,470 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using StellaOps.Policy.Engine.Vex;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Vex;
public sealed class VexDecisionSigningServiceTests
{
private readonly Mock<IVexSignerClient> _mockSignerClient;
private readonly Mock<IVexRekorClient> _mockRekorClient;
private readonly VexSigningOptions _options;
private readonly VexDecisionSigningService _service;
public VexDecisionSigningServiceTests()
{
_mockSignerClient = new Mock<IVexSignerClient>();
_mockRekorClient = new Mock<IVexRekorClient>();
_options = new VexSigningOptions
{
UseSignerService = true,
RekorEnabled = true
};
var optionsMonitor = new Mock<IOptionsMonitor<VexSigningOptions>>();
optionsMonitor.Setup(o => o.CurrentValue).Returns(_options);
_service = new VexDecisionSigningService(
_mockSignerClient.Object,
_mockRekorClient.Object,
optionsMonitor.Object,
TimeProvider.System,
NullLogger<VexDecisionSigningService>.Instance);
}
[Fact]
public async Task SignAsync_WithSignerService_ReturnsEnvelope()
{
var document = CreateTestDocument();
var request = CreateSigningRequest(document);
_mockSignerClient.Setup(c => c.SignAsync(It.IsAny<VexSignerRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResult
{
Success = true,
Signature = Convert.ToBase64String(new byte[32]),
KeyId = "test-key"
});
var result = await _service.SignAsync(request);
Assert.True(result.Success);
Assert.NotNull(result.Envelope);
Assert.NotNull(result.EnvelopeDigest);
Assert.StartsWith("sha256:", result.EnvelopeDigest);
}
[Fact]
public async Task SignAsync_WithSignerServiceFailure_ReturnsFailed()
{
var document = CreateTestDocument();
var request = CreateSigningRequest(document);
_mockSignerClient.Setup(c => c.SignAsync(It.IsAny<VexSignerRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResult
{
Success = false,
Error = "Signing failed"
});
var result = await _service.SignAsync(request);
Assert.False(result.Success);
Assert.Null(result.Envelope);
Assert.Contains("Signing failed", result.Error);
}
[Fact]
public async Task SignAsync_WithLocalSigning_ReturnsEnvelope()
{
var localOptions = new VexSigningOptions
{
UseSignerService = false,
RekorEnabled = false
};
var optionsMonitor = new Mock<IOptionsMonitor<VexSigningOptions>>();
optionsMonitor.Setup(o => o.CurrentValue).Returns(localOptions);
var service = new VexDecisionSigningService(
null,
null,
optionsMonitor.Object,
TimeProvider.System,
NullLogger<VexDecisionSigningService>.Instance);
var document = CreateTestDocument();
var request = CreateSigningRequest(document, submitToRekor: false);
var result = await service.SignAsync(request);
Assert.True(result.Success);
Assert.NotNull(result.Envelope);
Assert.Single(result.Envelope.Signatures);
Assert.Equal("local:sha256", result.Envelope.Signatures[0].KeyId);
}
[Fact]
public async Task SignAsync_WithRekorEnabled_SubmitsToRekor()
{
var document = CreateTestDocument();
var request = CreateSigningRequest(document, submitToRekor: true);
_mockSignerClient.Setup(c => c.SignAsync(It.IsAny<VexSignerRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResult
{
Success = true,
Signature = Convert.ToBase64String(new byte[32]),
KeyId = "test-key"
});
_mockRekorClient.Setup(c => c.SubmitAsync(It.IsAny<VexRekorSubmitRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexRekorSubmitResult
{
Success = true,
Metadata = new VexRekorMetadata
{
Uuid = "rekor-uuid-123",
Index = 12345,
LogUrl = "https://rekor.sigstore.dev",
IntegratedAt = DateTimeOffset.UtcNow
}
});
var result = await _service.SignAsync(request);
Assert.True(result.Success);
Assert.NotNull(result.RekorMetadata);
Assert.Equal("rekor-uuid-123", result.RekorMetadata.Uuid);
Assert.Equal(12345, result.RekorMetadata.Index);
}
[Fact]
public async Task SignAsync_WithRekorFailure_StillSucceeds()
{
var document = CreateTestDocument();
var request = CreateSigningRequest(document, submitToRekor: true);
_mockSignerClient.Setup(c => c.SignAsync(It.IsAny<VexSignerRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResult
{
Success = true,
Signature = Convert.ToBase64String(new byte[32]),
KeyId = "test-key"
});
_mockRekorClient.Setup(c => c.SubmitAsync(It.IsAny<VexRekorSubmitRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexRekorSubmitResult
{
Success = false,
Error = "Rekor unavailable"
});
var result = await _service.SignAsync(request);
Assert.True(result.Success);
Assert.NotNull(result.Envelope);
Assert.Null(result.RekorMetadata);
}
[Fact]
public async Task SignAsync_WithRekorDisabled_DoesNotSubmit()
{
var disabledOptions = new VexSigningOptions
{
UseSignerService = true,
RekorEnabled = false
};
var optionsMonitor = new Mock<IOptionsMonitor<VexSigningOptions>>();
optionsMonitor.Setup(o => o.CurrentValue).Returns(disabledOptions);
var service = new VexDecisionSigningService(
_mockSignerClient.Object,
_mockRekorClient.Object,
optionsMonitor.Object,
TimeProvider.System,
NullLogger<VexDecisionSigningService>.Instance);
var document = CreateTestDocument();
var request = CreateSigningRequest(document, submitToRekor: true);
_mockSignerClient.Setup(c => c.SignAsync(It.IsAny<VexSignerRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResult
{
Success = true,
Signature = Convert.ToBase64String(new byte[32]),
KeyId = "test-key"
});
var result = await service.SignAsync(request);
Assert.True(result.Success);
Assert.Null(result.RekorMetadata);
_mockRekorClient.Verify(c => c.SubmitAsync(It.IsAny<VexRekorSubmitRequest>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task SignAsync_SetsCorrectPayloadType()
{
var document = CreateTestDocument();
var request = CreateSigningRequest(document);
VexSignerRequest? capturedRequest = null;
_mockSignerClient.Setup(c => c.SignAsync(It.IsAny<VexSignerRequest>(), It.IsAny<CancellationToken>()))
.Callback<VexSignerRequest, CancellationToken>((req, _) => capturedRequest = req)
.ReturnsAsync(new VexSignerResult
{
Success = true,
Signature = Convert.ToBase64String(new byte[32]),
KeyId = "test-key"
});
await _service.SignAsync(request);
Assert.NotNull(capturedRequest);
Assert.Equal(VexPredicateTypes.VexDecision, capturedRequest.PayloadType);
}
// Verification Tests
[Fact]
public async Task VerifyAsync_WithValidEnvelope_ReturnsValid()
{
var document = CreateTestDocument();
var payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(document);
var envelope = new VexDsseEnvelope
{
PayloadType = VexPredicateTypes.VexDecision,
Payload = Convert.ToBase64String(payload),
Signatures = [new VexDsseSignature { KeyId = "test", Sig = Convert.ToBase64String(new byte[32]) }]
};
var request = new VexVerificationRequest
{
Envelope = envelope,
VerifyRekorInclusion = false
};
var result = await _service.VerifyAsync(request);
Assert.True(result.Valid);
Assert.NotNull(result.Document);
Assert.Equal(document.Id, result.Document.Id);
}
[Fact]
public async Task VerifyAsync_WithInvalidPayloadType_ReturnsInvalid()
{
var document = CreateTestDocument();
var payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(document);
var envelope = new VexDsseEnvelope
{
PayloadType = "invalid/type@v1",
Payload = Convert.ToBase64String(payload),
Signatures = [new VexDsseSignature { KeyId = "test", Sig = Convert.ToBase64String(new byte[32]) }]
};
var request = new VexVerificationRequest
{
Envelope = envelope,
VerifyRekorInclusion = false
};
var result = await _service.VerifyAsync(request);
Assert.False(result.Valid);
Assert.NotNull(result.Errors);
Assert.Contains(result.Errors, e => e.Contains("Invalid payload type"));
}
[Fact]
public async Task VerifyAsync_WithNoSignatures_ReturnsInvalid()
{
var document = CreateTestDocument();
var payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(document);
var envelope = new VexDsseEnvelope
{
PayloadType = VexPredicateTypes.VexDecision,
Payload = Convert.ToBase64String(payload),
Signatures = []
};
var request = new VexVerificationRequest
{
Envelope = envelope,
VerifyRekorInclusion = false
};
var result = await _service.VerifyAsync(request);
Assert.False(result.Valid);
Assert.NotNull(result.Errors);
Assert.Contains(result.Errors, e => e.Contains("no signatures"));
}
[Fact]
public async Task VerifyAsync_WithInvalidBase64Signature_ReturnsInvalid()
{
var document = CreateTestDocument();
var payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(document);
var envelope = new VexDsseEnvelope
{
PayloadType = VexPredicateTypes.VexDecision,
Payload = Convert.ToBase64String(payload),
Signatures = [new VexDsseSignature { KeyId = "test", Sig = "not-valid-base64!!!" }]
};
var request = new VexVerificationRequest
{
Envelope = envelope,
VerifyRekorInclusion = false
};
var result = await _service.VerifyAsync(request);
Assert.False(result.Valid);
Assert.NotNull(result.Errors);
Assert.Contains(result.Errors, e => e.Contains("Invalid base64"));
}
[Fact]
public async Task VerifyAsync_WithRekorVerification_CallsGetProof()
{
var document = CreateTestDocument();
var payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(document);
var envelope = new VexDsseEnvelope
{
PayloadType = VexPredicateTypes.VexDecision,
Payload = Convert.ToBase64String(payload),
Signatures = [new VexDsseSignature { KeyId = "test", Sig = Convert.ToBase64String(new byte[32]) }]
};
var rekorMetadata = new VexRekorMetadata
{
Uuid = "rekor-uuid-123",
Index = 12345,
LogUrl = "https://rekor.sigstore.dev",
IntegratedAt = DateTimeOffset.UtcNow
};
_mockRekorClient.Setup(c => c.GetProofAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(rekorMetadata);
var request = new VexVerificationRequest
{
Envelope = envelope,
ExpectedRekorMetadata = rekorMetadata,
VerifyRekorInclusion = true
};
var result = await _service.VerifyAsync(request);
Assert.True(result.Valid);
Assert.NotNull(result.RekorMetadata);
_mockRekorClient.Verify(c => c.GetProofAsync("rekor-uuid-123", It.IsAny<CancellationToken>()), Times.Once);
}
// Options Tests
[Fact]
public void VexSigningOptions_HasCorrectDefaults()
{
var options = new VexSigningOptions();
Assert.True(options.UseSignerService);
Assert.True(options.RekorEnabled);
Assert.Null(options.DefaultKeyId);
Assert.Null(options.RekorUrl);
Assert.Equal(TimeSpan.FromSeconds(30), options.RekorTimeout);
}
[Fact]
public void VexSigningOptions_SectionName_IsCorrect()
{
Assert.Equal("VexSigning", VexSigningOptions.SectionName);
}
// Predicate Types Tests
[Fact]
public void VexPredicateTypes_HasCorrectValues()
{
Assert.Equal("stella.ops/vexDecision@v1", VexPredicateTypes.VexDecision);
Assert.Equal("stella.ops/vex@v1", VexPredicateTypes.VexDocument);
Assert.Equal("https://openvex.dev/ns", VexPredicateTypes.OpenVex);
}
// Evidence Reference Tests
[Fact]
public async Task SignAsync_WithEvidenceRefs_IncludesInRequest()
{
var document = CreateTestDocument();
var evidenceRefs = new List<VexEvidenceReference>
{
new() { Type = "sbom", Digest = "sha256:abc123" },
new() { Type = "callgraph", Digest = "sha256:def456", CasUri = "cas://example/cg/1" }
};
var request = new VexSigningRequest
{
Document = document,
TenantId = "tenant-1",
SubmitToRekor = false,
EvidenceRefs = evidenceRefs
};
var localOptions = new VexSigningOptions { UseSignerService = false, RekorEnabled = false };
var optionsMonitor = new Mock<IOptionsMonitor<VexSigningOptions>>();
optionsMonitor.Setup(o => o.CurrentValue).Returns(localOptions);
var service = new VexDecisionSigningService(
null,
null,
optionsMonitor.Object,
TimeProvider.System,
NullLogger<VexDecisionSigningService>.Instance);
var result = await service.SignAsync(request);
Assert.True(result.Success);
Assert.NotNull(result.Envelope);
}
private static VexDecisionDocument CreateTestDocument()
{
var now = DateTimeOffset.UtcNow;
return new VexDecisionDocument
{
Id = $"https://stellaops.io/vex/{Guid.NewGuid():N}",
Author = "https://stellaops.io/policy-engine",
Timestamp = now,
Statements = ImmutableArray.Create(
new VexStatement
{
Vulnerability = new VexVulnerability { Id = "CVE-2025-12345" },
Status = "not_affected",
Justification = VexJustification.VulnerableCodeNotInExecutePath,
Timestamp = now,
Products = ImmutableArray.Create(
new VexProduct { Id = "pkg:maven/com.example/app@1.0.0" }
)
}
)
};
}
private static VexSigningRequest CreateSigningRequest(VexDecisionDocument document, bool submitToRekor = true)
{
return new VexSigningRequest
{
Document = document,
TenantId = "tenant-1",
SubmitToRekor = submitToRekor
};
}
}