new advisories work and features gaps work
This commit is contained in:
@@ -232,7 +232,14 @@ internal sealed record PolicyEvaluationReachability(
|
||||
bool HasRuntimeEvidence,
|
||||
string? Source,
|
||||
string? Method,
|
||||
string? EvidenceRef)
|
||||
string? EvidenceRef,
|
||||
// Sprint: SPRINT_20260112_007_POLICY_path_gate_inputs (PW-POL-002)
|
||||
string? PathHash = null,
|
||||
ImmutableArray<string>? NodeHashes = null,
|
||||
string? EntryNodeHash = null,
|
||||
string? SinkNodeHash = null,
|
||||
DateTimeOffset? RuntimeEvidenceAt = null,
|
||||
bool? ObservedAtRuntime = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Default unknown reachability state.
|
||||
|
||||
@@ -117,6 +117,38 @@ public sealed record ReachabilityInput
|
||||
/// Raw reachability score from advanced engine.
|
||||
/// </summary>
|
||||
public double? AdvancedScore { get; init; }
|
||||
|
||||
// --- Sprint: SPRINT_20260112_007_POLICY_path_gate_inputs (PW-POL-001) ---
|
||||
|
||||
/// <summary>
|
||||
/// Canonical path hash (sha256:hex) for the reachability path.
|
||||
/// </summary>
|
||||
public string? PathHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Node hashes for symbols along the path (top-K for efficiency).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? NodeHashes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Entry point node hash.
|
||||
/// </summary>
|
||||
public string? EntryNodeHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Sink (vulnerable function) node hash.
|
||||
/// </summary>
|
||||
public string? SinkNodeHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when runtime evidence was last captured (for freshness checks).
|
||||
/// </summary>
|
||||
public DateTimeOffset? RuntimeEvidenceAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the path was observed at runtime (not just static analysis).
|
||||
/// </summary>
|
||||
public bool? ObservedAtRuntime { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
301
src/Policy/StellaOps.Policy.Engine/Vex/VexOverrideSignals.cs
Normal file
301
src/Policy/StellaOps.Policy.Engine/Vex/VexOverrideSignals.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
// <copyright file="VexOverrideSignals.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_004_POLICY_signed_override_enforcement (POL-OVR-001, POL-OVR-002)
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Vex;
|
||||
|
||||
/// <summary>
|
||||
/// VEX override signature validation result for policy evaluation.
|
||||
/// </summary>
|
||||
public sealed record VexOverrideSignalInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the override is signed with a valid DSSE envelope.
|
||||
/// </summary>
|
||||
[JsonPropertyName("overrideSigned")]
|
||||
public required bool OverrideSigned { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the override has verified Rekor inclusion proof.
|
||||
/// </summary>
|
||||
[JsonPropertyName("overrideRekorVerified")]
|
||||
public required bool OverrideRekorVerified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signing key ID if signed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signingKeyId")]
|
||||
public string? SigningKeyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Issuer identity from the signature.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signerIdentity")]
|
||||
public string? SignerIdentity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE envelope digest if signed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("envelopeDigest")]
|
||||
public string? EnvelopeDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor log index if verified.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rekorLogIndex")]
|
||||
public long? RekorLogIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor integrated time (Unix seconds) if verified.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rekorIntegratedTime")]
|
||||
public long? RekorIntegratedTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Override validity period (start).
|
||||
/// </summary>
|
||||
[JsonPropertyName("validFrom")]
|
||||
public DateTimeOffset? ValidFrom { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Override validity period (end).
|
||||
/// </summary>
|
||||
[JsonPropertyName("validUntil")]
|
||||
public DateTimeOffset? ValidUntil { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the override is currently within its validity period.
|
||||
/// </summary>
|
||||
[JsonPropertyName("withinValidityPeriod")]
|
||||
public required bool WithinValidityPeriod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Trust level of the signing key (trusted, unknown, revoked).
|
||||
/// </summary>
|
||||
[JsonPropertyName("keyTrustLevel")]
|
||||
public required VexKeyTrustLevel KeyTrustLevel { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Validation error message if failed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("validationError")]
|
||||
public string? ValidationError { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trust level of a signing key.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum VexKeyTrustLevel
|
||||
{
|
||||
/// <summary>Key is in trusted keyring.</summary>
|
||||
Trusted,
|
||||
|
||||
/// <summary>Key is not in keyring but signature is valid.</summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>Key has been revoked.</summary>
|
||||
Revoked,
|
||||
|
||||
/// <summary>Key trust could not be determined (offline mode).</summary>
|
||||
Unavailable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override enforcement policy configuration.
|
||||
/// </summary>
|
||||
public sealed record VexOverrideEnforcementPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Require signed overrides (reject unsigned).
|
||||
/// </summary>
|
||||
[JsonPropertyName("requireSigned")]
|
||||
public bool RequireSigned { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Require Rekor verification.
|
||||
/// </summary>
|
||||
[JsonPropertyName("requireRekorVerified")]
|
||||
public bool RequireRekorVerified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Allow unknown keys (not in keyring) if signature is valid.
|
||||
/// </summary>
|
||||
[JsonPropertyName("allowUnknownKeys")]
|
||||
public bool AllowUnknownKeys { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum age for override validity (zero = no limit).
|
||||
/// </summary>
|
||||
[JsonPropertyName("maxOverrideAge")]
|
||||
public TimeSpan MaxOverrideAge { get; init; } = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Allowed signer identities (empty = all allowed).
|
||||
/// </summary>
|
||||
[JsonPropertyName("allowedSigners")]
|
||||
public ImmutableArray<string> AllowedSigners { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of VEX override enforcement check.
|
||||
/// </summary>
|
||||
public sealed record VexOverrideEnforcementResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the override is allowed by policy.
|
||||
/// </summary>
|
||||
[JsonPropertyName("allowed")]
|
||||
public required bool Allowed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason if rejected.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rejectionReason")]
|
||||
public string? RejectionReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Enforcement rule that triggered rejection.
|
||||
/// </summary>
|
||||
[JsonPropertyName("enforcementRule")]
|
||||
public string? EnforcementRule { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The input signals used for evaluation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signals")]
|
||||
public required VexOverrideSignalInput Signals { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an allowed result.
|
||||
/// </summary>
|
||||
public static VexOverrideEnforcementResult Allow(VexOverrideSignalInput signals) => new()
|
||||
{
|
||||
Allowed = true,
|
||||
Signals = signals
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a rejected result.
|
||||
/// </summary>
|
||||
public static VexOverrideEnforcementResult Reject(
|
||||
VexOverrideSignalInput signals,
|
||||
string reason,
|
||||
string rule) => new()
|
||||
{
|
||||
Allowed = false,
|
||||
RejectionReason = reason,
|
||||
EnforcementRule = rule,
|
||||
Signals = signals
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for validating VEX override signatures and enforcing policy.
|
||||
/// </summary>
|
||||
public interface IVexOverrideSignatureValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates override signature and produces policy signals.
|
||||
/// </summary>
|
||||
Task<VexOverrideSignalInput> ValidateSignatureAsync(
|
||||
string envelopeBase64,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if override is allowed by enforcement policy.
|
||||
/// </summary>
|
||||
VexOverrideEnforcementResult CheckEnforcement(
|
||||
VexOverrideSignalInput signals,
|
||||
VexOverrideEnforcementPolicy policy,
|
||||
DateTimeOffset evaluationTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating VEX override signal inputs.
|
||||
/// </summary>
|
||||
public static class VexOverrideSignalFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a signal input for an unsigned override.
|
||||
/// </summary>
|
||||
public static VexOverrideSignalInput CreateUnsigned() => new()
|
||||
{
|
||||
OverrideSigned = false,
|
||||
OverrideRekorVerified = false,
|
||||
WithinValidityPeriod = true,
|
||||
KeyTrustLevel = VexKeyTrustLevel.Unavailable
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a signal input for a signed but unverified override.
|
||||
/// </summary>
|
||||
public static VexOverrideSignalInput CreateSignedUnverified(
|
||||
string signingKeyId,
|
||||
string? signerIdentity,
|
||||
string envelopeDigest,
|
||||
VexKeyTrustLevel keyTrustLevel,
|
||||
DateTimeOffset? validFrom,
|
||||
DateTimeOffset? validUntil,
|
||||
DateTimeOffset evaluationTime) => new()
|
||||
{
|
||||
OverrideSigned = true,
|
||||
OverrideRekorVerified = false,
|
||||
SigningKeyId = signingKeyId,
|
||||
SignerIdentity = signerIdentity,
|
||||
EnvelopeDigest = envelopeDigest,
|
||||
KeyTrustLevel = keyTrustLevel,
|
||||
ValidFrom = validFrom,
|
||||
ValidUntil = validUntil,
|
||||
WithinValidityPeriod = IsWithinValidityPeriod(validFrom, validUntil, evaluationTime)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a signal input for a fully verified override with Rekor inclusion.
|
||||
/// </summary>
|
||||
public static VexOverrideSignalInput CreateFullyVerified(
|
||||
string signingKeyId,
|
||||
string? signerIdentity,
|
||||
string envelopeDigest,
|
||||
VexKeyTrustLevel keyTrustLevel,
|
||||
long rekorLogIndex,
|
||||
long rekorIntegratedTime,
|
||||
DateTimeOffset? validFrom,
|
||||
DateTimeOffset? validUntil,
|
||||
DateTimeOffset evaluationTime) => new()
|
||||
{
|
||||
OverrideSigned = true,
|
||||
OverrideRekorVerified = true,
|
||||
SigningKeyId = signingKeyId,
|
||||
SignerIdentity = signerIdentity,
|
||||
EnvelopeDigest = envelopeDigest,
|
||||
RekorLogIndex = rekorLogIndex,
|
||||
RekorIntegratedTime = rekorIntegratedTime,
|
||||
KeyTrustLevel = keyTrustLevel,
|
||||
ValidFrom = validFrom,
|
||||
ValidUntil = validUntil,
|
||||
WithinValidityPeriod = IsWithinValidityPeriod(validFrom, validUntil, evaluationTime)
|
||||
};
|
||||
|
||||
private static bool IsWithinValidityPeriod(
|
||||
DateTimeOffset? validFrom,
|
||||
DateTimeOffset? validUntil,
|
||||
DateTimeOffset evaluationTime)
|
||||
{
|
||||
if (validFrom.HasValue && evaluationTime < validFrom.Value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validUntil.HasValue && evaluationTime > validUntil.Value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user