// ----------------------------------------------------------------------------- // 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; }