up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-27 23:44:42 +02:00
parent ef6e4b2067
commit 3b96b2e3ea
298 changed files with 47516 additions and 1168 deletions

View File

@@ -0,0 +1,266 @@
using System.Text.Json.Serialization;
using StellaOps.Policy.RiskProfile.Models;
namespace StellaOps.Policy.RiskProfile.Overrides;
/// <summary>
/// An override with full audit metadata.
/// </summary>
public sealed record AuditedOverride(
[property: JsonPropertyName("override_id")] string OverrideId,
[property: JsonPropertyName("profile_id")] string ProfileId,
[property: JsonPropertyName("override_type")] OverrideType OverrideType,
[property: JsonPropertyName("predicate")] OverridePredicate Predicate,
[property: JsonPropertyName("action")] OverrideAction Action,
[property: JsonPropertyName("priority")] int Priority,
[property: JsonPropertyName("audit")] OverrideAuditMetadata Audit,
[property: JsonPropertyName("status")] OverrideStatus Status,
[property: JsonPropertyName("expiration")] DateTimeOffset? Expiration = null,
[property: JsonPropertyName("tags")] IReadOnlyList<string>? Tags = null);
/// <summary>
/// Type of override.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<OverrideType>))]
public enum OverrideType
{
/// <summary>
/// Override the computed severity.
/// </summary>
[JsonPropertyName("severity")]
Severity,
/// <summary>
/// Override the recommended action/decision.
/// </summary>
[JsonPropertyName("decision")]
Decision,
/// <summary>
/// Override a signal weight.
/// </summary>
[JsonPropertyName("weight")]
Weight,
/// <summary>
/// Exception that exempts from policy.
/// </summary>
[JsonPropertyName("exception")]
Exception
}
/// <summary>
/// Status of an override.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<OverrideStatus>))]
public enum OverrideStatus
{
[JsonPropertyName("active")]
Active,
[JsonPropertyName("disabled")]
Disabled,
[JsonPropertyName("expired")]
Expired,
[JsonPropertyName("superseded")]
Superseded
}
/// <summary>
/// Predicate for when an override applies.
/// </summary>
public sealed record OverridePredicate(
[property: JsonPropertyName("conditions")] IReadOnlyList<OverrideCondition> Conditions,
[property: JsonPropertyName("match_mode")] PredicateMatchMode MatchMode = PredicateMatchMode.All);
/// <summary>
/// A single condition in an override predicate.
/// </summary>
public sealed record OverrideCondition(
[property: JsonPropertyName("field")] string Field,
[property: JsonPropertyName("operator")] ConditionOperator Operator,
[property: JsonPropertyName("value")] object? Value);
/// <summary>
/// Condition operator.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<ConditionOperator>))]
public enum ConditionOperator
{
[JsonPropertyName("eq")]
Equals,
[JsonPropertyName("neq")]
NotEquals,
[JsonPropertyName("gt")]
GreaterThan,
[JsonPropertyName("gte")]
GreaterThanOrEqual,
[JsonPropertyName("lt")]
LessThan,
[JsonPropertyName("lte")]
LessThanOrEqual,
[JsonPropertyName("in")]
In,
[JsonPropertyName("nin")]
NotIn,
[JsonPropertyName("contains")]
Contains,
[JsonPropertyName("regex")]
Regex
}
/// <summary>
/// Predicate match mode.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<PredicateMatchMode>))]
public enum PredicateMatchMode
{
/// <summary>
/// All conditions must match.
/// </summary>
[JsonPropertyName("all")]
All,
/// <summary>
/// Any condition must match.
/// </summary>
[JsonPropertyName("any")]
Any
}
/// <summary>
/// Action to take when override matches.
/// </summary>
public sealed record OverrideAction(
[property: JsonPropertyName("action_type")] OverrideActionType ActionType,
[property: JsonPropertyName("severity")] RiskSeverity? Severity = null,
[property: JsonPropertyName("decision")] RiskAction? Decision = null,
[property: JsonPropertyName("weight_factor")] double? WeightFactor = null,
[property: JsonPropertyName("reason")] string? Reason = null);
/// <summary>
/// Type of override action.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<OverrideActionType>))]
public enum OverrideActionType
{
[JsonPropertyName("set_severity")]
SetSeverity,
[JsonPropertyName("set_decision")]
SetDecision,
[JsonPropertyName("adjust_weight")]
AdjustWeight,
[JsonPropertyName("exempt")]
Exempt,
[JsonPropertyName("suppress")]
Suppress
}
/// <summary>
/// Audit metadata for an override.
/// </summary>
public sealed record OverrideAuditMetadata(
[property: JsonPropertyName("created_at")] DateTimeOffset CreatedAt,
[property: JsonPropertyName("created_by")] string? CreatedBy,
[property: JsonPropertyName("reason")] string Reason,
[property: JsonPropertyName("justification")] string? Justification,
[property: JsonPropertyName("ticket_ref")] string? TicketRef,
[property: JsonPropertyName("approved_by")] string? ApprovedBy,
[property: JsonPropertyName("approved_at")] DateTimeOffset? ApprovedAt,
[property: JsonPropertyName("review_required")] bool ReviewRequired = false,
[property: JsonPropertyName("last_modified_at")] DateTimeOffset? LastModifiedAt = null,
[property: JsonPropertyName("last_modified_by")] string? LastModifiedBy = null);
/// <summary>
/// Request to create an override.
/// </summary>
public sealed record CreateOverrideRequest(
[property: JsonPropertyName("profile_id")] string ProfileId,
[property: JsonPropertyName("override_type")] OverrideType OverrideType,
[property: JsonPropertyName("predicate")] OverridePredicate Predicate,
[property: JsonPropertyName("action")] OverrideAction Action,
[property: JsonPropertyName("priority")] int? Priority,
[property: JsonPropertyName("reason")] string Reason,
[property: JsonPropertyName("justification")] string? Justification,
[property: JsonPropertyName("ticket_ref")] string? TicketRef,
[property: JsonPropertyName("expiration")] DateTimeOffset? Expiration,
[property: JsonPropertyName("tags")] IReadOnlyList<string>? Tags,
[property: JsonPropertyName("review_required")] bool ReviewRequired = false);
/// <summary>
/// Result of override conflict validation.
/// </summary>
public sealed record OverrideConflictValidation(
[property: JsonPropertyName("has_conflicts")] bool HasConflicts,
[property: JsonPropertyName("conflicts")] IReadOnlyList<OverrideConflict> Conflicts,
[property: JsonPropertyName("warnings")] IReadOnlyList<string> Warnings);
/// <summary>
/// Details of a conflict between overrides.
/// </summary>
public sealed record OverrideConflict(
[property: JsonPropertyName("override_id")] string OverrideId,
[property: JsonPropertyName("conflict_type")] ConflictType ConflictType,
[property: JsonPropertyName("description")] string Description,
[property: JsonPropertyName("resolution")] ConflictResolution Resolution);
/// <summary>
/// Type of conflict.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<ConflictType>))]
public enum ConflictType
{
[JsonPropertyName("same_predicate")]
SamePredicate,
[JsonPropertyName("overlapping_predicate")]
OverlappingPredicate,
[JsonPropertyName("contradictory_action")]
ContradictoryAction,
[JsonPropertyName("priority_collision")]
PriorityCollision
}
/// <summary>
/// Resolution for a conflict.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<ConflictResolution>))]
public enum ConflictResolution
{
[JsonPropertyName("higher_priority_wins")]
HigherPriorityWins,
[JsonPropertyName("newer_wins")]
NewerWins,
[JsonPropertyName("manual_review_required")]
ManualReviewRequired
}
/// <summary>
/// Override application record for audit trail.
/// </summary>
public sealed record OverrideApplicationRecord(
[property: JsonPropertyName("override_id")] string OverrideId,
[property: JsonPropertyName("finding_id")] string FindingId,
[property: JsonPropertyName("applied_at")] DateTimeOffset AppliedAt,
[property: JsonPropertyName("original_value")] object? OriginalValue,
[property: JsonPropertyName("applied_value")] object? AppliedValue,
[property: JsonPropertyName("context")] Dictionary<string, object?> Context);

