update evidence bundle to include new evidence types and implement ProofSpine integration
Some checks failed
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
oas-ci / oas-validate (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
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
oas-ci / oas-validate (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
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.ProofSpine;
|
||||
|
||||
/// <summary>
|
||||
/// Service for DSSE (Dead Simple Signing Envelope) signing operations.
|
||||
/// </summary>
|
||||
public interface IDsseSigningService
|
||||
{
|
||||
/// <summary>
|
||||
/// Signs a payload and returns a DSSE envelope.
|
||||
/// </summary>
|
||||
Task<DsseEnvelope> SignAsync(
|
||||
object payload,
|
||||
ICryptoProfile cryptoProfile,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a DSSE envelope signature.
|
||||
/// </summary>
|
||||
Task<bool> VerifyAsync(
|
||||
DsseEnvelope envelope,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cryptographic profile for signing operations.
|
||||
/// </summary>
|
||||
public interface ICryptoProfile
|
||||
{
|
||||
/// <summary>
|
||||
/// Key identifier.
|
||||
/// </summary>
|
||||
string KeyId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Signing algorithm (e.g., "ed25519", "ecdsa-p256").
|
||||
/// </summary>
|
||||
string Algorithm { get; }
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.ProofSpine;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for ProofSpine persistence and queries.
|
||||
/// </summary>
|
||||
public interface IProofSpineRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a ProofSpine by its ID.
|
||||
/// </summary>
|
||||
Task<ProofSpine?> GetByIdAsync(string spineId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a ProofSpine by its decision criteria.
|
||||
/// </summary>
|
||||
Task<ProofSpine?> GetByDecisionAsync(
|
||||
string artifactId,
|
||||
string vulnId,
|
||||
string policyProfileId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all ProofSpines for a scan run.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<ProofSpine>> GetByScanRunAsync(
|
||||
string scanRunId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves a ProofSpine.
|
||||
/// </summary>
|
||||
Task<ProofSpine> SaveAsync(ProofSpine spine, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Supersedes an old spine with a new one.
|
||||
/// </summary>
|
||||
Task SupersedeAsync(
|
||||
string oldSpineId,
|
||||
string newSpineId,
|
||||
string reason,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all segments for a spine.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<ProofSegment>> GetSegmentsAsync(
|
||||
string spineId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.ProofSpine;
|
||||
|
||||
/// <summary>
|
||||
/// Builds ProofSpine chains from evidence segments.
|
||||
/// Ensures deterministic ordering and cryptographic chaining.
|
||||
/// </summary>
|
||||
public sealed class ProofSpineBuilder
|
||||
{
|
||||
private readonly List<ProofSegmentInput> _segments = new();
|
||||
private readonly IDsseSigningService _signer;
|
||||
private readonly ICryptoProfile _cryptoProfile;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private string? _artifactId;
|
||||
private string? _vulnerabilityId;
|
||||
private string? _policyProfileId;
|
||||
private string? _scanRunId;
|
||||
|
||||
public ProofSpineBuilder(
|
||||
IDsseSigningService signer,
|
||||
ICryptoProfile cryptoProfile,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_signer = signer;
|
||||
_cryptoProfile = cryptoProfile;
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
public ProofSpineBuilder ForArtifact(string artifactId)
|
||||
{
|
||||
_artifactId = artifactId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProofSpineBuilder ForVulnerability(string vulnId)
|
||||
{
|
||||
_vulnerabilityId = vulnId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProofSpineBuilder WithPolicyProfile(string profileId)
|
||||
{
|
||||
_policyProfileId = profileId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProofSpineBuilder WithScanRun(string scanRunId)
|
||||
{
|
||||
_scanRunId = scanRunId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an SBOM slice segment showing component relevance.
|
||||
/// </summary>
|
||||
public ProofSpineBuilder AddSbomSlice(
|
||||
string sbomDigest,
|
||||
IReadOnlyList<string> relevantPurls,
|
||||
string toolId,
|
||||
string toolVersion)
|
||||
{
|
||||
var input = new SbomSliceInput(sbomDigest, relevantPurls);
|
||||
var inputHash = ComputeCanonicalHash(input);
|
||||
var resultHash = ComputeCanonicalHash(relevantPurls);
|
||||
|
||||
_segments.Add(new ProofSegmentInput(
|
||||
ProofSegmentType.SbomSlice,
|
||||
inputHash,
|
||||
resultHash,
|
||||
input,
|
||||
toolId,
|
||||
toolVersion));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a vulnerability match segment.
|
||||
/// </summary>
|
||||
public ProofSpineBuilder AddMatch(
|
||||
string vulnId,
|
||||
string purl,
|
||||
string matchedVersion,
|
||||
string matchReason,
|
||||
string toolId,
|
||||
string toolVersion)
|
||||
{
|
||||
var input = new MatchInput(vulnId, purl, matchedVersion);
|
||||
var result = new MatchResult(matchReason);
|
||||
|
||||
_segments.Add(new ProofSegmentInput(
|
||||
ProofSegmentType.Match,
|
||||
ComputeCanonicalHash(input),
|
||||
ComputeCanonicalHash(result),
|
||||
new { Input = input, Result = result },
|
||||
toolId,
|
||||
toolVersion));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a reachability analysis segment.
|
||||
/// </summary>
|
||||
public ProofSpineBuilder AddReachability(
|
||||
string callgraphDigest,
|
||||
string latticeState,
|
||||
double confidence,
|
||||
IReadOnlyList<string>? pathWitness,
|
||||
string toolId,
|
||||
string toolVersion)
|
||||
{
|
||||
var input = new ReachabilityInput(callgraphDigest);
|
||||
var result = new ReachabilityResult(latticeState, confidence, pathWitness);
|
||||
|
||||
_segments.Add(new ProofSegmentInput(
|
||||
ProofSegmentType.Reachability,
|
||||
ComputeCanonicalHash(input),
|
||||
ComputeCanonicalHash(result),
|
||||
new { Input = input, Result = result },
|
||||
toolId,
|
||||
toolVersion));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a guard analysis segment (feature flags, config gates).
|
||||
/// </summary>
|
||||
public ProofSpineBuilder AddGuardAnalysis(
|
||||
IReadOnlyList<GuardCondition> guards,
|
||||
bool allGuardsPassed,
|
||||
string toolId,
|
||||
string toolVersion)
|
||||
{
|
||||
var input = new GuardAnalysisInput(guards);
|
||||
var result = new GuardAnalysisResult(allGuardsPassed);
|
||||
|
||||
_segments.Add(new ProofSegmentInput(
|
||||
ProofSegmentType.GuardAnalysis,
|
||||
ComputeCanonicalHash(input),
|
||||
ComputeCanonicalHash(result),
|
||||
new { Input = input, Result = result },
|
||||
toolId,
|
||||
toolVersion));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds runtime observation evidence.
|
||||
/// </summary>
|
||||
public ProofSpineBuilder AddRuntimeObservation(
|
||||
string runtimeFactsDigest,
|
||||
bool wasObserved,
|
||||
int hitCount,
|
||||
string toolId,
|
||||
string toolVersion)
|
||||
{
|
||||
var input = new RuntimeObservationInput(runtimeFactsDigest);
|
||||
var result = new RuntimeObservationResult(wasObserved, hitCount);
|
||||
|
||||
_segments.Add(new ProofSegmentInput(
|
||||
ProofSegmentType.RuntimeObservation,
|
||||
ComputeCanonicalHash(input),
|
||||
ComputeCanonicalHash(result),
|
||||
new { Input = input, Result = result },
|
||||
toolId,
|
||||
toolVersion));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds policy evaluation segment with final verdict.
|
||||
/// </summary>
|
||||
public ProofSpineBuilder AddPolicyEval(
|
||||
string policyDigest,
|
||||
string verdict,
|
||||
string verdictReason,
|
||||
IReadOnlyDictionary<string, object> factors,
|
||||
string toolId,
|
||||
string toolVersion)
|
||||
{
|
||||
var input = new PolicyEvalInput(policyDigest, factors);
|
||||
var result = new PolicyEvalResult(verdict, verdictReason);
|
||||
|
||||
_segments.Add(new ProofSegmentInput(
|
||||
ProofSegmentType.PolicyEval,
|
||||
ComputeCanonicalHash(input),
|
||||
ComputeCanonicalHash(result),
|
||||
new { Input = input, Result = result },
|
||||
toolId,
|
||||
toolVersion));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the final ProofSpine with chained, signed segments.
|
||||
/// </summary>
|
||||
public async Task<ProofSpine> BuildAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValidateBuilder();
|
||||
|
||||
// Sort segments by type (predetermined order)
|
||||
var orderedSegments = _segments
|
||||
.OrderBy(s => (int)s.Type)
|
||||
.ToList();
|
||||
|
||||
var builtSegments = new List<ProofSegment>();
|
||||
string? prevHash = null;
|
||||
|
||||
for (var i = 0; i < orderedSegments.Count; i++)
|
||||
{
|
||||
var input = orderedSegments[i];
|
||||
var createdAt = _timeProvider.GetUtcNow();
|
||||
|
||||
// Build payload for signing
|
||||
var payload = new ProofSegmentPayload(
|
||||
input.Type.ToString(),
|
||||
i,
|
||||
input.InputHash,
|
||||
input.ResultHash,
|
||||
prevHash,
|
||||
input.Payload,
|
||||
input.ToolId,
|
||||
input.ToolVersion,
|
||||
createdAt);
|
||||
|
||||
// Sign with DSSE
|
||||
var envelope = await _signer.SignAsync(
|
||||
payload,
|
||||
_cryptoProfile,
|
||||
cancellationToken);
|
||||
|
||||
var segmentId = ComputeSegmentId(input, i, prevHash);
|
||||
var segment = new ProofSegment(
|
||||
segmentId,
|
||||
input.Type,
|
||||
i,
|
||||
input.InputHash,
|
||||
input.ResultHash,
|
||||
prevHash,
|
||||
envelope,
|
||||
input.ToolId,
|
||||
input.ToolVersion,
|
||||
ProofSegmentStatus.Verified,
|
||||
createdAt);
|
||||
|
||||
builtSegments.Add(segment);
|
||||
prevHash = segment.ResultHash;
|
||||
}
|
||||
|
||||
// Compute root hash = hash(concat of all segment result hashes)
|
||||
var rootHash = ComputeRootHash(builtSegments);
|
||||
|
||||
// Compute deterministic spine ID
|
||||
var spineId = ComputeSpineId(_artifactId!, _vulnerabilityId!, _policyProfileId!, rootHash);
|
||||
|
||||
// Extract verdict from policy eval segment
|
||||
var (verdict, verdictReason) = ExtractVerdict(builtSegments);
|
||||
|
||||
return new ProofSpine(
|
||||
spineId,
|
||||
_artifactId!,
|
||||
_vulnerabilityId!,
|
||||
_policyProfileId!,
|
||||
builtSegments.ToImmutableArray(),
|
||||
verdict,
|
||||
verdictReason,
|
||||
rootHash,
|
||||
_scanRunId!,
|
||||
_timeProvider.GetUtcNow(),
|
||||
SupersededBySpineId: null);
|
||||
}
|
||||
|
||||
private void ValidateBuilder()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_artifactId))
|
||||
throw new InvalidOperationException("ArtifactId is required");
|
||||
if (string.IsNullOrWhiteSpace(_vulnerabilityId))
|
||||
throw new InvalidOperationException("VulnerabilityId is required");
|
||||
if (string.IsNullOrWhiteSpace(_policyProfileId))
|
||||
throw new InvalidOperationException("PolicyProfileId is required");
|
||||
if (string.IsNullOrWhiteSpace(_scanRunId))
|
||||
throw new InvalidOperationException("ScanRunId is required");
|
||||
if (_segments.Count == 0)
|
||||
throw new InvalidOperationException("At least one segment is required");
|
||||
}
|
||||
|
||||
private static string ComputeCanonicalHash(object input)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(input, CanonicalJsonOptions);
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static string ComputeSegmentId(ProofSegmentInput input, int index, string? prevHash)
|
||||
{
|
||||
var data = $"{input.Type}:{index}:{input.InputHash}:{input.ResultHash}:{prevHash ?? "null"}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(data));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant()[..32];
|
||||
}
|
||||
|
||||
private static string ComputeRootHash(IEnumerable<ProofSegment> segments)
|
||||
{
|
||||
var concat = string.Join(":", segments.Select(s => s.ResultHash));
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(concat));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static string ComputeSpineId(string artifactId, string vulnId, string profileId, string rootHash)
|
||||
{
|
||||
var data = $"{artifactId}:{vulnId}:{profileId}:{rootHash}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(data));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant()[..32];
|
||||
}
|
||||
|
||||
private static (string Verdict, string VerdictReason) ExtractVerdict(List<ProofSegment> segments)
|
||||
{
|
||||
// Default verdict if no policy eval segment
|
||||
return ("under_investigation", "No policy evaluation completed");
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
}
|
||||
|
||||
// Supporting input types
|
||||
internal sealed record ProofSegmentInput(
|
||||
ProofSegmentType Type,
|
||||
string InputHash,
|
||||
string ResultHash,
|
||||
object Payload,
|
||||
string ToolId,
|
||||
string ToolVersion);
|
||||
|
||||
internal sealed record SbomSliceInput(string SbomDigest, IReadOnlyList<string> RelevantPurls);
|
||||
internal sealed record MatchInput(string VulnId, string Purl, string MatchedVersion);
|
||||
internal sealed record MatchResult(string MatchReason);
|
||||
internal sealed record ReachabilityInput(string CallgraphDigest);
|
||||
internal sealed record ReachabilityResult(string LatticeState, double Confidence, IReadOnlyList<string>? PathWitness);
|
||||
internal sealed record GuardAnalysisInput(IReadOnlyList<GuardCondition> Guards);
|
||||
internal sealed record GuardAnalysisResult(bool AllGuardsPassed);
|
||||
internal sealed record RuntimeObservationInput(string RuntimeFactsDigest);
|
||||
internal sealed record RuntimeObservationResult(bool WasObserved, int HitCount);
|
||||
internal sealed record PolicyEvalInput(string PolicyDigest, IReadOnlyDictionary<string, object> Factors);
|
||||
internal sealed record PolicyEvalResult(string Verdict, string VerdictReason);
|
||||
internal sealed record ProofSegmentPayload(
|
||||
string SegmentType, int Index, string InputHash, string ResultHash,
|
||||
string? PrevSegmentHash, object Payload, string ToolId, string ToolVersion,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -1,86 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.ProofSpine;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a complete verifiable decision chain from SBOM to VEX verdict.
|
||||
/// </summary>
|
||||
public sealed record ProofSpine(
|
||||
string SpineId,
|
||||
string ArtifactId,
|
||||
string VulnerabilityId,
|
||||
string PolicyProfileId,
|
||||
IReadOnlyList<ProofSegment> Segments,
|
||||
string Verdict,
|
||||
string VerdictReason,
|
||||
string RootHash,
|
||||
string ScanRunId,
|
||||
DateTimeOffset CreatedAt,
|
||||
string? SupersededBySpineId);
|
||||
|
||||
/// <summary>
|
||||
/// A single evidence segment in the proof chain.
|
||||
/// </summary>
|
||||
public sealed record ProofSegment(
|
||||
string SegmentId,
|
||||
ProofSegmentType SegmentType,
|
||||
int Index,
|
||||
string InputHash,
|
||||
string ResultHash,
|
||||
string? PrevSegmentHash,
|
||||
DsseEnvelope Envelope,
|
||||
string ToolId,
|
||||
string ToolVersion,
|
||||
ProofSegmentStatus Status,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Segment types in execution order.
|
||||
/// </summary>
|
||||
public enum ProofSegmentType
|
||||
{
|
||||
SbomSlice = 1, // Component relevance extraction
|
||||
Match = 2, // SBOM-to-vulnerability mapping
|
||||
Reachability = 3, // Symbol reachability analysis
|
||||
GuardAnalysis = 4, // Config/feature flag gates
|
||||
RuntimeObservation = 5, // Runtime evidence correlation
|
||||
PolicyEval = 6 // Lattice decision computation
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verification status of a segment.
|
||||
/// </summary>
|
||||
public enum ProofSegmentStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Verified = 1,
|
||||
Partial = 2, // Some evidence missing but chain valid
|
||||
Invalid = 3, // Signature verification failed
|
||||
Untrusted = 4 // Key not in trust store
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DSSE envelope wrapper for signed content.
|
||||
/// </summary>
|
||||
public sealed record DsseEnvelope(
|
||||
string PayloadType,
|
||||
byte[] Payload,
|
||||
IReadOnlyList<DsseSignature> Signatures);
|
||||
|
||||
/// <summary>
|
||||
/// A signature in a DSSE envelope.
|
||||
/// </summary>
|
||||
public sealed record DsseSignature(
|
||||
string KeyId,
|
||||
byte[] Sig);
|
||||
|
||||
/// <summary>
|
||||
/// Guard condition for feature flag or config gate analysis.
|
||||
/// </summary>
|
||||
public sealed record GuardCondition(
|
||||
string Name,
|
||||
string Type,
|
||||
string Value,
|
||||
bool Passed);
|
||||
Reference in New Issue
Block a user