feat(api): Implement Console Export Client and Models
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled

- Added ConsoleExportClient for managing export requests and responses.
- Introduced ConsoleExportRequest and ConsoleExportResponse models.
- Implemented methods for creating and retrieving exports with appropriate headers.

feat(crypto): Add Software SM2/SM3 Cryptography Provider

- Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography.
- Added support for signing and verification using SM2 algorithm.
- Included hashing functionality with SM3 algorithm.
- Configured options for loading keys from files and environment gate checks.

test(crypto): Add unit tests for SmSoftCryptoProvider

- Created comprehensive tests for signing, verifying, and hashing functionalities.
- Ensured correct behavior for key management and error handling.

feat(api): Enhance Console Export Models

- Expanded ConsoleExport models to include detailed status and event types.
- Added support for various export formats and notification options.

test(time): Implement TimeAnchorPolicyService tests

- Developed tests for TimeAnchorPolicyService to validate time anchors.
- Covered scenarios for anchor validation, drift calculation, and policy enforcement.
This commit is contained in:
StellaOps Bot
2025-12-07 00:27:33 +02:00
parent 9bd6a73926
commit 0de92144d2
229 changed files with 32351 additions and 1481 deletions

View File

@@ -0,0 +1,401 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using StellaOps.Policy.Registry.Contracts;
using StellaOps.Policy.Registry.Storage;
namespace StellaOps.Policy.Registry.Services;
/// <summary>
/// Default implementation of quick policy simulation service.
/// Evaluates policy rules against provided input and returns violations.
/// </summary>
public sealed partial class PolicySimulationService : IPolicySimulationService
{
private readonly IPolicyPackStore _packStore;
private readonly TimeProvider _timeProvider;
// Regex patterns for input reference extraction
[GeneratedRegex(@"input\.(\w+(?:\.\w+)*)", RegexOptions.None)]
private static partial Regex InputReferenceRegex();
[GeneratedRegex(@"input\[""([^""]+)""\]", RegexOptions.None)]
private static partial Regex InputBracketReferenceRegex();
public PolicySimulationService(IPolicyPackStore packStore, TimeProvider? timeProvider = null)
{
_packStore = packStore ?? throw new ArgumentNullException(nameof(packStore));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<PolicySimulationResponse> SimulateAsync(
Guid tenantId,
Guid packId,
SimulationRequest request,
CancellationToken cancellationToken = default)
{
var start = _timeProvider.GetTimestamp();
var executedAt = _timeProvider.GetUtcNow();
var simulationId = GenerateSimulationId(tenantId, packId, executedAt);
var pack = await _packStore.GetByIdAsync(tenantId, packId, cancellationToken);
if (pack is null)
{
return new PolicySimulationResponse
{
SimulationId = simulationId,
Success = false,
ExecutedAt = executedAt,
DurationMilliseconds = GetElapsedMs(start),
Errors = [new SimulationError { Code = "PACK_NOT_FOUND", Message = $"Policy pack {packId} not found" }]
};
}
return await SimulateRulesInternalAsync(
simulationId,
pack.Rules ?? [],
request,
start,
executedAt,
cancellationToken);
}
public async Task<PolicySimulationResponse> SimulateRulesAsync(
Guid tenantId,
IReadOnlyList<PolicyRule> rules,
SimulationRequest request,
CancellationToken cancellationToken = default)
{
var start = _timeProvider.GetTimestamp();
var executedAt = _timeProvider.GetUtcNow();
var simulationId = GenerateSimulationId(tenantId, Guid.Empty, executedAt);
return await SimulateRulesInternalAsync(
simulationId,
rules,
request,
start,
executedAt,
cancellationToken);
}
public Task<InputValidationResult> ValidateInputAsync(
IReadOnlyDictionary<string, object> input,
CancellationToken cancellationToken = default)
{
var errors = new List<InputValidationError>();
if (input.Count == 0)
{
errors.Add(new InputValidationError
{
Path = "$",
Message = "Input must contain at least one property"
});
}
// Check for common required fields
var commonFields = new[] { "subject", "resource", "action", "context" };
var missingFields = commonFields.Where(f => !input.ContainsKey(f)).ToList();
if (missingFields.Count == commonFields.Length)
{
// Warn if none of the common fields are present
errors.Add(new InputValidationError
{
Path = "$",
Message = $"Input should contain at least one of: {string.Join(", ", commonFields)}"
});
}
return Task.FromResult(errors.Count > 0
? InputValidationResult.Invalid(errors)
: InputValidationResult.Valid());
}
private async Task<PolicySimulationResponse> SimulateRulesInternalAsync(
string simulationId,
IReadOnlyList<PolicyRule> rules,
SimulationRequest request,
long startTimestamp,
DateTimeOffset executedAt,
CancellationToken cancellationToken)
{
var violations = new List<SimulatedViolation>();
var errors = new List<SimulationError>();
var trace = new List<string>();
int rulesMatched = 0;
var enabledRules = rules.Where(r => r.Enabled).ToList();
foreach (var rule in enabledRules)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var (matched, violation, traceEntry) = EvaluateRule(rule, request.Input, request.Options);
if (request.Options?.Trace == true && traceEntry is not null)
{
trace.Add(traceEntry);
}
if (matched)
{
rulesMatched++;
if (violation is not null)
{
violations.Add(violation);
}
}
}
catch (Exception ex)
{
errors.Add(new SimulationError
{
RuleId = rule.RuleId,
Code = "EVALUATION_ERROR",
Message = ex.Message
});
}
}
var elapsed = GetElapsedMs(startTimestamp);
var severityCounts = violations
.GroupBy(v => v.Severity.ToLowerInvariant())
.ToDictionary(g => g.Key, g => g.Count());
var summary = new SimulationSummary
{
TotalRulesEvaluated = enabledRules.Count,
RulesMatched = rulesMatched,
ViolationsFound = violations.Count,
ViolationsBySeverity = severityCounts
};
var result = new SimulationResult
{
Result = new Dictionary<string, object>
{
["allow"] = violations.Count == 0,
["violations_count"] = violations.Count
},
Violations = violations.Count > 0 ? violations : null,
Trace = request.Options?.Trace == true && trace.Count > 0 ? trace : null,
Explain = request.Options?.Explain == true ? BuildExplainTrace(enabledRules, request.Input) : null
};
return new PolicySimulationResponse
{
SimulationId = simulationId,
Success = errors.Count == 0,
ExecutedAt = executedAt,
DurationMilliseconds = elapsed,
Result = result,
Summary = summary,
Errors = errors.Count > 0 ? errors : null
};
}
private (bool matched, SimulatedViolation? violation, string? trace) EvaluateRule(
PolicyRule rule,
IReadOnlyDictionary<string, object> input,
SimulationOptions? options)
{
// If no Rego code, use basic rule matching based on severity and name
if (string.IsNullOrWhiteSpace(rule.Rego))
{
// Without Rego, we do pattern-based matching on rule name/description
var matched = MatchRuleByName(rule, input);
var trace = options?.Trace == true
? $"Rule {rule.RuleId}: matched={matched} (no Rego, name-based)"
: null;
if (matched)
{
var violation = new SimulatedViolation
{
RuleId = rule.RuleId,
Severity = rule.Severity.ToString().ToLowerInvariant(),
Message = rule.Description ?? $"Violation of rule {rule.Name}"
};
return (true, violation, trace);
}
return (false, null, trace);
}
// Evaluate Rego-based rule
var regoResult = EvaluateRegoRule(rule, input);
var regoTrace = options?.Trace == true
? $"Rule {rule.RuleId}: matched={regoResult.matched}, inputs_used={string.Join(",", regoResult.inputsUsed)}"
: null;
if (regoResult.matched)
{
var violation = new SimulatedViolation
{
RuleId = rule.RuleId,
Severity = rule.Severity.ToString().ToLowerInvariant(),
Message = rule.Description ?? $"Violation of rule {rule.Name}",
Context = regoResult.context
};
return (true, violation, regoTrace);
}
return (false, null, regoTrace);
}
private static bool MatchRuleByName(PolicyRule rule, IReadOnlyDictionary<string, object> input)
{
// Simple heuristic matching for rules without Rego
var ruleName = rule.Name.ToLowerInvariant();
var ruleDesc = rule.Description?.ToLowerInvariant() ?? "";
// Check if any input key matches rule keywords
foreach (var (key, value) in input)
{
var keyLower = key.ToLowerInvariant();
var valueLower = value?.ToString()?.ToLowerInvariant() ?? "";
if (ruleName.Contains(keyLower) || ruleDesc.Contains(keyLower))
{
return true;
}
if (ruleName.Contains(valueLower) || ruleDesc.Contains(valueLower))
{
return true;
}
}
return false;
}
private (bool matched, HashSet<string> inputsUsed, IReadOnlyDictionary<string, object>? context) EvaluateRegoRule(
PolicyRule rule,
IReadOnlyDictionary<string, object> input)
{
// Extract input references from Rego code
var inputRefs = ExtractInputReferences(rule.Rego!);
var inputsUsed = new HashSet<string>();
var context = new Dictionary<string, object>();
// Simple evaluation: check if referenced inputs exist and have values
bool allInputsPresent = true;
foreach (var inputRef in inputRefs)
{
var value = GetNestedValue(input, inputRef);
if (value is not null)
{
inputsUsed.Add(inputRef);
context[inputRef] = value;
}
else
{
allInputsPresent = false;
}
}
// For this simplified simulation:
// - Rule matches if all referenced inputs are present
// - This simulates the rule being able to evaluate
var matched = inputRefs.Count > 0 && allInputsPresent;
return (matched, inputsUsed, context.Count > 0 ? context : null);
}
private static HashSet<string> ExtractInputReferences(string rego)
{
var refs = new HashSet<string>(StringComparer.Ordinal);
// Match input.field.subfield pattern
foreach (Match match in InputReferenceRegex().Matches(rego))
{
refs.Add(match.Groups[1].Value);
}
// Match input["field"] pattern
foreach (Match match in InputBracketReferenceRegex().Matches(rego))
{
refs.Add(match.Groups[1].Value);
}
return refs;
}
private static object? GetNestedValue(IReadOnlyDictionary<string, object> input, string path)
{
var parts = path.Split('.');
object? current = input;
foreach (var part in parts)
{
if (current is IReadOnlyDictionary<string, object> dict)
{
if (!dict.TryGetValue(part, out current))
{
return null;
}
}
else if (current is JsonElement jsonElement)
{
if (jsonElement.ValueKind == JsonValueKind.Object &&
jsonElement.TryGetProperty(part, out var prop))
{
current = prop;
}
else
{
return null;
}
}
else
{
return null;
}
}
return current;
}
private static PolicyExplainTrace BuildExplainTrace(
IReadOnlyList<PolicyRule> rules,
IReadOnlyDictionary<string, object> input)
{
var steps = new List<object>();
steps.Add(new { type = "input_received", keys = input.Keys.ToList() });
foreach (var rule in rules)
{
steps.Add(new
{
type = "rule_evaluation",
rule_id = rule.RuleId,
rule_name = rule.Name,
severity = rule.Severity.ToString(),
has_rego = !string.IsNullOrWhiteSpace(rule.Rego)
});
}
steps.Add(new { type = "evaluation_complete", rules_count = rules.Count });
return new PolicyExplainTrace { Steps = steps };
}
private static string GenerateSimulationId(Guid tenantId, Guid packId, DateTimeOffset timestamp)
{
var content = $"{tenantId}:{packId}:{timestamp.ToUnixTimeMilliseconds()}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
return $"sim_{Convert.ToHexString(hash)[..16].ToLowerInvariant()}";
}
private long GetElapsedMs(long startTimestamp)
{
var elapsed = _timeProvider.GetElapsedTime(startTimestamp, _timeProvider.GetTimestamp());
return (long)Math.Ceiling(elapsed.TotalMilliseconds);
}
}