View File

@@ -0,0 +1,570 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace StellaOps.Policy.RiskProfile.Overrides;
/// <summary>
/// Service for managing overrides with audit metadata and conflict validation.
/// </summary>
public sealed class OverrideService
{
private readonly TimeProvider _timeProvider;
private readonly ConcurrentDictionary<string, AuditedOverride> _overrides;
private readonly ConcurrentDictionary<string, List<string>> _profileIndex;
private readonly ConcurrentDictionary<string, List<OverrideApplicationRecord>> _applicationHistory;
public OverrideService(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_overrides = new ConcurrentDictionary<string, AuditedOverride>(StringComparer.OrdinalIgnoreCase);
_profileIndex = new ConcurrentDictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
_applicationHistory = new ConcurrentDictionary<string, List<OverrideApplicationRecord>>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Creates a new override with audit metadata.
/// </summary>
public AuditedOverride Create(CreateOverrideRequest request, string? createdBy = null)
{
ArgumentNullException.ThrowIfNull(request);
if (string.IsNullOrWhiteSpace(request.ProfileId))
{
throw new ArgumentException("ProfileId is required.");
}
if (string.IsNullOrWhiteSpace(request.Reason))
{
throw new ArgumentException("Reason is required for audit purposes.");
}
var now = _timeProvider.GetUtcNow();
var overrideId = GenerateOverrideId(request, now);
var audit = new OverrideAuditMetadata(
CreatedAt: now,
CreatedBy: createdBy,
Reason: request.Reason,
Justification: request.Justification,
TicketRef: request.TicketRef,
ApprovedBy: null,
ApprovedAt: null,
ReviewRequired: request.ReviewRequired);
var auditedOverride = new AuditedOverride(
OverrideId: overrideId,
ProfileId: request.ProfileId,
OverrideType: request.OverrideType,
Predicate: request.Predicate,
Action: request.Action,
Priority: request.Priority ?? 100,
Audit: audit,
Status: request.ReviewRequired ? OverrideStatus.Disabled : OverrideStatus.Active,
Expiration: request.Expiration,
Tags: request.Tags);
_overrides[overrideId] = auditedOverride;
IndexOverride(auditedOverride);
return auditedOverride;
}
/// <summary>
/// Gets an override by ID.
/// </summary>
public AuditedOverride? Get(string overrideId)
{
return _overrides.TryGetValue(overrideId, out var over) ? over : null;
}
/// <summary>
/// Lists overrides for a profile.
/// </summary>
public IReadOnlyList<AuditedOverride> ListByProfile(string profileId, bool includeInactive = false)
{
if (!_profileIndex.TryGetValue(profileId, out var ids))
{
return Array.Empty<AuditedOverride>();
}
var now = _timeProvider.GetUtcNow();
lock (ids)
{
var overrides = ids
.Select(id => _overrides.TryGetValue(id, out var o) ? o : null)
.Where(o => o != null)
.Cast<AuditedOverride>();
if (!includeInactive)
{
overrides = overrides.Where(o => IsActive(o, now));
}
return overrides
.OrderByDescending(o => o.Priority)
.ThenByDescending(o => o.Audit.CreatedAt)
.ToList()
.AsReadOnly();
}
}
/// <summary>
/// Validates an override for conflicts with existing overrides.
/// </summary>
public OverrideConflictValidation ValidateConflicts(CreateOverrideRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var conflicts = new List<OverrideConflict>();
var warnings = new List<string>();
var existingOverrides = ListByProfile(request.ProfileId, includeInactive: false);
foreach (var existing in existingOverrides)
{
// Check for same predicate
if (PredicatesEqual(request.Predicate, existing.Predicate))
{
conflicts.Add(new OverrideConflict(
OverrideId: existing.OverrideId,
ConflictType: ConflictType.SamePredicate,
Description: $"Override {existing.OverrideId} has identical predicate conditions.",
Resolution: ConflictResolution.HigherPriorityWins));
}
// Check for overlapping predicate
else if (PredicatesOverlap(request.Predicate, existing.Predicate))
{
warnings.Add($"Override may overlap with {existing.OverrideId}. Consider reviewing priority settings.");
// Check for contradictory actions
if (ActionsContradict(request.Action, existing.Action))
{
conflicts.Add(new OverrideConflict(
OverrideId: existing.OverrideId,
ConflictType: ConflictType.ContradictoryAction,
Description: $"Override {existing.OverrideId} has contradictory action for overlapping conditions.",
Resolution: ConflictResolution.ManualReviewRequired));
}
}
// Check for priority collision
if (request.Priority == existing.Priority && PredicatesOverlap(request.Predicate, existing.Predicate))
{
conflicts.Add(new OverrideConflict(
OverrideId: existing.OverrideId,
ConflictType: ConflictType.PriorityCollision,
Description: $"Override {existing.OverrideId} has same priority and overlapping conditions.",
Resolution: ConflictResolution.NewerWins));
}
}
return new OverrideConflictValidation(
HasConflicts: conflicts.Count > 0,
Conflicts: conflicts.AsReadOnly(),
Warnings: warnings.AsReadOnly());
}
/// <summary>
/// Approves an override that requires review.
/// </summary>
public AuditedOverride? Approve(string overrideId, string approvedBy)
{
if (!_overrides.TryGetValue(overrideId, out var existing))
{
return null;
}
if (!existing.Audit.ReviewRequired)
{
throw new InvalidOperationException("Override does not require approval.");
}
var now = _timeProvider.GetUtcNow();
var updated = existing with
{
Status = OverrideStatus.Active,
Audit = existing.Audit with
{
ApprovedBy = approvedBy,
ApprovedAt = now,
LastModifiedAt = now,
LastModifiedBy = approvedBy
}
};
_overrides[overrideId] = updated;
return updated;
}
/// <summary>
/// Disables an override.
/// </summary>
public AuditedOverride? Disable(string overrideId, string disabledBy, string? reason = null)
{
if (!_overrides.TryGetValue(overrideId, out var existing))
{
return null;
}
var now = _timeProvider.GetUtcNow();
var updated = existing with
{
Status = OverrideStatus.Disabled,
Audit = existing.Audit with
{
LastModifiedAt = now,
LastModifiedBy = disabledBy
}
};
_overrides[overrideId] = updated;
return updated;
}
/// <summary>
/// Deletes an override.
/// </summary>
public bool Delete(string overrideId)
{
if (_overrides.TryRemove(overrideId, out var removed))
{
RemoveFromIndex(removed);
return true;
}
return false;
}
/// <summary>
/// Records an override application for audit trail.
/// </summary>
public void RecordApplication(
string overrideId,
string findingId,
object? originalValue,
object? appliedValue,
Dictionary<string, object?>? context = null)
{
var record = new OverrideApplicationRecord(
OverrideId: overrideId,
FindingId: findingId,
AppliedAt: _timeProvider.GetUtcNow(),
OriginalValue: originalValue,
AppliedValue: appliedValue,
Context: context ?? new Dictionary<string, object?>());
var history = _applicationHistory.GetOrAdd(overrideId, _ => new List<OverrideApplicationRecord>());
lock (history)
{
history.Add(record);
// Keep only last 1000 records per override
if (history.Count > 1000)
{
history.RemoveRange(0, history.Count - 1000);
}
}
}
/// <summary>
/// Gets application history for an override.
/// </summary>
public IReadOnlyList<OverrideApplicationRecord> GetApplicationHistory(string overrideId, int limit = 100)
{
if (!_applicationHistory.TryGetValue(overrideId, out var history))
{
return Array.Empty<OverrideApplicationRecord>();
}
lock (history)
{
return history
.OrderByDescending(r => r.AppliedAt)
.Take(limit)
.ToList()
.AsReadOnly();
}
}
/// <summary>
/// Evaluates whether a finding matches an override's predicate.
/// </summary>
public bool EvaluatePredicate(OverridePredicate predicate, Dictionary<string, object?> signals)
{
if (predicate.Conditions.Count == 0)
{
return true;
}
var results = predicate.Conditions.Select(c => EvaluateCondition(c, signals));
return predicate.MatchMode == PredicateMatchMode.All
? results.All(r => r)
: results.Any(r => r);
}
private bool EvaluateCondition(OverrideCondition condition, Dictionary<string, object?> signals)
{
if (!signals.TryGetValue(condition.Field, out var actualValue))
{
return false;
}
return condition.Operator switch
{
ConditionOperator.Equals => ValuesEqual(actualValue, condition.Value),
ConditionOperator.NotEquals => !ValuesEqual(actualValue, condition.Value),
ConditionOperator.GreaterThan => CompareValues(actualValue, condition.Value) > 0,
ConditionOperator.GreaterThanOrEqual => CompareValues(actualValue, condition.Value) >= 0,
ConditionOperator.LessThan => CompareValues(actualValue, condition.Value) < 0,
ConditionOperator.LessThanOrEqual => CompareValues(actualValue, condition.Value) <= 0,
ConditionOperator.In => IsInCollection(actualValue, condition.Value),
ConditionOperator.NotIn => !IsInCollection(actualValue, condition.Value),
ConditionOperator.Contains => ContainsValue(actualValue, condition.Value),
ConditionOperator.Regex => MatchesRegex(actualValue, condition.Value),
_ => false
};
}
private bool IsActive(AuditedOverride over, DateTimeOffset asOf)
{
if (over.Status != OverrideStatus.Active)
{
return false;
}
if (over.Expiration.HasValue && asOf > over.Expiration.Value)
{
return false;
}
return true;
}
private static bool PredicatesEqual(OverridePredicate a, OverridePredicate b)
{
if (a.MatchMode != b.MatchMode)
{
return false;
}
if (a.Conditions.Count != b.Conditions.Count)
{
return false;
}
var aConditions = a.Conditions.OrderBy(c => c.Field).ThenBy(c => c.Operator.ToString()).ToList();
var bConditions = b.Conditions.OrderBy(c => c.Field).ThenBy(c => c.Operator.ToString()).ToList();
for (var i = 0; i < aConditions.Count; i++)
{
if (aConditions[i].Field != bConditions[i].Field ||
aConditions[i].Operator != bConditions[i].Operator ||
!ValuesEqual(aConditions[i].Value, bConditions[i].Value))
{
return false;
}
}
return true;
}
private static bool PredicatesOverlap(OverridePredicate a, OverridePredicate b)
{
// Simplified overlap check: if any fields match, consider them overlapping
var aFields = a.Conditions.Select(c => c.Field).ToHashSet(StringComparer.OrdinalIgnoreCase);
var bFields = b.Conditions.Select(c => c.Field).ToHashSet(StringComparer.OrdinalIgnoreCase);
return aFields.Overlaps(bFields);
}
private static bool ActionsContradict(OverrideAction a, OverrideAction b)
{
// Severity actions contradict if they set different severities
if (a.ActionType == OverrideActionType.SetSeverity &&
b.ActionType == OverrideActionType.SetSeverity &&
a.Severity != b.Severity)
{
return true;
}
// Decision actions contradict if they set different decisions
if (a.ActionType == OverrideActionType.SetDecision &&
b.ActionType == OverrideActionType.SetDecision &&
a.Decision != b.Decision)
{
return true;
}
// Exempt and Suppress contradict with any severity/decision setting
if ((a.ActionType == OverrideActionType.Exempt || a.ActionType == OverrideActionType.Suppress) &&
(b.ActionType == OverrideActionType.SetSeverity || b.ActionType == OverrideActionType.SetDecision))
{
return true;
}
if ((b.ActionType == OverrideActionType.Exempt || b.ActionType == OverrideActionType.Suppress) &&
(a.ActionType == OverrideActionType.SetSeverity || a.ActionType == OverrideActionType.SetDecision))
{
return true;
}
return false;
}
private static bool ValuesEqual(object? a, object? b)
{
if (a == null && b == null) return true;
if (a == null || b == null) return false;
if (a is JsonElement jeA && b is JsonElement jeB)
{
return jeA.GetRawText() == jeB.GetRawText();
}
var aStr = ConvertToString(a);
var bStr = ConvertToString(b);
return string.Equals(aStr, bStr, StringComparison.OrdinalIgnoreCase);
}
private static int CompareValues(object? a, object? b)
{
var aNum = ConvertToDouble(a);
var bNum = ConvertToDouble(b);
if (aNum.HasValue && bNum.HasValue)
{
return aNum.Value.CompareTo(bNum.Value);
}
var aStr = ConvertToString(a);
var bStr = ConvertToString(b);
return string.Compare(aStr, bStr, StringComparison.OrdinalIgnoreCase);
}
private static bool IsInCollection(object? actual, object? collection)
{
if (collection == null) return false;
IEnumerable<string>? items = null;
if (collection is JsonElement je && je.ValueKind == JsonValueKind.Array)
{
items = je.EnumerateArray().Select(e => ConvertToString(e));
}
else if (collection is IEnumerable<object> enumerable)
{
items = enumerable.Select(ConvertToString);
}
else if (collection is string str)
{
items = str.Split(',').Select(s => s.Trim());
}
if (items == null) return false;
var actualStr = ConvertToString(actual);
return items.Any(i => string.Equals(i, actualStr, StringComparison.OrdinalIgnoreCase));
}
private static bool ContainsValue(object? actual, object? search)
{
var actualStr = ConvertToString(actual);
var searchStr = ConvertToString(search);
return actualStr.Contains(searchStr, StringComparison.OrdinalIgnoreCase);
}
private static bool MatchesRegex(object? actual, object? pattern)
{
var actualStr = ConvertToString(actual);
var patternStr = ConvertToString(pattern);
try
{
return Regex.IsMatch(actualStr, patternStr, RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100));
}
catch
{
return false;
}
}
private static string ConvertToString(object? value)
{
if (value == null) return string.Empty;
if (value is JsonElement je)
{
return je.ValueKind switch
{
JsonValueKind.String => je.GetString() ?? string.Empty,
JsonValueKind.Number => je.GetRawText(),
JsonValueKind.True => "true",
JsonValueKind.False => "false",
JsonValueKind.Null => string.Empty,
_ => je.GetRawText()
};
}
return value.ToString() ?? string.Empty;
}
private static double? ConvertToDouble(object? value)
{
if (value == null) return null;
if (value is JsonElement je && je.TryGetDouble(out var d))
{
return d;
}
if (value is double dVal) return dVal;
if (value is float fVal) return fVal;
if (value is int iVal) return iVal;
if (value is long lVal) return lVal;
if (value is decimal decVal) return (double)decVal;
if (value is string str && double.TryParse(str, out var parsed))
{
return parsed;
}
return null;
}
private void IndexOverride(AuditedOverride over)
{
var list = _profileIndex.GetOrAdd(over.ProfileId, _ => new List<string>());
lock (list)
{
if (!list.Contains(over.OverrideId))
{
list.Add(over.OverrideId);
}
}
}
private void RemoveFromIndex(AuditedOverride over)
{
if (_profileIndex.TryGetValue(over.ProfileId, out var list))
{
lock (list)
{
list.Remove(over.OverrideId);
}
}
}
private static string GenerateOverrideId(CreateOverrideRequest request, DateTimeOffset timestamp)
{
var seed = $"{request.ProfileId}|{request.OverrideType}|{timestamp:O}|{Guid.NewGuid()}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(seed));
return $"ovr-{Convert.ToHexStringLower(hash)[..16]}";
}
}