using System.Collections.Immutable; using System.Security.Cryptography; using StellaOps.Policy; namespace StellaOps.PolicyDsl; /// /// Compiles policy DSL source code into an intermediate representation. /// public sealed class PolicyCompiler { public PolicyCompilationResult Compile(string source) { if (source is null) { throw new ArgumentNullException(nameof(source)); } var parseResult = PolicyParser.Parse(source); if (parseResult.Document is null) { return new PolicyCompilationResult( Success: false, Document: null, Checksum: null, CanonicalRepresentation: ImmutableArray.Empty, Diagnostics: parseResult.Diagnostics); } if (parseResult.Diagnostics.Any(static issue => issue.Severity == PolicyIssueSeverity.Error)) { return new PolicyCompilationResult( Success: false, Document: null, Checksum: null, CanonicalRepresentation: ImmutableArray.Empty, Diagnostics: parseResult.Diagnostics); } var irDocument = BuildIntermediateRepresentation(parseResult.Document); var canonical = PolicyIrSerializer.Serialize(irDocument); var checksum = Convert.ToHexString(SHA256.HashData(canonical.AsSpan())).ToLowerInvariant(); return new PolicyCompilationResult( Success: true, Document: irDocument, Checksum: checksum, CanonicalRepresentation: canonical, Diagnostics: parseResult.Diagnostics); } private static PolicyIrDocument BuildIntermediateRepresentation(PolicyDocumentNode node) { var metadata = node.Metadata .OrderBy(static kvp => kvp.Key, StringComparer.Ordinal) .ToImmutableSortedDictionary(static kvp => kvp.Key, kvp => ToIrLiteral(kvp.Value), StringComparer.Ordinal); var settings = node.Settings .OrderBy(static kvp => kvp.Key, StringComparer.Ordinal) .ToImmutableSortedDictionary(static kvp => kvp.Key, kvp => ToIrLiteral(kvp.Value), StringComparer.Ordinal); var profiles = ImmutableArray.CreateBuilder(node.Profiles.Length); foreach (var profile in node.Profiles) { var maps = ImmutableArray.CreateBuilder(); var envs = ImmutableArray.CreateBuilder(); var scalars = ImmutableArray.CreateBuilder(); foreach (var item in profile.Items) { switch (item) { case PolicyProfileMapNode map: maps.Add(new PolicyIrProfileMap( map.Name, map.Entries .Select(entry => new PolicyIrProfileMapEntry(entry.Source, entry.Weight)) .ToImmutableArray())); break; case PolicyProfileEnvNode env: envs.Add(new PolicyIrProfileEnv( env.Name, env.Entries .Select(entry => new PolicyIrProfileEnvEntry(entry.Condition, entry.Weight)) .ToImmutableArray())); break; case PolicyProfileScalarNode scalar: scalars.Add(new PolicyIrProfileScalar(scalar.Name, ToIrLiteral(scalar.Value))); break; } } profiles.Add(new PolicyIrProfile( profile.Name, maps.ToImmutable(), envs.ToImmutable(), scalars.ToImmutable())); } var rules = ImmutableArray.CreateBuilder(node.Rules.Length); foreach (var rule in node.Rules) { var thenActions = ImmutableArray.CreateBuilder(rule.ThenActions.Length); foreach (var action in rule.ThenActions) { var converted = ToIrAction(action); if (converted is not null) { thenActions.Add(converted); } } var elseActions = ImmutableArray.CreateBuilder(rule.ElseActions.Length); foreach (var action in rule.ElseActions) { var converted = ToIrAction(action); if (converted is not null) { elseActions.Add(converted); } } rules.Add(new PolicyIrRule( rule.Name, rule.Priority, rule.When, thenActions.ToImmutable(), elseActions.ToImmutable(), rule.Because ?? string.Empty)); } return new PolicyIrDocument( node.Name, node.Syntax, metadata, profiles.ToImmutable(), settings, rules.ToImmutable()); } private static PolicyIrLiteral ToIrLiteral(PolicyLiteralValue value) => value switch { PolicyStringLiteral s => new PolicyIrStringLiteral(s.Value), PolicyNumberLiteral n => new PolicyIrNumberLiteral(n.Value), PolicyBooleanLiteral b => new PolicyIrBooleanLiteral(b.Value), PolicyListLiteral list => new PolicyIrListLiteral(list.Items.Select(ToIrLiteral).ToImmutableArray()), _ => new PolicyIrStringLiteral(string.Empty), }; private static PolicyIrAction? ToIrAction(PolicyActionNode action) => action switch { PolicyAssignmentActionNode assign => new PolicyIrAssignmentAction(assign.Target.Segments, assign.Value), PolicyAnnotateActionNode annotate => new PolicyIrAnnotateAction(annotate.Target.Segments, annotate.Value), PolicyIgnoreActionNode ignore => new PolicyIrIgnoreAction(ignore.Until, ignore.Because), PolicyEscalateActionNode escalate => new PolicyIrEscalateAction(escalate.To, escalate.When), PolicyRequireVexActionNode require => new PolicyIrRequireVexAction( require.Conditions .OrderBy(static kvp => kvp.Key, StringComparer.Ordinal) .ToImmutableSortedDictionary(static kvp => kvp.Key, kvp => kvp.Value, StringComparer.Ordinal)), PolicyWarnActionNode warn => new PolicyIrWarnAction(warn.Message), PolicyDeferActionNode defer => new PolicyIrDeferAction(defer.Until), _ => null, }; } /// /// Result of compiling a policy DSL source. /// public sealed record PolicyCompilationResult( bool Success, PolicyIrDocument? Document, string? Checksum, ImmutableArray CanonicalRepresentation, ImmutableArray Diagnostics);