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
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:
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user