sprints and audit work
This commit is contained in:
379
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexGateOptions.cs
Normal file
379
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexGateOptions.cs
Normal file
@@ -0,0 +1,379 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user