// -----------------------------------------------------------------------------
// VexGateOptions.cs
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
// Task: T028 - Add gate policy to tenant configuration
// Description: Configuration options for VEX gate, bindable from YAML/JSON config.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
namespace StellaOps.Scanner.Gate;
///
/// Configuration options for VEX gate service.
/// Binds to "VexGate" section in configuration files.
///
public sealed class VexGateOptions : IValidatableObject
{
///
/// Configuration section name.
///
public const string SectionName = "VexGate";
///
/// Enable VEX-first gating. Default: false.
/// When disabled, all findings pass through to triage unchanged.
///
public bool Enabled { get; set; } = false;
///
/// Default decision when no rules match. Default: Warn.
///
public string DefaultDecision { get; set; } = "Warn";
///
/// Policy version for audit/replay purposes.
/// Should be incremented when rules change.
///
public string PolicyVersion { get; set; } = "1.0.0";
///
/// Evaluation rules (ordered by priority, highest first).
///
public List Rules { get; set; } = [];
///
/// Caching settings for VEX observation lookups.
///
public VexGateCacheOptions Cache { get; set; } = new();
///
/// Audit logging settings.
///
public VexGateAuditOptions Audit { get; set; } = new();
///
/// Metrics settings.
///
public VexGateMetricsOptions Metrics { get; set; } = new();
///
/// Bypass settings for emergency scans.
///
public VexGateBypassOptions Bypass { get; set; } = new();
///
/// Converts this options instance to a VexGatePolicy.
///
public VexGatePolicy ToPolicy()
{
var defaultDecision = ParseDecision(DefaultDecision);
var rules = Rules
.Select(r => r.ToRule())
.OrderByDescending(r => r.Priority)
.ToImmutableArray();
return new VexGatePolicy
{
DefaultDecision = defaultDecision,
Rules = rules,
};
}
///
/// Creates options from a VexGatePolicy.
///
public static VexGateOptions FromPolicy(VexGatePolicy policy)
{
return new VexGateOptions
{
Enabled = true,
DefaultDecision = policy.DefaultDecision.ToString(),
Rules = policy.Rules.Select(r => VexGateRuleOptions.FromRule(r)).ToList(),
};
}
private static VexGateDecision ParseDecision(string value)
{
return value.ToUpperInvariant() switch
{
"PASS" => VexGateDecision.Pass,
"WARN" => VexGateDecision.Warn,
"BLOCK" => VexGateDecision.Block,
_ => VexGateDecision.Warn,
};
}
///
public IEnumerable Validate(ValidationContext validationContext)
{
if (Enabled && Rules.Count == 0)
{
yield return new ValidationResult(
"At least one rule is required when VexGate is enabled",
[nameof(Rules)]);
}
var ruleIds = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var rule in Rules)
{
if (string.IsNullOrWhiteSpace(rule.RuleId))
{
yield return new ValidationResult(
"Rule ID is required for all rules",
[nameof(Rules)]);
}
else if (!ruleIds.Add(rule.RuleId))
{
yield return new ValidationResult(
$"Duplicate rule ID: {rule.RuleId}",
[nameof(Rules)]);
}
}
if (Cache.TtlSeconds <= 0)
{
yield return new ValidationResult(
"Cache TTL must be positive",
[nameof(Cache)]);
}
if (Cache.MaxEntries <= 0)
{
yield return new ValidationResult(
"Cache max entries must be positive",
[nameof(Cache)]);
}
}
}
///
/// Configuration options for a single VEX gate rule.
///
public sealed class VexGateRuleOptions
{
///
/// Unique identifier for this rule.
///
[Required]
public string RuleId { get; set; } = string.Empty;
///
/// Priority order (higher values evaluated first).
///
public int Priority { get; set; } = 0;
///
/// Decision to apply when this rule matches.
///
[Required]
public string Decision { get; set; } = "Warn";
///
/// Condition that must match for this rule to apply.
///
public VexGateConditionOptions Condition { get; set; } = new();
///
/// Converts to a VexGatePolicyRule.
///
public VexGatePolicyRule ToRule()
{
return new VexGatePolicyRule
{
RuleId = RuleId,
Priority = Priority,
Decision = ParseDecision(Decision),
Condition = Condition.ToCondition(),
};
}
///
/// Creates options from a VexGatePolicyRule.
///
public static VexGateRuleOptions FromRule(VexGatePolicyRule rule)
{
return new VexGateRuleOptions
{
RuleId = rule.RuleId,
Priority = rule.Priority,
Decision = rule.Decision.ToString(),
Condition = VexGateConditionOptions.FromCondition(rule.Condition),
};
}
private static VexGateDecision ParseDecision(string value)
{
return value.ToUpperInvariant() switch
{
"PASS" => VexGateDecision.Pass,
"WARN" => VexGateDecision.Warn,
"BLOCK" => VexGateDecision.Block,
_ => VexGateDecision.Warn,
};
}
}
///
/// Configuration options for a rule condition.
///
public sealed class VexGateConditionOptions
{
///
/// Required VEX vendor status.
/// Options: not_affected, fixed, affected, under_investigation.
///
public string? VendorStatus { get; set; }
///
/// Whether the vulnerability must be exploitable.
///
public bool? IsExploitable { get; set; }
///
/// Whether the vulnerable code must be reachable.
///
public bool? IsReachable { get; set; }
///
/// Whether compensating controls must be present.
///
public bool? HasCompensatingControl { get; set; }
///
/// Whether the CVE is in KEV (Known Exploited Vulnerabilities).
///
public bool? IsKnownExploited { get; set; }
///
/// Required severity levels (any match).
///
public List? SeverityLevels { get; set; }
///
/// Minimum confidence score required.
///
public double? ConfidenceThreshold { get; set; }
///
/// Converts to a VexGatePolicyCondition.
///
public VexGatePolicyCondition ToCondition()
{
return new VexGatePolicyCondition
{
VendorStatus = ParseVexStatus(VendorStatus),
IsExploitable = IsExploitable,
IsReachable = IsReachable,
HasCompensatingControl = HasCompensatingControl,
SeverityLevels = SeverityLevels?.ToArray(),
MinConfidence = ConfidenceThreshold,
};
}
///
/// Creates options from a VexGatePolicyCondition.
///
public static VexGateConditionOptions FromCondition(VexGatePolicyCondition condition)
{
return new VexGateConditionOptions
{
VendorStatus = condition.VendorStatus?.ToString().ToLowerInvariant(),
IsExploitable = condition.IsExploitable,
IsReachable = condition.IsReachable,
HasCompensatingControl = condition.HasCompensatingControl,
SeverityLevels = condition.SeverityLevels?.ToList(),
ConfidenceThreshold = condition.MinConfidence,
};
}
private static VexStatus? ParseVexStatus(string? value)
{
if (string.IsNullOrWhiteSpace(value))
return null;
return value.ToLowerInvariant() switch
{
"not_affected" or "notaffected" => VexStatus.NotAffected,
"fixed" => VexStatus.Fixed,
"affected" => VexStatus.Affected,
"under_investigation" or "underinvestigation" => VexStatus.UnderInvestigation,
_ => null,
};
}
}
///
/// Cache configuration options.
///
public sealed class VexGateCacheOptions
{
///
/// TTL for cached VEX observations (seconds). Default: 300.
///
public int TtlSeconds { get; set; } = 300;
///
/// Maximum cache entries. Default: 10000.
///
public int MaxEntries { get; set; } = 10000;
}
///
/// Audit logging configuration options.
///
public sealed class VexGateAuditOptions
{
///
/// Enable structured audit logging for compliance. Default: true.
///
public bool Enabled { get; set; } = true;
///
/// Include full evidence in audit logs. Default: true.
///
public bool IncludeEvidence { get; set; } = true;
///
/// Log level for gate decisions. Default: Information.
///
public string LogLevel { get; set; } = "Information";
}
///
/// Metrics configuration options.
///
public sealed class VexGateMetricsOptions
{
///
/// Enable OpenTelemetry metrics. Default: true.
///
public bool Enabled { get; set; } = true;
///
/// Histogram buckets for evaluation latency (milliseconds).
///
public List LatencyBuckets { get; set; } = [1, 5, 10, 25, 50, 100, 250];
}
///
/// Bypass configuration options.
///
public sealed class VexGateBypassOptions
{
///
/// Allow gate bypass via CLI flag (--bypass-gate). Default: true.
///
public bool AllowCliBypass { get; set; } = true;
///
/// Require specific reason when bypassing. Default: false.
///
public bool RequireReason { get; set; } = false;
///
/// Emit warning when bypass is used. Default: true.
///
public bool WarnOnBypass { get; set; } = true;
}