Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexGateOptions.cs
2026-01-07 09:43:12 +02:00

380 lines
11 KiB
C#

// -----------------------------------------------------------------------------
// 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;
/// <summary>
/// Configuration options for VEX gate service.
/// Binds to "VexGate" section in configuration files.
/// </summary>
public sealed class VexGateOptions : IValidatableObject
{
/// <summary>
/// Configuration section name.
/// </summary>
public const string SectionName = "VexGate";
/// <summary>
/// Enable VEX-first gating. Default: false.
/// When disabled, all findings pass through to triage unchanged.
/// </summary>
public bool Enabled { get; set; } = false;
/// <summary>
/// Default decision when no rules match. Default: Warn.
/// </summary>
public string DefaultDecision { get; set; } = "Warn";
/// <summary>
/// Policy version for audit/replay purposes.
/// Should be incremented when rules change.
/// </summary>
public string PolicyVersion { get; set; } = "1.0.0";
/// <summary>
/// Evaluation rules (ordered by priority, highest first).
/// </summary>
public List<VexGateRuleOptions> Rules { get; set; } = [];
/// <summary>
/// Caching settings for VEX observation lookups.
/// </summary>
public VexGateCacheOptions Cache { get; set; } = new();
/// <summary>
/// Audit logging settings.
/// </summary>
public VexGateAuditOptions Audit { get; set; } = new();
/// <summary>
/// Metrics settings.
/// </summary>
public VexGateMetricsOptions Metrics { get; set; } = new();
/// <summary>
/// Bypass settings for emergency scans.
/// </summary>
public VexGateBypassOptions Bypass { get; set; } = new();
/// <summary>
/// Converts this options instance to a VexGatePolicy.
/// </summary>
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,
};
}
/// <summary>
/// Creates options from a VexGatePolicy.
/// </summary>
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,
};
}
/// <inheritdoc/>
public IEnumerable<ValidationResult> 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<string>(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)]);
}
}
}
/// <summary>
/// Configuration options for a single VEX gate rule.
/// </summary>
public sealed class VexGateRuleOptions
{
/// <summary>
/// Unique identifier for this rule.
/// </summary>
[Required]
public string RuleId { get; set; } = string.Empty;
/// <summary>
/// Priority order (higher values evaluated first).
/// </summary>
public int Priority { get; set; } = 0;
/// <summary>
/// Decision to apply when this rule matches.
/// </summary>
[Required]
public string Decision { get; set; } = "Warn";
/// <summary>
/// Condition that must match for this rule to apply.
/// </summary>
public VexGateConditionOptions Condition { get; set; } = new();
/// <summary>
/// Converts to a VexGatePolicyRule.
/// </summary>
public VexGatePolicyRule ToRule()
{
return new VexGatePolicyRule
{
RuleId = RuleId,
Priority = Priority,
Decision = ParseDecision(Decision),
Condition = Condition.ToCondition(),
};
}
/// <summary>
/// Creates options from a VexGatePolicyRule.
/// </summary>
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,
};
}
}
/// <summary>
/// Configuration options for a rule condition.
/// </summary>
public sealed class VexGateConditionOptions
{
/// <summary>
/// Required VEX vendor status.
/// Options: not_affected, fixed, affected, under_investigation.
/// </summary>
public string? VendorStatus { get; set; }
/// <summary>
/// Whether the vulnerability must be exploitable.
/// </summary>
public bool? IsExploitable { get; set; }
/// <summary>
/// Whether the vulnerable code must be reachable.
/// </summary>
public bool? IsReachable { get; set; }
/// <summary>
/// Whether compensating controls must be present.
/// </summary>
public bool? HasCompensatingControl { get; set; }
/// <summary>
/// Whether the CVE is in KEV (Known Exploited Vulnerabilities).
/// </summary>
public bool? IsKnownExploited { get; set; }
/// <summary>
/// Required severity levels (any match).
/// </summary>
public List<string>? SeverityLevels { get; set; }
/// <summary>
/// Minimum confidence score required.
/// </summary>
public double? ConfidenceThreshold { get; set; }
/// <summary>
/// Converts to a VexGatePolicyCondition.
/// </summary>
public VexGatePolicyCondition ToCondition()
{
return new VexGatePolicyCondition
{
VendorStatus = ParseVexStatus(VendorStatus),
IsExploitable = IsExploitable,
IsReachable = IsReachable,
HasCompensatingControl = HasCompensatingControl,
SeverityLevels = SeverityLevels?.ToArray(),
MinConfidence = ConfidenceThreshold,
};
}
/// <summary>
/// Creates options from a VexGatePolicyCondition.
/// </summary>
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,
};
}
}
/// <summary>
/// Cache configuration options.
/// </summary>
public sealed class VexGateCacheOptions
{
/// <summary>
/// TTL for cached VEX observations (seconds). Default: 300.
/// </summary>
public int TtlSeconds { get; set; } = 300;
/// <summary>
/// Maximum cache entries. Default: 10000.
/// </summary>
public int MaxEntries { get; set; } = 10000;
}
/// <summary>
/// Audit logging configuration options.
/// </summary>
public sealed class VexGateAuditOptions
{
/// <summary>
/// Enable structured audit logging for compliance. Default: true.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Include full evidence in audit logs. Default: true.
/// </summary>
public bool IncludeEvidence { get; set; } = true;
/// <summary>
/// Log level for gate decisions. Default: Information.
/// </summary>
public string LogLevel { get; set; } = "Information";
}
/// <summary>
/// Metrics configuration options.
/// </summary>
public sealed class VexGateMetricsOptions
{
/// <summary>
/// Enable OpenTelemetry metrics. Default: true.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Histogram buckets for evaluation latency (milliseconds).
/// </summary>
public List<double> LatencyBuckets { get; set; } = [1, 5, 10, 25, 50, 100, 250];
}
/// <summary>
/// Bypass configuration options.
/// </summary>
public sealed class VexGateBypassOptions
{
/// <summary>
/// Allow gate bypass via CLI flag (--bypass-gate). Default: true.
/// </summary>
public bool AllowCliBypass { get; set; } = true;
/// <summary>
/// Require specific reason when bypassing. Default: false.
/// </summary>
public bool RequireReason { get; set; } = false;
/// <summary>
/// Emit warning when bypass is used. Default: true.
/// </summary>
public bool WarnOnBypass { get; set; } = true;
}