up
This commit is contained in:
@@ -1,160 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Compilation;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a precise source location within a policy DSL document.
|
||||
/// </summary>
|
||||
public readonly struct SourceLocation : IEquatable<SourceLocation>, IComparable<SourceLocation>
|
||||
{
|
||||
public SourceLocation(int offset, int line, int column)
|
||||
{
|
||||
if (offset < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
}
|
||||
|
||||
if (line < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(line));
|
||||
}
|
||||
|
||||
if (column < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(column));
|
||||
}
|
||||
|
||||
Offset = offset;
|
||||
Line = line;
|
||||
Column = column;
|
||||
}
|
||||
|
||||
public int Offset { get; }
|
||||
|
||||
public int Line { get; }
|
||||
|
||||
public int Column { get; }
|
||||
|
||||
public override string ToString() => $"(L{Line}, C{Column})";
|
||||
|
||||
public bool Equals(SourceLocation other) =>
|
||||
Offset == other.Offset && Line == other.Line && Column == other.Column;
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj) =>
|
||||
obj is SourceLocation other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(Offset, Line, Column);
|
||||
|
||||
public int CompareTo(SourceLocation other) => Offset.CompareTo(other.Offset);
|
||||
|
||||
public static bool operator ==(SourceLocation left, SourceLocation right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(SourceLocation left, SourceLocation right) => !left.Equals(right);
|
||||
|
||||
public static bool operator <(SourceLocation left, SourceLocation right) => left.CompareTo(right) < 0;
|
||||
|
||||
public static bool operator <=(SourceLocation left, SourceLocation right) => left.CompareTo(right) <= 0;
|
||||
|
||||
public static bool operator >(SourceLocation left, SourceLocation right) => left.CompareTo(right) > 0;
|
||||
|
||||
public static bool operator >=(SourceLocation left, SourceLocation right) => left.CompareTo(right) >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a start/end location pair within a policy DSL source document.
|
||||
/// </summary>
|
||||
public readonly struct SourceSpan : IEquatable<SourceSpan>
|
||||
{
|
||||
public SourceSpan(SourceLocation start, SourceLocation end)
|
||||
{
|
||||
if (start.Offset > end.Offset)
|
||||
{
|
||||
throw new ArgumentException("Start must not be after end.", nameof(start));
|
||||
}
|
||||
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
|
||||
public SourceLocation Start { get; }
|
||||
|
||||
public SourceLocation End { get; }
|
||||
|
||||
public override string ToString() => $"{Start}->{End}";
|
||||
|
||||
public bool Equals(SourceSpan other) => Start.Equals(other.Start) && End.Equals(other.End);
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj) => obj is SourceSpan other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(Start, End);
|
||||
|
||||
public static SourceSpan Combine(SourceSpan first, SourceSpan second)
|
||||
{
|
||||
var start = first.Start <= second.Start ? first.Start : second.Start;
|
||||
var end = first.End >= second.End ? first.End : second.End;
|
||||
return new SourceSpan(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
internal enum TokenKind
|
||||
{
|
||||
EndOfFile = 0,
|
||||
Identifier,
|
||||
StringLiteral,
|
||||
NumberLiteral,
|
||||
BooleanLiteral,
|
||||
LeftBrace,
|
||||
RightBrace,
|
||||
LeftParen,
|
||||
RightParen,
|
||||
LeftBracket,
|
||||
RightBracket,
|
||||
Comma,
|
||||
Semicolon,
|
||||
Colon,
|
||||
Arrow, // =>
|
||||
Assign, // =
|
||||
Define, // :=
|
||||
Dot,
|
||||
KeywordPolicy,
|
||||
KeywordSyntax,
|
||||
KeywordMetadata,
|
||||
KeywordProfile,
|
||||
KeywordRule,
|
||||
KeywordMap,
|
||||
KeywordSource,
|
||||
KeywordEnv,
|
||||
KeywordIf,
|
||||
KeywordThen,
|
||||
KeywordWhen,
|
||||
KeywordAnd,
|
||||
KeywordOr,
|
||||
KeywordNot,
|
||||
KeywordPriority,
|
||||
KeywordElse,
|
||||
KeywordBecause,
|
||||
KeywordSettings,
|
||||
KeywordIgnore,
|
||||
KeywordUntil,
|
||||
KeywordEscalate,
|
||||
KeywordTo,
|
||||
KeywordRequireVex,
|
||||
KeywordWarn,
|
||||
KeywordMessage,
|
||||
KeywordDefer,
|
||||
KeywordAnnotate,
|
||||
KeywordIn,
|
||||
EqualEqual,
|
||||
NotEqual,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
GreaterThan,
|
||||
GreaterThanOrEqual,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
internal readonly record struct DslToken(
|
||||
TokenKind Kind,
|
||||
string Text,
|
||||
SourceSpan Span,
|
||||
object? Value = null);
|
||||
@@ -1,576 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using StellaOps.Policy;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Compilation;
|
||||
|
||||
internal static class DslTokenizer
|
||||
{
|
||||
public static TokenizerResult Tokenize(string source)
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
var tokens = ImmutableArray.CreateBuilder<DslToken>();
|
||||
var diagnostics = ImmutableArray.CreateBuilder<PolicyIssue>();
|
||||
|
||||
var index = 0;
|
||||
var line = 1;
|
||||
var column = 1;
|
||||
|
||||
while (index < source.Length)
|
||||
{
|
||||
var current = source[index];
|
||||
if (char.IsWhiteSpace(current))
|
||||
{
|
||||
(index, line, column) = AdvanceWhitespace(source, index, line, column);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == '/' && index + 1 < source.Length)
|
||||
{
|
||||
if (source[index + 1] == '/')
|
||||
{
|
||||
(index, line, column) = SkipSingleLineComment(source, index + 2, line, column + 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source[index + 1] == '*')
|
||||
{
|
||||
(index, line, column) = SkipMultiLineComment(source, index + 2, line, column + 2, diagnostics);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var startLocation = new SourceLocation(index, line, column);
|
||||
switch (current)
|
||||
{
|
||||
case '{':
|
||||
tokens.Add(CreateToken(TokenKind.LeftBrace, "{", startLocation, ref index, ref column));
|
||||
break;
|
||||
case '}':
|
||||
tokens.Add(CreateToken(TokenKind.RightBrace, "}", startLocation, ref index, ref column));
|
||||
break;
|
||||
case '(':
|
||||
tokens.Add(CreateToken(TokenKind.LeftParen, "(", startLocation, ref index, ref column));
|
||||
break;
|
||||
case ')':
|
||||
tokens.Add(CreateToken(TokenKind.RightParen, ")", startLocation, ref index, ref column));
|
||||
break;
|
||||
case '[':
|
||||
tokens.Add(CreateToken(TokenKind.LeftBracket, "[", startLocation, ref index, ref column));
|
||||
break;
|
||||
case ']':
|
||||
tokens.Add(CreateToken(TokenKind.RightBracket, "]", startLocation, ref index, ref column));
|
||||
break;
|
||||
case ',':
|
||||
tokens.Add(CreateToken(TokenKind.Comma, ",", startLocation, ref index, ref column));
|
||||
break;
|
||||
case ';':
|
||||
tokens.Add(CreateToken(TokenKind.Semicolon, ";", startLocation, ref index, ref column));
|
||||
break;
|
||||
case ':':
|
||||
{
|
||||
if (Match(source, index + 1, '='))
|
||||
{
|
||||
tokens.Add(CreateToken(TokenKind.Define, ":=", startLocation, ref index, ref column, advance: 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
tokens.Add(CreateToken(TokenKind.Colon, ":", startLocation, ref index, ref column));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case '=':
|
||||
{
|
||||
if (Match(source, index + 1, '>'))
|
||||
{
|
||||
tokens.Add(CreateToken(TokenKind.Arrow, "=>", startLocation, ref index, ref column, advance: 2));
|
||||
}
|
||||
else if (Match(source, index + 1, '='))
|
||||
{
|
||||
tokens.Add(CreateToken(TokenKind.EqualEqual, "==", startLocation, ref index, ref column, advance: 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
tokens.Add(CreateToken(TokenKind.Assign, "=", startLocation, ref index, ref column));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case '!':
|
||||
{
|
||||
if (Match(source, index + 1, '='))
|
||||
{
|
||||
tokens.Add(CreateToken(TokenKind.NotEqual, "!=", startLocation, ref index, ref column, advance: 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
ReportUnexpectedCharacter(diagnostics, current, startLocation);
|
||||
index++;
|
||||
column++;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case '<':
|
||||
{
|
||||
if (Match(source, index + 1, '='))
|
||||
{
|
||||
tokens.Add(CreateToken(TokenKind.LessThanOrEqual, "<=", startLocation, ref index, ref column, advance: 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
tokens.Add(CreateToken(TokenKind.LessThan, "<", startLocation, ref index, ref column));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case '>':
|
||||
{
|
||||
if (Match(source, index + 1, '='))
|
||||
{
|
||||
tokens.Add(CreateToken(TokenKind.GreaterThanOrEqual, ">=", startLocation, ref index, ref column, advance: 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
tokens.Add(CreateToken(TokenKind.GreaterThan, ">", startLocation, ref index, ref column));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case '.':
|
||||
tokens.Add(CreateToken(TokenKind.Dot, ".", startLocation, ref index, ref column));
|
||||
break;
|
||||
case '"':
|
||||
TokenizeString(source, ref index, ref line, ref column, startLocation, tokens, diagnostics);
|
||||
break;
|
||||
case '+':
|
||||
case '-':
|
||||
{
|
||||
if (index + 1 < source.Length && char.IsDigit(source[index + 1]))
|
||||
{
|
||||
TokenizeNumber(source, ref index, ref line, ref column, startLocation, tokens, diagnostics);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReportUnexpectedCharacter(diagnostics, current, startLocation);
|
||||
index++;
|
||||
column++;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
if (char.IsDigit(current))
|
||||
{
|
||||
TokenizeNumber(source, ref index, ref line, ref column, startLocation, tokens, diagnostics);
|
||||
}
|
||||
else if (IsIdentifierStart(current))
|
||||
{
|
||||
TokenizeIdentifierOrKeyword(source, ref index, ref line, ref column, startLocation, tokens);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReportUnexpectedCharacter(diagnostics, current, startLocation);
|
||||
index++;
|
||||
column++;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var eofLocation = new SourceLocation(index, line, column);
|
||||
tokens.Add(new DslToken(TokenKind.EndOfFile, string.Empty, new SourceSpan(eofLocation, eofLocation)));
|
||||
|
||||
return new TokenizerResult(tokens.ToImmutable(), diagnostics.ToImmutable());
|
||||
}
|
||||
|
||||
private static void TokenizeString(
|
||||
string source,
|
||||
ref int index,
|
||||
ref int line,
|
||||
ref int column,
|
||||
SourceLocation start,
|
||||
ImmutableArray<DslToken>.Builder tokens,
|
||||
ImmutableArray<PolicyIssue>.Builder diagnostics)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
var i = index + 1;
|
||||
var currentLine = line;
|
||||
var currentColumn = column + 1;
|
||||
|
||||
while (i < source.Length)
|
||||
{
|
||||
var ch = source[i];
|
||||
if (ch == '"')
|
||||
{
|
||||
var end = new SourceLocation(i + 1, currentLine, currentColumn + 1);
|
||||
index = i + 1;
|
||||
column = currentColumn + 1;
|
||||
tokens.Add(new DslToken(TokenKind.StringLiteral, builder.ToString(), new SourceSpan(start, end), builder.ToString()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ch == '\\')
|
||||
{
|
||||
if (i + 1 >= source.Length)
|
||||
{
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.UnterminatedString, "Unterminated string literal.", $"@{start.Line}:{start.Column}"));
|
||||
index = source.Length;
|
||||
line = currentLine;
|
||||
column = currentColumn;
|
||||
return;
|
||||
}
|
||||
|
||||
var escape = source[i + 1];
|
||||
switch (escape)
|
||||
{
|
||||
case '\\':
|
||||
builder.Append('\\');
|
||||
break;
|
||||
case '"':
|
||||
builder.Append('"');
|
||||
break;
|
||||
case 'n':
|
||||
builder.Append('\n');
|
||||
break;
|
||||
case 'r':
|
||||
builder.Append('\r');
|
||||
break;
|
||||
case 't':
|
||||
builder.Append('\t');
|
||||
break;
|
||||
default:
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.InvalidEscapeSequence, $"Invalid escape sequence '\\{escape}'.", $"@{currentLine}:{currentColumn}"));
|
||||
builder.Append(escape);
|
||||
break;
|
||||
}
|
||||
|
||||
i += 2;
|
||||
currentColumn += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '\r' || ch == '\n')
|
||||
{
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.UnterminatedString, "Unterminated string literal.", $"@{start.Line}:{start.Column}"));
|
||||
(index, line, column) = AdvanceWhitespace(source, i, currentLine, currentColumn);
|
||||
return;
|
||||
}
|
||||
|
||||
builder.Append(ch);
|
||||
i++;
|
||||
currentColumn++;
|
||||
}
|
||||
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.UnterminatedString, "Unterminated string literal.", $"@{start.Line}:{start.Column}"));
|
||||
index = source.Length;
|
||||
line = currentLine;
|
||||
column = currentColumn;
|
||||
}
|
||||
|
||||
private static void TokenizeNumber(
|
||||
string source,
|
||||
ref int index,
|
||||
ref int line,
|
||||
ref int column,
|
||||
SourceLocation start,
|
||||
ImmutableArray<DslToken>.Builder tokens,
|
||||
ImmutableArray<PolicyIssue>.Builder diagnostics)
|
||||
{
|
||||
var i = index;
|
||||
var hasDecimal = false;
|
||||
|
||||
if (source[i] == '+' || source[i] == '-')
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
while (i < source.Length)
|
||||
{
|
||||
var ch = source[i];
|
||||
if (char.IsDigit(ch))
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '.')
|
||||
{
|
||||
if (hasDecimal)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
hasDecimal = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var percent = false;
|
||||
if (i < source.Length && source[i] == '%')
|
||||
{
|
||||
percent = true;
|
||||
i++;
|
||||
}
|
||||
|
||||
var text = source.Substring(index, i - index);
|
||||
if (!decimal.TryParse(text, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.InvalidNumber, $"Invalid numeric literal '{text}'.", $"@{start.Line}:{start.Column}"));
|
||||
index = i;
|
||||
column += i - index;
|
||||
return;
|
||||
}
|
||||
|
||||
if (percent)
|
||||
{
|
||||
value /= 100m;
|
||||
}
|
||||
|
||||
var end = new SourceLocation(i, line, column + (i - index));
|
||||
tokens.Add(new DslToken(TokenKind.NumberLiteral, text, new SourceSpan(start, end), value));
|
||||
column += i - index;
|
||||
index = i;
|
||||
}
|
||||
|
||||
private static void TokenizeIdentifierOrKeyword(
|
||||
string source,
|
||||
ref int index,
|
||||
ref int line,
|
||||
ref int column,
|
||||
SourceLocation start,
|
||||
ImmutableArray<DslToken>.Builder tokens)
|
||||
{
|
||||
var i = index + 1;
|
||||
while (i < source.Length && IsIdentifierPart(source[i]))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
var text = source.Substring(index, i - index);
|
||||
var kind = GetKeywordKind(text);
|
||||
|
||||
if (kind == TokenKind.BooleanLiteral)
|
||||
{
|
||||
var value = string.Equals(text, "true", StringComparison.Ordinal);
|
||||
var end = new SourceLocation(i, line, column + (i - index));
|
||||
tokens.Add(new DslToken(TokenKind.BooleanLiteral, text, new SourceSpan(start, end), value));
|
||||
}
|
||||
else if (kind == TokenKind.Identifier)
|
||||
{
|
||||
var end = new SourceLocation(i, line, column + (i - index));
|
||||
tokens.Add(new DslToken(TokenKind.Identifier, text, new SourceSpan(start, end)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var end = new SourceLocation(i, line, column + (i - index));
|
||||
tokens.Add(new DslToken(kind, text, new SourceSpan(start, end)));
|
||||
}
|
||||
|
||||
column += i - index;
|
||||
index = i;
|
||||
}
|
||||
|
||||
private static TokenKind GetKeywordKind(string text)
|
||||
{
|
||||
return text switch
|
||||
{
|
||||
"policy" => TokenKind.KeywordPolicy,
|
||||
"syntax" => TokenKind.KeywordSyntax,
|
||||
"metadata" => TokenKind.KeywordMetadata,
|
||||
"profile" => TokenKind.KeywordProfile,
|
||||
"rule" => TokenKind.KeywordRule,
|
||||
"map" => TokenKind.KeywordMap,
|
||||
"source" => TokenKind.KeywordSource,
|
||||
"env" => TokenKind.Identifier,
|
||||
"if" => TokenKind.KeywordIf,
|
||||
"then" => TokenKind.KeywordThen,
|
||||
"when" => TokenKind.KeywordWhen,
|
||||
"and" => TokenKind.KeywordAnd,
|
||||
"or" => TokenKind.KeywordOr,
|
||||
"not" => TokenKind.KeywordNot,
|
||||
"priority" => TokenKind.KeywordPriority,
|
||||
"else" => TokenKind.KeywordElse,
|
||||
"because" => TokenKind.KeywordBecause,
|
||||
"settings" => TokenKind.KeywordSettings,
|
||||
"ignore" => TokenKind.KeywordIgnore,
|
||||
"until" => TokenKind.KeywordUntil,
|
||||
"escalate" => TokenKind.KeywordEscalate,
|
||||
"to" => TokenKind.KeywordTo,
|
||||
"requireVex" => TokenKind.KeywordRequireVex,
|
||||
"warn" => TokenKind.KeywordWarn,
|
||||
"message" => TokenKind.KeywordMessage,
|
||||
"defer" => TokenKind.KeywordDefer,
|
||||
"annotate" => TokenKind.KeywordAnnotate,
|
||||
"in" => TokenKind.KeywordIn,
|
||||
"true" => TokenKind.BooleanLiteral,
|
||||
"false" => TokenKind.BooleanLiteral,
|
||||
_ => TokenKind.Identifier,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsIdentifierStart(char ch) => char.IsLetter(ch) || ch == '_';
|
||||
|
||||
private static bool IsIdentifierPart(char ch) => char.IsLetterOrDigit(ch) || ch == '_' || ch == '-';
|
||||
|
||||
private static (int Index, int Line, int Column) AdvanceWhitespace(string source, int index, int line, int column)
|
||||
{
|
||||
var i = index;
|
||||
var currentLine = line;
|
||||
var currentColumn = column;
|
||||
|
||||
while (i < source.Length)
|
||||
{
|
||||
var ch = source[i];
|
||||
if (ch == '\r')
|
||||
{
|
||||
if (i + 1 < source.Length && source[i + 1] == '\n')
|
||||
{
|
||||
i += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
currentLine++;
|
||||
currentColumn = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '\n')
|
||||
{
|
||||
i++;
|
||||
currentLine++;
|
||||
currentColumn = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!char.IsWhiteSpace(ch))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
i++;
|
||||
currentColumn++;
|
||||
}
|
||||
|
||||
return (i, currentLine, currentColumn);
|
||||
}
|
||||
|
||||
private static (int Index, int Line, int Column) SkipSingleLineComment(string source, int index, int line, int column)
|
||||
{
|
||||
var i = index;
|
||||
var currentLine = line;
|
||||
var currentColumn = column;
|
||||
|
||||
while (i < source.Length)
|
||||
{
|
||||
var ch = source[i];
|
||||
if (ch == '\r' || ch == '\n')
|
||||
{
|
||||
return AdvanceWhitespace(source, i, currentLine, currentColumn);
|
||||
}
|
||||
|
||||
i++;
|
||||
currentColumn++;
|
||||
}
|
||||
|
||||
return (i, currentLine, currentColumn);
|
||||
}
|
||||
|
||||
private static (int Index, int Line, int Column) SkipMultiLineComment(
|
||||
string source,
|
||||
int index,
|
||||
int line,
|
||||
int column,
|
||||
ImmutableArray<PolicyIssue>.Builder diagnostics)
|
||||
{
|
||||
var i = index;
|
||||
var currentLine = line;
|
||||
var currentColumn = column;
|
||||
|
||||
while (i < source.Length)
|
||||
{
|
||||
var ch = source[i];
|
||||
if (ch == '*' && i + 1 < source.Length && source[i + 1] == '/')
|
||||
{
|
||||
return (i + 2, currentLine, currentColumn + 2);
|
||||
}
|
||||
|
||||
if (ch == '\r')
|
||||
{
|
||||
if (i + 1 < source.Length && source[i + 1] == '\n')
|
||||
{
|
||||
i += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
currentLine++;
|
||||
currentColumn = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '\n')
|
||||
{
|
||||
i++;
|
||||
currentLine++;
|
||||
currentColumn = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
currentColumn++;
|
||||
}
|
||||
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.UnexpectedCharacter, "Unterminated comment block.", $"@{line}:{column}"));
|
||||
return (source.Length, currentLine, currentColumn);
|
||||
}
|
||||
|
||||
private static DslToken CreateToken(
|
||||
TokenKind kind,
|
||||
string text,
|
||||
SourceLocation start,
|
||||
ref int index,
|
||||
ref int column,
|
||||
int advance = 1)
|
||||
{
|
||||
var end = new SourceLocation(index + advance, start.Line, start.Column + advance);
|
||||
index += advance;
|
||||
column += advance;
|
||||
return new DslToken(kind, text, new SourceSpan(start, end));
|
||||
}
|
||||
|
||||
private static void ReportUnexpectedCharacter(
|
||||
ImmutableArray<PolicyIssue>.Builder diagnostics,
|
||||
char ch,
|
||||
SourceLocation location)
|
||||
{
|
||||
diagnostics.Add(PolicyIssue.Error(
|
||||
PolicyDslDiagnosticCodes.UnexpectedCharacter,
|
||||
$"Unexpected character '{ch}'.",
|
||||
$"@{location.Line}:{location.Column}"));
|
||||
}
|
||||
|
||||
private static bool Match(string source, int index, char expected) =>
|
||||
index < source.Length && source[index] == expected;
|
||||
}
|
||||
|
||||
internal readonly record struct TokenizerResult(
|
||||
ImmutableArray<DslToken> Tokens,
|
||||
ImmutableArray<PolicyIssue> Diagnostics);
|
||||
@@ -1,169 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using StellaOps.Policy;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Compilation;
|
||||
|
||||
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<byte>.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<byte>.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<PolicyIrProfile>(node.Profiles.Length);
|
||||
foreach (var profile in node.Profiles)
|
||||
{
|
||||
var maps = ImmutableArray.CreateBuilder<PolicyIrProfileMap>();
|
||||
var envs = ImmutableArray.CreateBuilder<PolicyIrProfileEnv>();
|
||||
var scalars = ImmutableArray.CreateBuilder<PolicyIrProfileScalar>();
|
||||
|
||||
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<PolicyIrRule>(node.Rules.Length);
|
||||
foreach (var rule in node.Rules)
|
||||
{
|
||||
var thenActions = ImmutableArray.CreateBuilder<PolicyIrAction>(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<PolicyIrAction>(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,
|
||||
};
|
||||
}
|
||||
|
||||
public sealed record PolicyCompilationResult(
|
||||
bool Success,
|
||||
PolicyIrDocument? Document,
|
||||
string? Checksum,
|
||||
ImmutableArray<byte> CanonicalRepresentation,
|
||||
ImmutableArray<PolicyIssue> Diagnostics);
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.PolicyDsl;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Compilation;
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
namespace StellaOps.Policy.Engine.Compilation;
|
||||
|
||||
internal static class PolicyDslDiagnosticCodes
|
||||
{
|
||||
public const string UnexpectedCharacter = "POLICY-DSL-LEX-001";
|
||||
public const string UnterminatedString = "POLICY-DSL-LEX-002";
|
||||
public const string InvalidEscapeSequence = "POLICY-DSL-LEX-003";
|
||||
public const string InvalidNumber = "POLICY-DSL-LEX-004";
|
||||
public const string UnexpectedToken = "POLICY-DSL-PARSE-001";
|
||||
public const string DuplicateSection = "POLICY-DSL-PARSE-002";
|
||||
public const string MissingPolicyHeader = "POLICY-DSL-PARSE-003";
|
||||
public const string UnsupportedSyntaxVersion = "POLICY-DSL-PARSE-004";
|
||||
public const string DuplicateRuleName = "POLICY-DSL-PARSE-005";
|
||||
public const string MissingBecauseClause = "POLICY-DSL-PARSE-006";
|
||||
public const string MissingTerminator = "POLICY-DSL-PARSE-007";
|
||||
public const string InvalidAction = "POLICY-DSL-PARSE-008";
|
||||
public const string InvalidLiteral = "POLICY-DSL-PARSE-009";
|
||||
public const string UnexpectedSection = "POLICY-DSL-PARSE-010";
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Compilation;
|
||||
|
||||
public sealed record PolicyIrDocument(
|
||||
string Name,
|
||||
string Syntax,
|
||||
ImmutableSortedDictionary<string, PolicyIrLiteral> Metadata,
|
||||
ImmutableArray<PolicyIrProfile> Profiles,
|
||||
ImmutableSortedDictionary<string, PolicyIrLiteral> Settings,
|
||||
ImmutableArray<PolicyIrRule> Rules);
|
||||
|
||||
public abstract record PolicyIrLiteral;
|
||||
|
||||
public sealed record PolicyIrStringLiteral(string Value) : PolicyIrLiteral;
|
||||
|
||||
public sealed record PolicyIrNumberLiteral(decimal Value) : PolicyIrLiteral;
|
||||
|
||||
public sealed record PolicyIrBooleanLiteral(bool Value) : PolicyIrLiteral;
|
||||
|
||||
public sealed record PolicyIrListLiteral(ImmutableArray<PolicyIrLiteral> Items) : PolicyIrLiteral;
|
||||
|
||||
public sealed record PolicyIrProfile(
|
||||
string Name,
|
||||
ImmutableArray<PolicyIrProfileMap> Maps,
|
||||
ImmutableArray<PolicyIrProfileEnv> Environments,
|
||||
ImmutableArray<PolicyIrProfileScalar> Scalars);
|
||||
|
||||
public sealed record PolicyIrProfileMap(string Name, ImmutableArray<PolicyIrProfileMapEntry> Entries);
|
||||
|
||||
public sealed record PolicyIrProfileMapEntry(string Source, decimal Weight);
|
||||
|
||||
public sealed record PolicyIrProfileEnv(string Name, ImmutableArray<PolicyIrProfileEnvEntry> Entries);
|
||||
|
||||
public sealed record PolicyIrProfileEnvEntry(PolicyExpression Condition, decimal Weight);
|
||||
|
||||
public sealed record PolicyIrProfileScalar(string Name, PolicyIrLiteral Value);
|
||||
|
||||
public sealed record PolicyIrRule(
|
||||
string Name,
|
||||
int Priority,
|
||||
PolicyExpression When,
|
||||
ImmutableArray<PolicyIrAction> ThenActions,
|
||||
ImmutableArray<PolicyIrAction> ElseActions,
|
||||
string Because);
|
||||
|
||||
public abstract record PolicyIrAction;
|
||||
|
||||
public sealed record PolicyIrAssignmentAction(ImmutableArray<string> Target, PolicyExpression Value) : PolicyIrAction;
|
||||
|
||||
public sealed record PolicyIrAnnotateAction(ImmutableArray<string> Target, PolicyExpression Value) : PolicyIrAction;
|
||||
|
||||
public sealed record PolicyIrIgnoreAction(PolicyExpression? Until, string? Because) : PolicyIrAction;
|
||||
|
||||
public sealed record PolicyIrEscalateAction(PolicyExpression? To, PolicyExpression? When) : PolicyIrAction;
|
||||
|
||||
public sealed record PolicyIrRequireVexAction(ImmutableSortedDictionary<string, PolicyExpression> Conditions) : PolicyIrAction;
|
||||
|
||||
public sealed record PolicyIrWarnAction(PolicyExpression? Message) : PolicyIrAction;
|
||||
|
||||
public sealed record PolicyIrDeferAction(PolicyExpression? Until) : PolicyIrAction;
|
||||
@@ -1,415 +0,0 @@
|
||||
using System.Buffers;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Compilation;
|
||||
|
||||
internal static class PolicyIrSerializer
|
||||
{
|
||||
public static ImmutableArray<byte> Serialize(PolicyIrDocument document)
|
||||
{
|
||||
var buffer = new ArrayBufferWriter<byte>();
|
||||
using var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
|
||||
{
|
||||
Indented = false,
|
||||
SkipValidation = false
|
||||
});
|
||||
|
||||
WriteDocument(writer, document);
|
||||
writer.Flush();
|
||||
|
||||
return buffer.WrittenSpan.ToArray().ToImmutableArray();
|
||||
}
|
||||
|
||||
private static void WriteDocument(Utf8JsonWriter writer, PolicyIrDocument document)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("name", document.Name);
|
||||
writer.WriteString("syntax", document.Syntax);
|
||||
|
||||
writer.WritePropertyName("metadata");
|
||||
WriteLiteralDictionary(writer, document.Metadata);
|
||||
|
||||
writer.WritePropertyName("profiles");
|
||||
writer.WriteStartArray();
|
||||
foreach (var profile in document.Profiles)
|
||||
{
|
||||
WriteProfile(writer, profile);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyName("settings");
|
||||
WriteLiteralDictionary(writer, document.Settings);
|
||||
|
||||
writer.WritePropertyName("rules");
|
||||
writer.WriteStartArray();
|
||||
foreach (var rule in document.Rules)
|
||||
{
|
||||
WriteRule(writer, rule);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static void WriteProfile(Utf8JsonWriter writer, PolicyIrProfile profile)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("name", profile.Name);
|
||||
|
||||
writer.WritePropertyName("maps");
|
||||
writer.WriteStartArray();
|
||||
foreach (var map in profile.Maps)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("name", map.Name);
|
||||
writer.WritePropertyName("entries");
|
||||
writer.WriteStartArray();
|
||||
foreach (var entry in map.Entries)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("source", entry.Source);
|
||||
writer.WriteNumber("weight", entry.Weight);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyName("env");
|
||||
writer.WriteStartArray();
|
||||
foreach (var env in profile.Environments)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("name", env.Name);
|
||||
writer.WritePropertyName("entries");
|
||||
writer.WriteStartArray();
|
||||
foreach (var entry in env.Entries)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("condition");
|
||||
WriteExpression(writer, entry.Condition);
|
||||
writer.WriteNumber("weight", entry.Weight);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyName("scalars");
|
||||
writer.WriteStartArray();
|
||||
foreach (var scalar in profile.Scalars)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("name", scalar.Name);
|
||||
writer.WritePropertyName("value");
|
||||
WriteLiteral(writer, scalar.Value);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static void WriteRule(Utf8JsonWriter writer, PolicyIrRule rule)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("name", rule.Name);
|
||||
writer.WriteNumber("priority", rule.Priority);
|
||||
writer.WritePropertyName("when");
|
||||
WriteExpression(writer, rule.When);
|
||||
|
||||
writer.WritePropertyName("then");
|
||||
WriteActions(writer, rule.ThenActions);
|
||||
|
||||
writer.WritePropertyName("else");
|
||||
WriteActions(writer, rule.ElseActions);
|
||||
|
||||
writer.WriteString("because", rule.Because);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static void WriteActions(Utf8JsonWriter writer, ImmutableArray<PolicyIrAction> actions)
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
foreach (var action in actions)
|
||||
{
|
||||
WriteAction(writer, action);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
private static void WriteAction(Utf8JsonWriter writer, PolicyIrAction action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case PolicyIrAssignmentAction assign:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "assign");
|
||||
WriteReference(writer, assign.Target);
|
||||
writer.WritePropertyName("value");
|
||||
WriteExpression(writer, assign.Value);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyIrAnnotateAction annotate:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "annotate");
|
||||
WriteReference(writer, annotate.Target);
|
||||
writer.WritePropertyName("value");
|
||||
WriteExpression(writer, annotate.Value);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyIrIgnoreAction ignore:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "ignore");
|
||||
writer.WritePropertyName("until");
|
||||
WriteOptionalExpression(writer, ignore.Until);
|
||||
writer.WriteString("because", ignore.Because ?? string.Empty);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyIrEscalateAction escalate:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "escalate");
|
||||
writer.WritePropertyName("to");
|
||||
WriteOptionalExpression(writer, escalate.To);
|
||||
writer.WritePropertyName("when");
|
||||
WriteOptionalExpression(writer, escalate.When);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyIrRequireVexAction require:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "requireVex");
|
||||
writer.WritePropertyName("conditions");
|
||||
writer.WriteStartObject();
|
||||
foreach (var kvp in require.Conditions)
|
||||
{
|
||||
writer.WritePropertyName(kvp.Key);
|
||||
WriteExpression(writer, kvp.Value);
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyIrWarnAction warn:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "warn");
|
||||
writer.WritePropertyName("message");
|
||||
WriteOptionalExpression(writer, warn.Message);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyIrDeferAction defer:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "defer");
|
||||
writer.WritePropertyName("until");
|
||||
WriteOptionalExpression(writer, defer.Until);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteReference(Utf8JsonWriter writer, ImmutableArray<string> segments)
|
||||
{
|
||||
writer.WritePropertyName("target");
|
||||
writer.WriteStartArray();
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
writer.WriteStringValue(segment);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
private static void WriteOptionalExpression(Utf8JsonWriter writer, PolicyExpression? expression)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
WriteExpression(writer, expression);
|
||||
}
|
||||
|
||||
private static void WriteExpression(Utf8JsonWriter writer, PolicyExpression expression)
|
||||
{
|
||||
switch (expression)
|
||||
{
|
||||
case PolicyLiteralExpression literal:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "literal");
|
||||
writer.WritePropertyName("value");
|
||||
WriteLiteralValue(writer, literal.Value);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyListExpression list:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "list");
|
||||
writer.WritePropertyName("items");
|
||||
writer.WriteStartArray();
|
||||
foreach (var item in list.Items)
|
||||
{
|
||||
WriteExpression(writer, item);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyIdentifierExpression identifier:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "identifier");
|
||||
writer.WriteString("name", identifier.Name);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyMemberAccessExpression member:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "member");
|
||||
writer.WritePropertyName("target");
|
||||
WriteExpression(writer, member.Target);
|
||||
writer.WriteString("member", member.Member);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyInvocationExpression invocation:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "call");
|
||||
writer.WritePropertyName("target");
|
||||
WriteExpression(writer, invocation.Target);
|
||||
writer.WritePropertyName("args");
|
||||
writer.WriteStartArray();
|
||||
foreach (var arg in invocation.Arguments)
|
||||
{
|
||||
WriteExpression(writer, arg);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyIndexerExpression indexer:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "indexer");
|
||||
writer.WritePropertyName("target");
|
||||
WriteExpression(writer, indexer.Target);
|
||||
writer.WritePropertyName("index");
|
||||
WriteExpression(writer, indexer.Index);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyUnaryExpression unary:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "unary");
|
||||
writer.WriteString("op", unary.Operator switch
|
||||
{
|
||||
PolicyUnaryOperator.Not => "not",
|
||||
_ => unary.Operator.ToString().ToLowerInvariant(),
|
||||
});
|
||||
writer.WritePropertyName("operand");
|
||||
WriteExpression(writer, unary.Operand);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case PolicyBinaryExpression binary:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "binary");
|
||||
writer.WriteString("op", GetBinaryOperator(binary.Operator));
|
||||
writer.WritePropertyName("left");
|
||||
WriteExpression(writer, binary.Left);
|
||||
writer.WritePropertyName("right");
|
||||
WriteExpression(writer, binary.Right);
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
default:
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", "unknown");
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetBinaryOperator(PolicyBinaryOperator op) => op switch
|
||||
{
|
||||
PolicyBinaryOperator.And => "and",
|
||||
PolicyBinaryOperator.Or => "or",
|
||||
PolicyBinaryOperator.Equal => "eq",
|
||||
PolicyBinaryOperator.NotEqual => "neq",
|
||||
PolicyBinaryOperator.LessThan => "lt",
|
||||
PolicyBinaryOperator.LessThanOrEqual => "lte",
|
||||
PolicyBinaryOperator.GreaterThan => "gt",
|
||||
PolicyBinaryOperator.GreaterThanOrEqual => "gte",
|
||||
PolicyBinaryOperator.In => "in",
|
||||
PolicyBinaryOperator.NotIn => "not_in",
|
||||
_ => op.ToString().ToLowerInvariant(),
|
||||
};
|
||||
|
||||
private static void WriteLiteralDictionary(Utf8JsonWriter writer, ImmutableSortedDictionary<string, PolicyIrLiteral> dictionary)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
foreach (var kvp in dictionary)
|
||||
{
|
||||
writer.WritePropertyName(kvp.Key);
|
||||
WriteLiteral(writer, kvp.Value);
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static void WriteLiteral(Utf8JsonWriter writer, PolicyIrLiteral literal)
|
||||
{
|
||||
switch (literal)
|
||||
{
|
||||
case PolicyIrStringLiteral s:
|
||||
writer.WriteStringValue(s.Value);
|
||||
break;
|
||||
case PolicyIrNumberLiteral n:
|
||||
writer.WriteNumberValue(n.Value);
|
||||
break;
|
||||
case PolicyIrBooleanLiteral b:
|
||||
writer.WriteBooleanValue(b.Value);
|
||||
break;
|
||||
case PolicyIrListLiteral list:
|
||||
writer.WriteStartArray();
|
||||
foreach (var item in list.Items)
|
||||
{
|
||||
WriteLiteral(writer, item);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
break;
|
||||
default:
|
||||
writer.WriteNullValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteLiteralValue(Utf8JsonWriter writer, object? value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case null:
|
||||
writer.WriteNullValue();
|
||||
break;
|
||||
case string s:
|
||||
writer.WriteStringValue(s);
|
||||
break;
|
||||
case bool b:
|
||||
writer.WriteBooleanValue(b);
|
||||
break;
|
||||
case decimal dec:
|
||||
writer.WriteNumberValue(dec);
|
||||
break;
|
||||
case double dbl:
|
||||
writer.WriteNumberValue(dbl);
|
||||
break;
|
||||
case int i:
|
||||
writer.WriteNumberValue(i);
|
||||
break;
|
||||
default:
|
||||
writer.WriteStringValue(value.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,692 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Policy;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Compilation;
|
||||
|
||||
internal sealed class PolicyParser
|
||||
{
|
||||
private readonly ImmutableArray<DslToken> tokens;
|
||||
private readonly List<PolicyIssue> diagnostics = new();
|
||||
private int position;
|
||||
|
||||
private PolicyParser(ImmutableArray<DslToken> tokens)
|
||||
{
|
||||
this.tokens = tokens;
|
||||
}
|
||||
|
||||
public static PolicyParseResult Parse(string source)
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
var tokenization = DslTokenizer.Tokenize(source);
|
||||
var parser = new PolicyParser(tokenization.Tokens);
|
||||
var document = parser.ParseDocument();
|
||||
var allDiagnostics = tokenization.Diagnostics.AddRange(parser.diagnostics).ToImmutableArray();
|
||||
return new PolicyParseResult(document, allDiagnostics);
|
||||
}
|
||||
|
||||
private PolicyDocumentNode? ParseDocument()
|
||||
{
|
||||
if (!Match(TokenKind.KeywordPolicy))
|
||||
{
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.MissingPolicyHeader, "Expected 'policy' declaration.", "policy"));
|
||||
return null;
|
||||
}
|
||||
|
||||
var nameToken = Consume(TokenKind.StringLiteral, "Policy name must be a string literal.", "policy.name");
|
||||
var name = nameToken.Value as string ?? nameToken.Text;
|
||||
|
||||
Consume(TokenKind.KeywordSyntax, "Expected 'syntax' declaration.", "policy.syntax");
|
||||
var syntaxToken = Consume(TokenKind.StringLiteral, "Policy syntax must be a string literal.", "policy.syntax.value");
|
||||
var syntax = syntaxToken.Value as string ?? syntaxToken.Text;
|
||||
|
||||
Consume(TokenKind.LeftBrace, "Expected '{' to start policy body.", "policy.body");
|
||||
|
||||
var metadataBuilder = ImmutableDictionary.CreateBuilder<string, PolicyLiteralValue>(StringComparer.Ordinal);
|
||||
var settingsBuilder = ImmutableDictionary.CreateBuilder<string, PolicyLiteralValue>(StringComparer.Ordinal);
|
||||
var profiles = ImmutableArray.CreateBuilder<PolicyProfileNode>();
|
||||
var rules = ImmutableArray.CreateBuilder<PolicyRuleNode>();
|
||||
|
||||
while (!Check(TokenKind.RightBrace) && !IsAtEnd)
|
||||
{
|
||||
if (Match(TokenKind.KeywordMetadata))
|
||||
{
|
||||
foreach (var kvp in ParseKeyValueBlock("policy.metadata"))
|
||||
{
|
||||
metadataBuilder[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordSettings))
|
||||
{
|
||||
foreach (var kvp in ParseKeyValueBlock("policy.settings"))
|
||||
{
|
||||
settingsBuilder[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordProfile))
|
||||
{
|
||||
var profile = ParseProfile();
|
||||
if (profile is not null)
|
||||
{
|
||||
profiles.Add(profile);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordRule))
|
||||
{
|
||||
var rule = ParseRule();
|
||||
if (rule is not null)
|
||||
{
|
||||
rules.Add(rule);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.UnexpectedSection, $"Unexpected token '{Current.Text}' in policy body.", "policy.body"));
|
||||
Advance();
|
||||
}
|
||||
|
||||
var close = Consume(TokenKind.RightBrace, "Expected '}' to close policy definition.", "policy");
|
||||
|
||||
if (!string.Equals(syntax, "stella-dsl@1", StringComparison.Ordinal))
|
||||
{
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.UnsupportedSyntaxVersion, $"Unsupported syntax '{syntax}'.", "policy.syntax"));
|
||||
}
|
||||
|
||||
var span = new SourceSpan(tokens[0].Span.Start, close.Span.End);
|
||||
return new PolicyDocumentNode(
|
||||
name,
|
||||
syntax,
|
||||
metadataBuilder.ToImmutable(),
|
||||
profiles.ToImmutable(),
|
||||
settingsBuilder.ToImmutable(),
|
||||
rules.ToImmutable(),
|
||||
span);
|
||||
}
|
||||
|
||||
private PolicyProfileNode? ParseProfile()
|
||||
{
|
||||
var nameToken = Consume(TokenKind.Identifier, "Profile requires a name.", "policy.profile");
|
||||
var name = nameToken.Text;
|
||||
Consume(TokenKind.LeftBrace, "Expected '{' after profile declaration.", $"policy.profile.{name}");
|
||||
|
||||
var start = nameToken.Span.Start;
|
||||
var depth = 1;
|
||||
while (depth > 0 && !IsAtEnd)
|
||||
{
|
||||
if (Match(TokenKind.LeftBrace))
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
else if (Match(TokenKind.RightBrace))
|
||||
{
|
||||
depth--;
|
||||
}
|
||||
else
|
||||
{
|
||||
Advance();
|
||||
}
|
||||
}
|
||||
|
||||
var close = Previous;
|
||||
return new PolicyProfileNode(
|
||||
name,
|
||||
ImmutableArray<PolicyProfileItemNode>.Empty,
|
||||
new SourceSpan(start, close.Span.End));
|
||||
}
|
||||
|
||||
private PolicyRuleNode? ParseRule()
|
||||
{
|
||||
var nameToken = Consume(TokenKind.Identifier, "Rule requires a name.", "policy.rule");
|
||||
var name = nameToken.Text;
|
||||
|
||||
var priority = 0;
|
||||
if (Match(TokenKind.KeywordPriority))
|
||||
{
|
||||
var priorityToken = Consume(TokenKind.NumberLiteral, "Priority must be numeric.", $"policy.rule.{name}");
|
||||
if (priorityToken.Value is decimal dec)
|
||||
{
|
||||
priority = (int)Math.Round(dec, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
|
||||
Consume(TokenKind.LeftBrace, "Expected '{' to start rule.", $"policy.rule.{name}");
|
||||
Consume(TokenKind.KeywordWhen, "Rule requires a 'when' clause.", $"policy.rule.{name}");
|
||||
var when = ParseExpression();
|
||||
|
||||
Consume(TokenKind.KeywordThen, "Rule requires a 'then' clause.", $"policy.rule.{name}");
|
||||
var thenActions = ParseActions(name, "then");
|
||||
|
||||
var elseActions = ImmutableArray<PolicyActionNode>.Empty;
|
||||
if (Match(TokenKind.KeywordElse))
|
||||
{
|
||||
elseActions = ParseActions(name, "else");
|
||||
}
|
||||
|
||||
string? because = null;
|
||||
if (Match(TokenKind.KeywordBecause))
|
||||
{
|
||||
var becauseToken = Consume(TokenKind.StringLiteral, "Because clause must be string.", $"policy.rule.{name}.because");
|
||||
because = becauseToken.Value as string ?? becauseToken.Text;
|
||||
}
|
||||
|
||||
var close = Consume(TokenKind.RightBrace, "Expected '}' to close rule.", $"policy.rule.{name}");
|
||||
|
||||
if (because is null)
|
||||
{
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.MissingBecauseClause, $"Rule '{name}' missing 'because' clause.", $"policy.rule.{name}"));
|
||||
}
|
||||
|
||||
return new PolicyRuleNode(name, priority, when, thenActions, elseActions, because, new SourceSpan(nameToken.Span.Start, close.Span.End));
|
||||
}
|
||||
|
||||
private ImmutableArray<PolicyActionNode> ParseActions(string ruleName, string clause)
|
||||
{
|
||||
var actions = ImmutableArray.CreateBuilder<PolicyActionNode>();
|
||||
while (!Check(TokenKind.RightBrace) && !Check(TokenKind.KeywordElse) && !Check(TokenKind.KeywordBecause) && !IsAtEnd)
|
||||
{
|
||||
if (Check(TokenKind.Identifier))
|
||||
{
|
||||
actions.Add(ParseAssignmentAction(ruleName, clause));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordAnnotate))
|
||||
{
|
||||
actions.Add(ParseAnnotateAction(ruleName, clause));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordWarn))
|
||||
{
|
||||
actions.Add(ParseWarnAction());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordEscalate))
|
||||
{
|
||||
actions.Add(ParseEscalateAction());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordRequireVex))
|
||||
{
|
||||
actions.Add(ParseRequireVexAction(ruleName, clause));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordIgnore))
|
||||
{
|
||||
actions.Add(ParseIgnoreAction(ruleName, clause));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordDefer))
|
||||
{
|
||||
actions.Add(ParseDeferAction(ruleName, clause));
|
||||
continue;
|
||||
}
|
||||
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.InvalidAction, $"Unexpected token '{Current.Text}' in {clause} actions.", $"policy.rule.{ruleName}.{clause}"));
|
||||
Advance();
|
||||
}
|
||||
|
||||
return actions.ToImmutable();
|
||||
}
|
||||
|
||||
private PolicyActionNode ParseAssignmentAction(string ruleName, string clause)
|
||||
{
|
||||
var segments = ImmutableArray.CreateBuilder<string>();
|
||||
var first = Consume(TokenKind.Identifier, "Assignment target must start with identifier.", $"policy.rule.{ruleName}.{clause}");
|
||||
segments.Add(first.Text);
|
||||
while (Match(TokenKind.Dot))
|
||||
{
|
||||
segments.Add(Consume(TokenKind.Identifier, "Expected identifier after '.'.", $"policy.rule.{ruleName}.{clause}").Text);
|
||||
}
|
||||
|
||||
Consume(TokenKind.Define, "Expected ':=' in action.", $"policy.rule.{ruleName}.{clause}");
|
||||
var value = ParseExpression();
|
||||
Match(TokenKind.Semicolon);
|
||||
return new PolicyAssignmentActionNode(new PolicyReference(segments.ToImmutable(), new SourceSpan(first.Span.Start, value.Span.End)), value, new SourceSpan(first.Span.Start, value.Span.End));
|
||||
}
|
||||
|
||||
private PolicyActionNode ParseAnnotateAction(string ruleName, string clause)
|
||||
{
|
||||
var reference = ParseReference($"policy.rule.{ruleName}.{clause}.annotate");
|
||||
Consume(TokenKind.Define, "Expected ':=' in annotate action.", $"policy.rule.{ruleName}.{clause}.annotate");
|
||||
var value = ParseExpression();
|
||||
Match(TokenKind.Semicolon);
|
||||
return new PolicyAnnotateActionNode(reference, value, new SourceSpan(reference.Span.Start, value.Span.End));
|
||||
}
|
||||
|
||||
private PolicyActionNode ParseWarnAction()
|
||||
{
|
||||
PolicyExpression? message = null;
|
||||
if (Match(TokenKind.KeywordMessage))
|
||||
{
|
||||
message = ParseExpression();
|
||||
}
|
||||
|
||||
Match(TokenKind.Semicolon);
|
||||
var span = message?.Span ?? Previous.Span;
|
||||
return new PolicyWarnActionNode(message, span);
|
||||
}
|
||||
|
||||
private PolicyActionNode ParseEscalateAction()
|
||||
{
|
||||
PolicyExpression? to = null;
|
||||
PolicyExpression? when = null;
|
||||
|
||||
if (Match(TokenKind.KeywordTo))
|
||||
{
|
||||
to = ParseExpression();
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordWhen))
|
||||
{
|
||||
when = ParseExpression();
|
||||
}
|
||||
|
||||
Match(TokenKind.Semicolon);
|
||||
var end = when?.Span.End ?? to?.Span.End ?? Previous.Span.End;
|
||||
return new PolicyEscalateActionNode(to, when, new SourceSpan(Previous.Span.Start, end));
|
||||
}
|
||||
|
||||
private PolicyActionNode ParseRequireVexAction(string ruleName, string clause)
|
||||
{
|
||||
Consume(TokenKind.LeftBrace, "Expected '{' after requireVex.", $"policy.rule.{ruleName}.{clause}.requireVex");
|
||||
var builder = ImmutableDictionary.CreateBuilder<string, PolicyExpression>(StringComparer.Ordinal);
|
||||
while (!Check(TokenKind.RightBrace) && !IsAtEnd)
|
||||
{
|
||||
var key = Consume(TokenKind.Identifier, "requireVex key must be identifier.", $"policy.rule.{ruleName}.{clause}.requireVex").Text;
|
||||
Consume(TokenKind.Assign, "Expected '=' in requireVex condition.", $"policy.rule.{ruleName}.{clause}.requireVex");
|
||||
builder[key] = ParseExpression();
|
||||
Match(TokenKind.Comma);
|
||||
}
|
||||
|
||||
var close = Consume(TokenKind.RightBrace, "Expected '}' to close requireVex block.", $"policy.rule.{ruleName}.{clause}.requireVex");
|
||||
Match(TokenKind.Semicolon);
|
||||
return new PolicyRequireVexActionNode(builder.ToImmutable(), new SourceSpan(close.Span.Start, close.Span.End));
|
||||
}
|
||||
|
||||
private PolicyActionNode ParseIgnoreAction(string ruleName, string clause)
|
||||
{
|
||||
PolicyExpression? until = null;
|
||||
string? because = null;
|
||||
if (Match(TokenKind.KeywordUntil))
|
||||
{
|
||||
until = ParseExpression();
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordBecause))
|
||||
{
|
||||
var becauseToken = Consume(TokenKind.StringLiteral, "Ignore 'because' must be string.", $"policy.rule.{ruleName}.{clause}.ignore");
|
||||
because = becauseToken.Value as string ?? becauseToken.Text;
|
||||
}
|
||||
|
||||
Match(TokenKind.Semicolon);
|
||||
return new PolicyIgnoreActionNode(until, because, new SourceSpan(Previous.Span.Start, (until?.Span.End ?? Previous.Span.End)));
|
||||
}
|
||||
|
||||
private PolicyActionNode ParseDeferAction(string ruleName, string clause)
|
||||
{
|
||||
PolicyExpression? until = null;
|
||||
if (Match(TokenKind.KeywordUntil))
|
||||
{
|
||||
until = ParseExpression();
|
||||
}
|
||||
|
||||
Match(TokenKind.Semicolon);
|
||||
return new PolicyDeferActionNode(until, new SourceSpan(Previous.Span.Start, (until?.Span.End ?? Previous.Span.End)));
|
||||
}
|
||||
|
||||
private PolicyReference ParseReference(string path)
|
||||
{
|
||||
var segments = ImmutableArray.CreateBuilder<string>();
|
||||
var first = Consume(TokenKind.Identifier, "Expected identifier.", path);
|
||||
segments.Add(first.Text);
|
||||
while (Match(TokenKind.Dot))
|
||||
{
|
||||
segments.Add(Consume(TokenKind.Identifier, "Expected identifier after '.'.", path).Text);
|
||||
}
|
||||
|
||||
return new PolicyReference(segments.ToImmutable(), first.Span);
|
||||
}
|
||||
|
||||
private Dictionary<string, PolicyLiteralValue> ParseKeyValueBlock(string path)
|
||||
{
|
||||
Consume(TokenKind.LeftBrace, "Expected '{'.", path);
|
||||
var entries = new Dictionary<string, PolicyLiteralValue>(StringComparer.Ordinal);
|
||||
while (!Check(TokenKind.RightBrace) && !IsAtEnd)
|
||||
{
|
||||
var key = Consume(TokenKind.Identifier, "Expected identifier.", path).Text;
|
||||
Consume(TokenKind.Assign, "Expected '='.", path);
|
||||
entries[key] = ParseLiteralValue(path);
|
||||
Match(TokenKind.Semicolon);
|
||||
}
|
||||
|
||||
Consume(TokenKind.RightBrace, "Expected '}'.", path);
|
||||
return entries;
|
||||
}
|
||||
|
||||
private PolicyLiteralValue ParseLiteralValue(string path)
|
||||
{
|
||||
if (Match(TokenKind.StringLiteral))
|
||||
{
|
||||
return new PolicyStringLiteral(Previous.Value as string ?? Previous.Text, Previous.Span);
|
||||
}
|
||||
|
||||
if (Match(TokenKind.NumberLiteral))
|
||||
{
|
||||
return new PolicyNumberLiteral(Previous.Value is decimal dec ? dec : 0m, Previous.Span);
|
||||
}
|
||||
|
||||
if (Match(TokenKind.BooleanLiteral))
|
||||
{
|
||||
return new PolicyBooleanLiteral(Previous.Value is bool b && b, Previous.Span);
|
||||
}
|
||||
|
||||
if (Match(TokenKind.LeftBracket))
|
||||
{
|
||||
var start = Previous.Span.Start;
|
||||
var items = ImmutableArray.CreateBuilder<PolicyLiteralValue>();
|
||||
while (!Check(TokenKind.RightBracket) && !IsAtEnd)
|
||||
{
|
||||
items.Add(ParseLiteralValue(path));
|
||||
Match(TokenKind.Comma);
|
||||
}
|
||||
|
||||
var close = Consume(TokenKind.RightBracket, "Expected ']' in list literal.", path);
|
||||
return new PolicyListLiteral(items.ToImmutable(), new SourceSpan(start, close.Span.End));
|
||||
}
|
||||
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.InvalidLiteral, "Invalid literal.", path));
|
||||
return new PolicyStringLiteral(string.Empty, Current.Span);
|
||||
}
|
||||
|
||||
private PolicyExpression ParseExpression() => ParseOr();
|
||||
|
||||
private PolicyExpression ParseOr()
|
||||
{
|
||||
var expr = ParseAnd();
|
||||
while (Match(TokenKind.KeywordOr))
|
||||
{
|
||||
var right = ParseAnd();
|
||||
expr = new PolicyBinaryExpression(expr, PolicyBinaryOperator.Or, right, new SourceSpan(expr.Span.Start, right.Span.End));
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private PolicyExpression ParseAnd()
|
||||
{
|
||||
var expr = ParseEquality();
|
||||
while (Match(TokenKind.KeywordAnd))
|
||||
{
|
||||
var right = ParseEquality();
|
||||
expr = new PolicyBinaryExpression(expr, PolicyBinaryOperator.And, right, new SourceSpan(expr.Span.Start, right.Span.End));
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private PolicyExpression ParseEquality()
|
||||
{
|
||||
var expr = ParseUnary();
|
||||
while (true)
|
||||
{
|
||||
if (Match(TokenKind.EqualEqual))
|
||||
{
|
||||
var right = ParseUnary();
|
||||
expr = new PolicyBinaryExpression(expr, PolicyBinaryOperator.Equal, right, new SourceSpan(expr.Span.Start, right.Span.End));
|
||||
}
|
||||
else if (Match(TokenKind.NotEqual))
|
||||
{
|
||||
var right = ParseUnary();
|
||||
expr = new PolicyBinaryExpression(expr, PolicyBinaryOperator.NotEqual, right, new SourceSpan(expr.Span.Start, right.Span.End));
|
||||
}
|
||||
else if (Match(TokenKind.KeywordIn))
|
||||
{
|
||||
var right = ParseUnary();
|
||||
expr = new PolicyBinaryExpression(expr, PolicyBinaryOperator.In, right, new SourceSpan(expr.Span.Start, right.Span.End));
|
||||
}
|
||||
else if (Match(TokenKind.KeywordNot))
|
||||
{
|
||||
if (Match(TokenKind.KeywordIn))
|
||||
{
|
||||
var right = ParseUnary();
|
||||
expr = new PolicyBinaryExpression(expr, PolicyBinaryOperator.NotIn, right, new SourceSpan(expr.Span.Start, right.Span.End));
|
||||
}
|
||||
else
|
||||
{
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.UnexpectedToken, "Expected 'in' after 'not'.", "expression.not"));
|
||||
}
|
||||
}
|
||||
else if (Match(TokenKind.LessThan))
|
||||
{
|
||||
var right = ParseUnary();
|
||||
expr = new PolicyBinaryExpression(expr, PolicyBinaryOperator.LessThan, right, new SourceSpan(expr.Span.Start, right.Span.End));
|
||||
}
|
||||
else if (Match(TokenKind.LessThanOrEqual))
|
||||
{
|
||||
var right = ParseUnary();
|
||||
expr = new PolicyBinaryExpression(expr, PolicyBinaryOperator.LessThanOrEqual, right, new SourceSpan(expr.Span.Start, right.Span.End));
|
||||
}
|
||||
else if (Match(TokenKind.GreaterThan))
|
||||
{
|
||||
var right = ParseUnary();
|
||||
expr = new PolicyBinaryExpression(expr, PolicyBinaryOperator.GreaterThan, right, new SourceSpan(expr.Span.Start, right.Span.End));
|
||||
}
|
||||
else if (Match(TokenKind.GreaterThanOrEqual))
|
||||
{
|
||||
var right = ParseUnary();
|
||||
expr = new PolicyBinaryExpression(expr, PolicyBinaryOperator.GreaterThanOrEqual, right, new SourceSpan(expr.Span.Start, right.Span.End));
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private PolicyExpression ParseUnary()
|
||||
{
|
||||
if (Match(TokenKind.KeywordNot))
|
||||
{
|
||||
var operand = ParseUnary();
|
||||
return new PolicyUnaryExpression(PolicyUnaryOperator.Not, operand, new SourceSpan(Previous.Span.Start, operand.Span.End));
|
||||
}
|
||||
|
||||
return ParsePrimary();
|
||||
}
|
||||
|
||||
private PolicyExpression ParsePrimary()
|
||||
{
|
||||
if (Match(TokenKind.StringLiteral))
|
||||
{
|
||||
return new PolicyLiteralExpression(Previous.Value as string ?? Previous.Text, Previous.Span);
|
||||
}
|
||||
|
||||
if (Match(TokenKind.NumberLiteral))
|
||||
{
|
||||
return new PolicyLiteralExpression(Previous.Value ?? 0m, Previous.Span);
|
||||
}
|
||||
|
||||
if (Match(TokenKind.BooleanLiteral))
|
||||
{
|
||||
return new PolicyLiteralExpression(Previous.Value ?? false, Previous.Span);
|
||||
}
|
||||
|
||||
if (Match(TokenKind.LeftBracket))
|
||||
{
|
||||
var start = Previous.Span.Start;
|
||||
var items = ImmutableArray.CreateBuilder<PolicyExpression>();
|
||||
while (!Check(TokenKind.RightBracket) && !IsAtEnd)
|
||||
{
|
||||
items.Add(ParseExpression());
|
||||
Match(TokenKind.Comma);
|
||||
}
|
||||
|
||||
var close = Consume(TokenKind.RightBracket, "Expected ']' to close list expression.", "expression.list");
|
||||
return new PolicyListExpression(items.ToImmutable(), new SourceSpan(start, close.Span.End));
|
||||
}
|
||||
|
||||
if (Match(TokenKind.LeftParen))
|
||||
{
|
||||
var expr = ParseExpression();
|
||||
Consume(TokenKind.RightParen, "Expected ')' to close grouped expression.", "expression.group");
|
||||
return expr;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.Identifier))
|
||||
{
|
||||
return ParseIdentifierExpression(Previous);
|
||||
}
|
||||
|
||||
if (Match(TokenKind.KeywordEnv))
|
||||
{
|
||||
return ParseIdentifierExpression(Previous);
|
||||
}
|
||||
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.UnexpectedToken, $"Unexpected token '{Current.Text}' in expression.", "expression"));
|
||||
var bad = Advance();
|
||||
return new PolicyLiteralExpression(null, bad.Span);
|
||||
}
|
||||
|
||||
private PolicyExpression ParseIdentifierExpression(DslToken identifier)
|
||||
{
|
||||
PolicyExpression expr = new PolicyIdentifierExpression(identifier.Text, identifier.Span);
|
||||
while (true)
|
||||
{
|
||||
if (Match(TokenKind.Dot))
|
||||
{
|
||||
var member = ConsumeIdentifier("Expected identifier after '.'.", "expression.member");
|
||||
expr = new PolicyMemberAccessExpression(expr, member.Text, new SourceSpan(expr.Span.Start, member.Span.End));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.LeftParen))
|
||||
{
|
||||
var args = ImmutableArray.CreateBuilder<PolicyExpression>();
|
||||
if (!Check(TokenKind.RightParen))
|
||||
{
|
||||
do
|
||||
{
|
||||
args.Add(ParseExpression());
|
||||
}
|
||||
while (Match(TokenKind.Comma));
|
||||
}
|
||||
|
||||
var close = Consume(TokenKind.RightParen, "Expected ')' to close invocation.", "expression.call");
|
||||
expr = new PolicyInvocationExpression(expr, args.ToImmutable(), new SourceSpan(expr.Span.Start, close.Span.End));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.LeftBracket))
|
||||
{
|
||||
var indexExpr = ParseExpression();
|
||||
var close = Consume(TokenKind.RightBracket, "Expected ']' to close indexer.", "expression.indexer");
|
||||
expr = new PolicyIndexerExpression(expr, indexExpr, new SourceSpan(expr.Span.Start, close.Span.End));
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private DslToken ConsumeIdentifier(string message, string path)
|
||||
{
|
||||
if (Check(TokenKind.Identifier) || IsKeywordIdentifier(Current.Kind))
|
||||
{
|
||||
return Advance();
|
||||
}
|
||||
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.UnexpectedToken, message, path));
|
||||
return Advance();
|
||||
}
|
||||
|
||||
private static bool IsKeywordIdentifier(TokenKind kind) =>
|
||||
kind == TokenKind.KeywordSource;
|
||||
|
||||
private bool Match(TokenKind kind)
|
||||
{
|
||||
if (Check(kind))
|
||||
{
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool Check(TokenKind kind) => !IsAtEnd && Current.Kind == kind;
|
||||
|
||||
private DslToken Consume(TokenKind kind, string message, string path)
|
||||
{
|
||||
if (Check(kind))
|
||||
{
|
||||
return Advance();
|
||||
}
|
||||
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.UnexpectedToken, message, path));
|
||||
return Advance();
|
||||
}
|
||||
|
||||
private void SkipBlock()
|
||||
{
|
||||
var depth = 1;
|
||||
while (depth > 0 && !IsAtEnd)
|
||||
{
|
||||
if (Match(TokenKind.LeftBrace))
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
else if (Match(TokenKind.RightBrace))
|
||||
{
|
||||
depth--;
|
||||
}
|
||||
else
|
||||
{
|
||||
Advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DslToken Advance()
|
||||
{
|
||||
if (!IsAtEnd)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
|
||||
return tokens[position - 1];
|
||||
}
|
||||
|
||||
private bool IsAtEnd => Current.Kind == TokenKind.EndOfFile;
|
||||
|
||||
private DslToken Current => tokens[position];
|
||||
|
||||
private DslToken Previous => tokens[position - 1];
|
||||
}
|
||||
|
||||
internal readonly record struct PolicyParseResult(
|
||||
PolicyDocumentNode? Document,
|
||||
ImmutableArray<PolicyIssue> Diagnostics);
|
||||
@@ -1,141 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Compilation;
|
||||
|
||||
public abstract record SyntaxNode(SourceSpan Span);
|
||||
|
||||
public sealed record PolicyDocumentNode(
|
||||
string Name,
|
||||
string Syntax,
|
||||
ImmutableDictionary<string, PolicyLiteralValue> Metadata,
|
||||
ImmutableArray<PolicyProfileNode> Profiles,
|
||||
ImmutableDictionary<string, PolicyLiteralValue> Settings,
|
||||
ImmutableArray<PolicyRuleNode> Rules,
|
||||
SourceSpan Span) : SyntaxNode(Span);
|
||||
|
||||
public sealed record PolicyProfileNode(
|
||||
string Name,
|
||||
ImmutableArray<PolicyProfileItemNode> Items,
|
||||
SourceSpan Span) : SyntaxNode(Span);
|
||||
|
||||
public abstract record PolicyProfileItemNode(SourceSpan Span);
|
||||
|
||||
public sealed record PolicyProfileMapNode(
|
||||
string Name,
|
||||
ImmutableArray<PolicyProfileMapEntryNode> Entries,
|
||||
SourceSpan Span) : PolicyProfileItemNode(Span);
|
||||
|
||||
public sealed record PolicyProfileMapEntryNode(
|
||||
string Source,
|
||||
decimal Weight,
|
||||
SourceSpan Span) : SyntaxNode(Span);
|
||||
|
||||
public sealed record PolicyProfileEnvNode(
|
||||
string Name,
|
||||
ImmutableArray<PolicyProfileEnvEntryNode> Entries,
|
||||
SourceSpan Span) : PolicyProfileItemNode(Span);
|
||||
|
||||
public sealed record PolicyProfileEnvEntryNode(
|
||||
PolicyExpression Condition,
|
||||
decimal Weight,
|
||||
SourceSpan Span) : SyntaxNode(Span);
|
||||
|
||||
public sealed record PolicyProfileScalarNode(
|
||||
string Name,
|
||||
PolicyLiteralValue Value,
|
||||
SourceSpan Span) : PolicyProfileItemNode(Span);
|
||||
|
||||
public sealed record PolicyRuleNode(
|
||||
string Name,
|
||||
int Priority,
|
||||
PolicyExpression When,
|
||||
ImmutableArray<PolicyActionNode> ThenActions,
|
||||
ImmutableArray<PolicyActionNode> ElseActions,
|
||||
string? Because,
|
||||
SourceSpan Span) : SyntaxNode(Span);
|
||||
|
||||
public abstract record PolicyActionNode(SourceSpan Span);
|
||||
|
||||
public sealed record PolicyAssignmentActionNode(
|
||||
PolicyReference Target,
|
||||
PolicyExpression Value,
|
||||
SourceSpan Span) : PolicyActionNode(Span);
|
||||
|
||||
public sealed record PolicyAnnotateActionNode(
|
||||
PolicyReference Target,
|
||||
PolicyExpression Value,
|
||||
SourceSpan Span) : PolicyActionNode(Span);
|
||||
|
||||
public sealed record PolicyIgnoreActionNode(
|
||||
PolicyExpression? Until,
|
||||
string? Because,
|
||||
SourceSpan Span) : PolicyActionNode(Span);
|
||||
|
||||
public sealed record PolicyEscalateActionNode(
|
||||
PolicyExpression? To,
|
||||
PolicyExpression? When,
|
||||
SourceSpan Span) : PolicyActionNode(Span);
|
||||
|
||||
public sealed record PolicyRequireVexActionNode(
|
||||
ImmutableDictionary<string, PolicyExpression> Conditions,
|
||||
SourceSpan Span) : PolicyActionNode(Span);
|
||||
|
||||
public sealed record PolicyWarnActionNode(
|
||||
PolicyExpression? Message,
|
||||
SourceSpan Span) : PolicyActionNode(Span);
|
||||
|
||||
public sealed record PolicyDeferActionNode(
|
||||
PolicyExpression? Until,
|
||||
SourceSpan Span) : PolicyActionNode(Span);
|
||||
|
||||
public abstract record PolicyExpression(SourceSpan Span);
|
||||
|
||||
public sealed record PolicyLiteralExpression(object? Value, SourceSpan Span) : PolicyExpression(Span);
|
||||
|
||||
public sealed record PolicyListExpression(ImmutableArray<PolicyExpression> Items, SourceSpan Span) : PolicyExpression(Span);
|
||||
|
||||
public sealed record PolicyIdentifierExpression(string Name, SourceSpan Span) : PolicyExpression(Span);
|
||||
|
||||
public sealed record PolicyMemberAccessExpression(PolicyExpression Target, string Member, SourceSpan Span) : PolicyExpression(Span);
|
||||
|
||||
public sealed record PolicyInvocationExpression(PolicyExpression Target, ImmutableArray<PolicyExpression> Arguments, SourceSpan Span) : PolicyExpression(Span);
|
||||
|
||||
public sealed record PolicyIndexerExpression(PolicyExpression Target, PolicyExpression Index, SourceSpan Span) : PolicyExpression(Span);
|
||||
|
||||
public sealed record PolicyUnaryExpression(PolicyUnaryOperator Operator, PolicyExpression Operand, SourceSpan Span) : PolicyExpression(Span);
|
||||
|
||||
public sealed record PolicyBinaryExpression(PolicyExpression Left, PolicyBinaryOperator Operator, PolicyExpression Right, SourceSpan Span) : PolicyExpression(Span);
|
||||
|
||||
public enum PolicyUnaryOperator
|
||||
{
|
||||
Not,
|
||||
}
|
||||
|
||||
public enum PolicyBinaryOperator
|
||||
{
|
||||
And,
|
||||
Or,
|
||||
Equal,
|
||||
NotEqual,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
GreaterThan,
|
||||
GreaterThanOrEqual,
|
||||
In,
|
||||
NotIn,
|
||||
}
|
||||
|
||||
public sealed record PolicyReference(ImmutableArray<string> Segments, SourceSpan Span)
|
||||
{
|
||||
public override string ToString() => string.Join(".", Segments);
|
||||
}
|
||||
|
||||
public abstract record PolicyLiteralValue(SourceSpan Span);
|
||||
|
||||
public sealed record PolicyStringLiteral(string Value, SourceSpan Span) : PolicyLiteralValue(Span);
|
||||
|
||||
public sealed record PolicyNumberLiteral(decimal Value, SourceSpan Span) : PolicyLiteralValue(Span);
|
||||
|
||||
public sealed record PolicyBooleanLiteral(bool Value, SourceSpan Span) : PolicyLiteralValue(Span);
|
||||
|
||||
public sealed record PolicyListLiteral(ImmutableArray<PolicyLiteralValue> Items, SourceSpan Span) : PolicyLiteralValue(Span);
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Engine.Compilation;
|
||||
using StellaOps.PolicyDsl;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Evaluation;
|
||||
|
||||
@@ -11,13 +11,13 @@ internal sealed record PolicyEvaluationRequest(
|
||||
PolicyIrDocument Document,
|
||||
PolicyEvaluationContext Context);
|
||||
|
||||
internal sealed record PolicyEvaluationContext(
|
||||
PolicyEvaluationSeverity Severity,
|
||||
PolicyEvaluationEnvironment Environment,
|
||||
PolicyEvaluationAdvisory Advisory,
|
||||
PolicyEvaluationVexEvidence Vex,
|
||||
PolicyEvaluationSbom Sbom,
|
||||
PolicyEvaluationExceptions Exceptions);
|
||||
internal sealed record PolicyEvaluationContext(
|
||||
PolicyEvaluationSeverity Severity,
|
||||
PolicyEvaluationEnvironment Environment,
|
||||
PolicyEvaluationAdvisory Advisory,
|
||||
PolicyEvaluationVexEvidence Vex,
|
||||
PolicyEvaluationSbom Sbom,
|
||||
PolicyEvaluationExceptions Exceptions);
|
||||
|
||||
internal sealed record PolicyEvaluationSeverity(string Normalized, decimal? Score = null);
|
||||
|
||||
@@ -43,28 +43,28 @@ internal sealed record PolicyEvaluationVexStatement(
|
||||
string StatementId,
|
||||
DateTimeOffset? Timestamp = null);
|
||||
|
||||
internal sealed record PolicyEvaluationSbom(
|
||||
ImmutableHashSet<string> Tags,
|
||||
ImmutableArray<PolicyEvaluationComponent> Components)
|
||||
{
|
||||
public PolicyEvaluationSbom(ImmutableHashSet<string> Tags)
|
||||
: this(Tags, ImmutableArray<PolicyEvaluationComponent>.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public static readonly PolicyEvaluationSbom Empty = new(
|
||||
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
|
||||
ImmutableArray<PolicyEvaluationComponent>.Empty);
|
||||
|
||||
public bool HasTag(string tag) => Tags.Contains(tag);
|
||||
}
|
||||
|
||||
internal sealed record PolicyEvaluationComponent(
|
||||
string Name,
|
||||
string Version,
|
||||
string Type,
|
||||
string? Purl,
|
||||
ImmutableDictionary<string, string> Metadata);
|
||||
internal sealed record PolicyEvaluationSbom(
|
||||
ImmutableHashSet<string> Tags,
|
||||
ImmutableArray<PolicyEvaluationComponent> Components)
|
||||
{
|
||||
public PolicyEvaluationSbom(ImmutableHashSet<string> Tags)
|
||||
: this(Tags, ImmutableArray<PolicyEvaluationComponent>.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public static readonly PolicyEvaluationSbom Empty = new(
|
||||
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
|
||||
ImmutableArray<PolicyEvaluationComponent>.Empty);
|
||||
|
||||
public bool HasTag(string tag) => Tags.Contains(tag);
|
||||
}
|
||||
|
||||
internal sealed record PolicyEvaluationComponent(
|
||||
string Name,
|
||||
string Version,
|
||||
string Type,
|
||||
string? Purl,
|
||||
ImmutableDictionary<string, string> Metadata);
|
||||
|
||||
internal sealed record PolicyEvaluationResult(
|
||||
bool Matched,
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Engine.Compilation;
|
||||
using StellaOps.PolicyDsl;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Evaluation;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using StellaOps.Policy.Engine.Compilation;
|
||||
using StellaOps.PolicyDsl;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Evaluation;
|
||||
|
||||
@@ -98,20 +98,20 @@ internal sealed class PolicyExpressionEvaluator
|
||||
return sbom.Get(member.Member);
|
||||
}
|
||||
|
||||
if (raw is ComponentScope componentScope)
|
||||
{
|
||||
return componentScope.Get(member.Member);
|
||||
}
|
||||
|
||||
if (raw is RubyComponentScope rubyScope)
|
||||
{
|
||||
return rubyScope.Get(member.Member);
|
||||
}
|
||||
|
||||
if (raw is ImmutableDictionary<string, object?> dict && dict.TryGetValue(member.Member, out var value))
|
||||
{
|
||||
return new EvaluationValue(value);
|
||||
}
|
||||
if (raw is ComponentScope componentScope)
|
||||
{
|
||||
return componentScope.Get(member.Member);
|
||||
}
|
||||
|
||||
if (raw is RubyComponentScope rubyScope)
|
||||
{
|
||||
return rubyScope.Get(member.Member);
|
||||
}
|
||||
|
||||
if (raw is ImmutableDictionary<string, object?> dict && dict.TryGetValue(member.Member, out var value))
|
||||
{
|
||||
return new EvaluationValue(value);
|
||||
}
|
||||
|
||||
if (raw is PolicyEvaluationVexStatement stmt)
|
||||
{
|
||||
@@ -139,51 +139,51 @@ internal sealed class PolicyExpressionEvaluator
|
||||
}
|
||||
}
|
||||
|
||||
if (invocation.Target is PolicyMemberAccessExpression member)
|
||||
{
|
||||
var targetValue = Evaluate(member.Target, scope);
|
||||
var targetRaw = targetValue.Raw;
|
||||
if (targetRaw is RubyComponentScope rubyScope)
|
||||
{
|
||||
return rubyScope.Invoke(member.Member, invocation.Arguments, scope, this);
|
||||
}
|
||||
|
||||
if (targetRaw is ComponentScope componentScope)
|
||||
{
|
||||
return componentScope.Invoke(member.Member, invocation.Arguments, scope, this);
|
||||
}
|
||||
|
||||
if (member.Target is PolicyIdentifierExpression root)
|
||||
{
|
||||
if (root.Name == "vex" && targetRaw is VexScope vexScope)
|
||||
{
|
||||
return member.Member switch
|
||||
{
|
||||
"any" => new EvaluationValue(vexScope.Any(invocation.Arguments, scope)),
|
||||
"latest" => new EvaluationValue(vexScope.Latest()),
|
||||
_ => EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
if (root.Name == "sbom" && targetRaw is SbomScope sbomScope)
|
||||
{
|
||||
return member.Member switch
|
||||
{
|
||||
"has_tag" => sbomScope.HasTag(invocation.Arguments, scope, this),
|
||||
"any_component" => sbomScope.AnyComponent(invocation.Arguments, scope, this),
|
||||
_ => EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
if (root.Name == "advisory" && targetRaw is AdvisoryScope advisoryScope)
|
||||
{
|
||||
return advisoryScope.Invoke(member.Member, invocation.Arguments, scope, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
if (invocation.Target is PolicyMemberAccessExpression member)
|
||||
{
|
||||
var targetValue = Evaluate(member.Target, scope);
|
||||
var targetRaw = targetValue.Raw;
|
||||
if (targetRaw is RubyComponentScope rubyScope)
|
||||
{
|
||||
return rubyScope.Invoke(member.Member, invocation.Arguments, scope, this);
|
||||
}
|
||||
|
||||
if (targetRaw is ComponentScope componentScope)
|
||||
{
|
||||
return componentScope.Invoke(member.Member, invocation.Arguments, scope, this);
|
||||
}
|
||||
|
||||
if (member.Target is PolicyIdentifierExpression root)
|
||||
{
|
||||
if (root.Name == "vex" && targetRaw is VexScope vexScope)
|
||||
{
|
||||
return member.Member switch
|
||||
{
|
||||
"any" => new EvaluationValue(vexScope.Any(invocation.Arguments, scope)),
|
||||
"latest" => new EvaluationValue(vexScope.Latest()),
|
||||
_ => EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
if (root.Name == "sbom" && targetRaw is SbomScope sbomScope)
|
||||
{
|
||||
return member.Member switch
|
||||
{
|
||||
"has_tag" => sbomScope.HasTag(invocation.Arguments, scope, this),
|
||||
"any_component" => sbomScope.AnyComponent(invocation.Arguments, scope, this),
|
||||
_ => EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
if (root.Name == "advisory" && targetRaw is AdvisoryScope advisoryScope)
|
||||
{
|
||||
return advisoryScope.Invoke(member.Member, invocation.Arguments, scope, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
|
||||
private EvaluationValue EvaluateIndexer(PolicyIndexerExpression indexer, EvaluationScope scope)
|
||||
{
|
||||
@@ -442,322 +442,322 @@ internal sealed class PolicyExpressionEvaluator
|
||||
this.sbom = sbom;
|
||||
}
|
||||
|
||||
public EvaluationValue Get(string member)
|
||||
{
|
||||
if (member.Equals("tags", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new EvaluationValue(sbom.Tags.ToImmutableArray<object?>());
|
||||
}
|
||||
|
||||
if (member.Equals("components", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new EvaluationValue(sbom.Components
|
||||
.Select(component => (object?)new ComponentScope(component))
|
||||
.ToImmutableArray());
|
||||
}
|
||||
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
|
||||
public EvaluationValue HasTag(ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
var tag = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
if (string.IsNullOrWhiteSpace(tag))
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
return new EvaluationValue(sbom.HasTag(tag!));
|
||||
}
|
||||
|
||||
public EvaluationValue AnyComponent(ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
if (arguments.Length == 0 || sbom.Components.IsDefaultOrEmpty)
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
var predicate = arguments[0];
|
||||
foreach (var component in sbom.Components)
|
||||
{
|
||||
var locals = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["component"] = new ComponentScope(component),
|
||||
};
|
||||
|
||||
if (component.Type.Equals("gem", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
locals["ruby"] = new RubyComponentScope(component);
|
||||
}
|
||||
|
||||
var nestedScope = EvaluationScope.FromLocals(scope.Globals, locals);
|
||||
if (evaluator.EvaluateBoolean(predicate, nestedScope))
|
||||
{
|
||||
return EvaluationValue.True;
|
||||
}
|
||||
}
|
||||
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ComponentScope
|
||||
{
|
||||
private readonly PolicyEvaluationComponent component;
|
||||
|
||||
public ComponentScope(PolicyEvaluationComponent component)
|
||||
{
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
public EvaluationValue Get(string member)
|
||||
{
|
||||
return member.ToLowerInvariant() switch
|
||||
{
|
||||
"name" => new EvaluationValue(component.Name),
|
||||
"version" => new EvaluationValue(component.Version),
|
||||
"type" => new EvaluationValue(component.Type),
|
||||
"purl" => new EvaluationValue(component.Purl),
|
||||
"metadata" => new EvaluationValue(component.Metadata),
|
||||
_ => component.Metadata.TryGetValue(member, out var value)
|
||||
? new EvaluationValue(value)
|
||||
: EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
public EvaluationValue Invoke(string member, ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
if (member.Equals("has_metadata", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var key = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
return new EvaluationValue(component.Metadata.ContainsKey(key!));
|
||||
}
|
||||
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RubyComponentScope
|
||||
{
|
||||
private readonly PolicyEvaluationComponent component;
|
||||
private readonly ImmutableHashSet<string> groups;
|
||||
|
||||
public RubyComponentScope(PolicyEvaluationComponent component)
|
||||
{
|
||||
this.component = component;
|
||||
groups = ParseGroups(component.Metadata);
|
||||
}
|
||||
|
||||
public EvaluationValue Get(string member)
|
||||
{
|
||||
return member.ToLowerInvariant() switch
|
||||
{
|
||||
"groups" => new EvaluationValue(groups.Select(value => (object?)value).ToImmutableArray()),
|
||||
"declaredonly" => new EvaluationValue(IsDeclaredOnly()),
|
||||
"source" => new EvaluationValue(GetSource() ?? string.Empty),
|
||||
_ => component.Metadata.TryGetValue(member, out var value)
|
||||
? new EvaluationValue(value)
|
||||
: EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
public EvaluationValue Invoke(string member, ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
switch (member.ToLowerInvariant())
|
||||
{
|
||||
case "group":
|
||||
{
|
||||
var name = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
return new EvaluationValue(name is not null && groups.Contains(name));
|
||||
}
|
||||
case "groups":
|
||||
return new EvaluationValue(groups.Select(value => (object?)value).ToImmutableArray());
|
||||
case "declared_only":
|
||||
return new EvaluationValue(IsDeclaredOnly());
|
||||
case "source":
|
||||
{
|
||||
if (arguments.Length == 0)
|
||||
{
|
||||
return new EvaluationValue(GetSource() ?? string.Empty);
|
||||
}
|
||||
|
||||
var requested = evaluator.Evaluate(arguments[0], scope).AsString();
|
||||
if (string.IsNullOrWhiteSpace(requested))
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
var kind = GetSourceKind();
|
||||
return new EvaluationValue(string.Equals(kind, requested, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
case "capability":
|
||||
{
|
||||
var name = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
return new EvaluationValue(HasCapability(name));
|
||||
}
|
||||
case "capability_any":
|
||||
{
|
||||
var capabilities = EvaluateAsStringSet(arguments, scope, evaluator);
|
||||
return new EvaluationValue(capabilities.Any(HasCapability));
|
||||
}
|
||||
default:
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasCapability(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = name.Trim();
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (component.Metadata.TryGetValue($"capability.{normalized}", out var value))
|
||||
{
|
||||
return IsTruthy(value);
|
||||
}
|
||||
|
||||
if (normalized.StartsWith("scheduler.", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var group = normalized.Substring("scheduler.".Length);
|
||||
var schedulerList = component.Metadata.TryGetValue("capability.scheduler", out var listValue)
|
||||
? listValue
|
||||
: null;
|
||||
return ContainsDelimitedValue(schedulerList, group);
|
||||
}
|
||||
|
||||
if (normalized.Equals("scheduler", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var schedulerList = component.Metadata.TryGetValue("capability.scheduler", out var listValue)
|
||||
? listValue
|
||||
: null;
|
||||
return !string.IsNullOrWhiteSpace(schedulerList);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsDeclaredOnly()
|
||||
{
|
||||
return component.Metadata.TryGetValue("declaredOnly", out var value) && IsTruthy(value);
|
||||
}
|
||||
|
||||
private string? GetSource()
|
||||
{
|
||||
return component.Metadata.TryGetValue("source", out var value) ? value : null;
|
||||
}
|
||||
|
||||
private string? GetSourceKind()
|
||||
{
|
||||
var source = GetSource();
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
source = source.Trim();
|
||||
if (source.StartsWith("git:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "git";
|
||||
}
|
||||
|
||||
if (source.StartsWith("path:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "path";
|
||||
}
|
||||
|
||||
if (source.StartsWith("vendor-cache", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "vendor-cache";
|
||||
}
|
||||
|
||||
if (source.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
|| source.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "registry";
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
private static ImmutableHashSet<string> ParseGroups(ImmutableDictionary<string, string> metadata)
|
||||
{
|
||||
if (!metadata.TryGetValue("groups", out var value) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return ImmutableHashSet<string>.Empty;
|
||||
}
|
||||
|
||||
var groups = value
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(static g => !string.IsNullOrWhiteSpace(g))
|
||||
.Select(static g => g.Trim())
|
||||
.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private static bool ContainsDelimitedValue(string? delimited, string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(delimited) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return delimited
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Any(entry => entry.Equals(value, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsTruthy(string? value)
|
||||
{
|
||||
return value is not null
|
||||
&& (value.Equals("true", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("1", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("yes", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static ImmutableHashSet<string> EvaluateAsStringSet(ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
var builder = ImmutableHashSet.CreateBuilder<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
var evaluated = evaluator.Evaluate(argument, scope).Raw;
|
||||
switch (evaluated)
|
||||
{
|
||||
case ImmutableArray<object?> array:
|
||||
foreach (var item in array)
|
||||
{
|
||||
if (item is string text && !string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
builder.Add(text.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case string text when !string.IsNullOrWhiteSpace(text):
|
||||
builder.Add(text.Trim());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class VexScope
|
||||
{
|
||||
private readonly PolicyExpressionEvaluator evaluator;
|
||||
public EvaluationValue Get(string member)
|
||||
{
|
||||
if (member.Equals("tags", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new EvaluationValue(sbom.Tags.ToImmutableArray<object?>());
|
||||
}
|
||||
|
||||
if (member.Equals("components", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new EvaluationValue(sbom.Components
|
||||
.Select(component => (object?)new ComponentScope(component))
|
||||
.ToImmutableArray());
|
||||
}
|
||||
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
|
||||
public EvaluationValue HasTag(ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
var tag = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
if (string.IsNullOrWhiteSpace(tag))
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
return new EvaluationValue(sbom.HasTag(tag!));
|
||||
}
|
||||
|
||||
public EvaluationValue AnyComponent(ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
if (arguments.Length == 0 || sbom.Components.IsDefaultOrEmpty)
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
var predicate = arguments[0];
|
||||
foreach (var component in sbom.Components)
|
||||
{
|
||||
var locals = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["component"] = new ComponentScope(component),
|
||||
};
|
||||
|
||||
if (component.Type.Equals("gem", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
locals["ruby"] = new RubyComponentScope(component);
|
||||
}
|
||||
|
||||
var nestedScope = EvaluationScope.FromLocals(scope.Globals, locals);
|
||||
if (evaluator.EvaluateBoolean(predicate, nestedScope))
|
||||
{
|
||||
return EvaluationValue.True;
|
||||
}
|
||||
}
|
||||
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ComponentScope
|
||||
{
|
||||
private readonly PolicyEvaluationComponent component;
|
||||
|
||||
public ComponentScope(PolicyEvaluationComponent component)
|
||||
{
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
public EvaluationValue Get(string member)
|
||||
{
|
||||
return member.ToLowerInvariant() switch
|
||||
{
|
||||
"name" => new EvaluationValue(component.Name),
|
||||
"version" => new EvaluationValue(component.Version),
|
||||
"type" => new EvaluationValue(component.Type),
|
||||
"purl" => new EvaluationValue(component.Purl),
|
||||
"metadata" => new EvaluationValue(component.Metadata),
|
||||
_ => component.Metadata.TryGetValue(member, out var value)
|
||||
? new EvaluationValue(value)
|
||||
: EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
public EvaluationValue Invoke(string member, ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
if (member.Equals("has_metadata", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var key = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
return new EvaluationValue(component.Metadata.ContainsKey(key!));
|
||||
}
|
||||
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RubyComponentScope
|
||||
{
|
||||
private readonly PolicyEvaluationComponent component;
|
||||
private readonly ImmutableHashSet<string> groups;
|
||||
|
||||
public RubyComponentScope(PolicyEvaluationComponent component)
|
||||
{
|
||||
this.component = component;
|
||||
groups = ParseGroups(component.Metadata);
|
||||
}
|
||||
|
||||
public EvaluationValue Get(string member)
|
||||
{
|
||||
return member.ToLowerInvariant() switch
|
||||
{
|
||||
"groups" => new EvaluationValue(groups.Select(value => (object?)value).ToImmutableArray()),
|
||||
"declaredonly" => new EvaluationValue(IsDeclaredOnly()),
|
||||
"source" => new EvaluationValue(GetSource() ?? string.Empty),
|
||||
_ => component.Metadata.TryGetValue(member, out var value)
|
||||
? new EvaluationValue(value)
|
||||
: EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
public EvaluationValue Invoke(string member, ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
switch (member.ToLowerInvariant())
|
||||
{
|
||||
case "group":
|
||||
{
|
||||
var name = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
return new EvaluationValue(name is not null && groups.Contains(name));
|
||||
}
|
||||
case "groups":
|
||||
return new EvaluationValue(groups.Select(value => (object?)value).ToImmutableArray());
|
||||
case "declared_only":
|
||||
return new EvaluationValue(IsDeclaredOnly());
|
||||
case "source":
|
||||
{
|
||||
if (arguments.Length == 0)
|
||||
{
|
||||
return new EvaluationValue(GetSource() ?? string.Empty);
|
||||
}
|
||||
|
||||
var requested = evaluator.Evaluate(arguments[0], scope).AsString();
|
||||
if (string.IsNullOrWhiteSpace(requested))
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
var kind = GetSourceKind();
|
||||
return new EvaluationValue(string.Equals(kind, requested, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
case "capability":
|
||||
{
|
||||
var name = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
return new EvaluationValue(HasCapability(name));
|
||||
}
|
||||
case "capability_any":
|
||||
{
|
||||
var capabilities = EvaluateAsStringSet(arguments, scope, evaluator);
|
||||
return new EvaluationValue(capabilities.Any(HasCapability));
|
||||
}
|
||||
default:
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasCapability(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = name.Trim();
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (component.Metadata.TryGetValue($"capability.{normalized}", out var value))
|
||||
{
|
||||
return IsTruthy(value);
|
||||
}
|
||||
|
||||
if (normalized.StartsWith("scheduler.", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var group = normalized.Substring("scheduler.".Length);
|
||||
var schedulerList = component.Metadata.TryGetValue("capability.scheduler", out var listValue)
|
||||
? listValue
|
||||
: null;
|
||||
return ContainsDelimitedValue(schedulerList, group);
|
||||
}
|
||||
|
||||
if (normalized.Equals("scheduler", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var schedulerList = component.Metadata.TryGetValue("capability.scheduler", out var listValue)
|
||||
? listValue
|
||||
: null;
|
||||
return !string.IsNullOrWhiteSpace(schedulerList);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsDeclaredOnly()
|
||||
{
|
||||
return component.Metadata.TryGetValue("declaredOnly", out var value) && IsTruthy(value);
|
||||
}
|
||||
|
||||
private string? GetSource()
|
||||
{
|
||||
return component.Metadata.TryGetValue("source", out var value) ? value : null;
|
||||
}
|
||||
|
||||
private string? GetSourceKind()
|
||||
{
|
||||
var source = GetSource();
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
source = source.Trim();
|
||||
if (source.StartsWith("git:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "git";
|
||||
}
|
||||
|
||||
if (source.StartsWith("path:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "path";
|
||||
}
|
||||
|
||||
if (source.StartsWith("vendor-cache", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "vendor-cache";
|
||||
}
|
||||
|
||||
if (source.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
|| source.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "registry";
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
private static ImmutableHashSet<string> ParseGroups(ImmutableDictionary<string, string> metadata)
|
||||
{
|
||||
if (!metadata.TryGetValue("groups", out var value) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return ImmutableHashSet<string>.Empty;
|
||||
}
|
||||
|
||||
var groups = value
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(static g => !string.IsNullOrWhiteSpace(g))
|
||||
.Select(static g => g.Trim())
|
||||
.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private static bool ContainsDelimitedValue(string? delimited, string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(delimited) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return delimited
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Any(entry => entry.Equals(value, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsTruthy(string? value)
|
||||
{
|
||||
return value is not null
|
||||
&& (value.Equals("true", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("1", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("yes", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static ImmutableHashSet<string> EvaluateAsStringSet(ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
var builder = ImmutableHashSet.CreateBuilder<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
var evaluated = evaluator.Evaluate(argument, scope).Raw;
|
||||
switch (evaluated)
|
||||
{
|
||||
case ImmutableArray<object?> array:
|
||||
foreach (var item in array)
|
||||
{
|
||||
if (item is string text && !string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
builder.Add(text.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case string text when !string.IsNullOrWhiteSpace(text):
|
||||
builder.Add(text.Trim());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class VexScope
|
||||
{
|
||||
private readonly PolicyExpressionEvaluator evaluator;
|
||||
private readonly PolicyEvaluationVexEvidence vex;
|
||||
|
||||
public VexScope(PolicyExpressionEvaluator evaluator, PolicyEvaluationVexEvidence vex)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NetEscapades.Configuration.Yaml;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.Client;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Configuration;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NetEscapades.Configuration.Yaml;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.Client;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Policy.Engine.Hosting;
|
||||
using StellaOps.Policy.Engine.Options;
|
||||
using StellaOps.Policy.Engine.Compilation;
|
||||
using StellaOps.Policy.Engine.Endpoints;
|
||||
using StellaOps.PolicyDsl;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
using StellaOps.Policy.Engine.Workers;
|
||||
using StellaOps.Policy.Engine.Streaming;
|
||||
@@ -33,17 +34,17 @@ var policyEngineActivationConfigFiles = new[]
|
||||
"policy-engine.activation.yaml",
|
||||
"policy-engine.activation.local.yaml"
|
||||
};
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddConsole();
|
||||
|
||||
builder.Configuration.AddStellaOpsDefaults(options =>
|
||||
{
|
||||
options.BasePath = builder.Environment.ContentRootPath;
|
||||
options.EnvironmentPrefix = "STELLAOPS_POLICY_ENGINE_";
|
||||
options.ConfigureBuilder = configurationBuilder =>
|
||||
{
|
||||
var contentRoot = builder.Environment.ContentRootPath;
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddConsole();
|
||||
|
||||
builder.Configuration.AddStellaOpsDefaults(options =>
|
||||
{
|
||||
options.BasePath = builder.Environment.ContentRootPath;
|
||||
options.EnvironmentPrefix = "STELLAOPS_POLICY_ENGINE_";
|
||||
options.ConfigureBuilder = configurationBuilder =>
|
||||
{
|
||||
var contentRoot = builder.Environment.ContentRootPath;
|
||||
foreach (var relative in policyEngineConfigFiles)
|
||||
{
|
||||
var path = Path.Combine(contentRoot, relative);
|
||||
@@ -59,12 +60,12 @@ builder.Configuration.AddStellaOpsDefaults(options =>
|
||||
});
|
||||
|
||||
var bootstrap = StellaOpsConfigurationBootstrapper.Build<PolicyEngineOptions>(options =>
|
||||
{
|
||||
options.BasePath = builder.Environment.ContentRootPath;
|
||||
options.EnvironmentPrefix = "STELLAOPS_POLICY_ENGINE_";
|
||||
options.BindingSection = PolicyEngineOptions.SectionName;
|
||||
options.ConfigureBuilder = configurationBuilder =>
|
||||
{
|
||||
{
|
||||
options.BasePath = builder.Environment.ContentRootPath;
|
||||
options.EnvironmentPrefix = "STELLAOPS_POLICY_ENGINE_";
|
||||
options.BindingSection = PolicyEngineOptions.SectionName;
|
||||
options.ConfigureBuilder = configurationBuilder =>
|
||||
{
|
||||
foreach (var relative in policyEngineConfigFiles)
|
||||
{
|
||||
var path = Path.Combine(builder.Environment.ContentRootPath, relative);
|
||||
@@ -79,35 +80,35 @@ var bootstrap = StellaOpsConfigurationBootstrapper.Build<PolicyEngineOptions>(op
|
||||
};
|
||||
options.PostBind = static (value, _) => value.Validate();
|
||||
});
|
||||
|
||||
|
||||
builder.Configuration.AddConfiguration(bootstrap.Configuration);
|
||||
|
||||
builder.Services.AddAirGapEgressPolicy(builder.Configuration, sectionName: "AirGap");
|
||||
|
||||
builder.Services.AddOptions<PolicyEngineOptions>()
|
||||
.Bind(builder.Configuration.GetSection(PolicyEngineOptions.SectionName))
|
||||
.Validate(options =>
|
||||
{
|
||||
try
|
||||
{
|
||||
options.Validate();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new OptionsValidationException(
|
||||
PolicyEngineOptions.SectionName,
|
||||
typeof(PolicyEngineOptions),
|
||||
new[] { ex.Message });
|
||||
}
|
||||
})
|
||||
.ValidateOnStart();
|
||||
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<PolicyEngineOptions>>().Value);
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
.Bind(builder.Configuration.GetSection(PolicyEngineOptions.SectionName))
|
||||
.Validate(options =>
|
||||
{
|
||||
try
|
||||
{
|
||||
options.Validate();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new OptionsValidationException(
|
||||
PolicyEngineOptions.SectionName,
|
||||
typeof(PolicyEngineOptions),
|
||||
new[] { ex.Message });
|
||||
}
|
||||
})
|
||||
.ValidateOnStart();
|
||||
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<PolicyEngineOptions>>().Value);
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
builder.Services.AddSingleton<PolicyEngineStartupDiagnostics>();
|
||||
builder.Services.AddHostedService<PolicyEngineBootstrapWorker>();
|
||||
builder.Services.AddSingleton<PolicyCompiler>();
|
||||
builder.Services.AddSingleton<StellaOps.PolicyDsl.PolicyCompiler>();
|
||||
builder.Services.AddSingleton<PolicyCompilationService>();
|
||||
builder.Services.AddSingleton<StellaOps.Policy.Engine.Services.PathScopeMetrics>();
|
||||
builder.Services.AddSingleton<PolicyEvaluationService>();
|
||||
@@ -140,36 +141,36 @@ builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
builder.Services.AddAuthentication();
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddStellaOpsScopeHandler();
|
||||
builder.Services.AddStellaOpsResourceServerAuthentication(
|
||||
builder.Configuration,
|
||||
configurationSection: $"{PolicyEngineOptions.SectionName}:ResourceServer");
|
||||
|
||||
if (bootstrap.Options.Authority.Enabled)
|
||||
{
|
||||
builder.Services.AddStellaOpsAuthClient(clientOptions =>
|
||||
{
|
||||
clientOptions.Authority = bootstrap.Options.Authority.Issuer;
|
||||
clientOptions.ClientId = bootstrap.Options.Authority.ClientId;
|
||||
clientOptions.ClientSecret = bootstrap.Options.Authority.ClientSecret;
|
||||
clientOptions.HttpTimeout = TimeSpan.FromSeconds(bootstrap.Options.Authority.BackchannelTimeoutSeconds);
|
||||
|
||||
clientOptions.DefaultScopes.Clear();
|
||||
foreach (var scope in bootstrap.Options.Authority.Scopes)
|
||||
{
|
||||
clientOptions.DefaultScopes.Add(scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
builder.Services.AddAuthentication();
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddStellaOpsScopeHandler();
|
||||
builder.Services.AddStellaOpsResourceServerAuthentication(
|
||||
builder.Configuration,
|
||||
configurationSection: $"{PolicyEngineOptions.SectionName}:ResourceServer");
|
||||
|
||||
if (bootstrap.Options.Authority.Enabled)
|
||||
{
|
||||
builder.Services.AddStellaOpsAuthClient(clientOptions =>
|
||||
{
|
||||
clientOptions.Authority = bootstrap.Options.Authority.Issuer;
|
||||
clientOptions.ClientId = bootstrap.Options.Authority.ClientId;
|
||||
clientOptions.ClientSecret = bootstrap.Options.Authority.ClientSecret;
|
||||
clientOptions.HttpTimeout = TimeSpan.FromSeconds(bootstrap.Options.Authority.BackchannelTimeoutSeconds);
|
||||
|
||||
clientOptions.DefaultScopes.Clear();
|
||||
foreach (var scope in bootstrap.Options.Authority.Scopes)
|
||||
{
|
||||
clientOptions.DefaultScopes.Add(scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapHealthChecks("/healthz");
|
||||
app.MapGet("/readyz", (PolicyEngineStartupDiagnostics diagnostics) =>
|
||||
diagnostics.IsReady
|
||||
|
||||
@@ -4,22 +4,34 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Engine.Compilation;
|
||||
using StellaOps.Policy.Engine.Options;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides deterministic compilation for <c>stella-dsl@1</c> policy documents and exposes
|
||||
/// basic statistics consumed by API/CLI surfaces.
|
||||
/// </summary>
|
||||
using StellaOps.PolicyDsl;
|
||||
using DslCompiler = StellaOps.PolicyDsl.PolicyCompiler;
|
||||
using DslCompilationResult = StellaOps.PolicyDsl.PolicyCompilationResult;
|
||||
using IrDocument = StellaOps.PolicyDsl.PolicyIrDocument;
|
||||
using IrAction = StellaOps.PolicyDsl.PolicyIrAction;
|
||||
using IrAssignmentAction = StellaOps.PolicyDsl.PolicyIrAssignmentAction;
|
||||
using IrAnnotateAction = StellaOps.PolicyDsl.PolicyIrAnnotateAction;
|
||||
using IrIgnoreAction = StellaOps.PolicyDsl.PolicyIrIgnoreAction;
|
||||
using IrEscalateAction = StellaOps.PolicyDsl.PolicyIrEscalateAction;
|
||||
using IrRequireVexAction = StellaOps.PolicyDsl.PolicyIrRequireVexAction;
|
||||
using IrWarnAction = StellaOps.PolicyDsl.PolicyIrWarnAction;
|
||||
using IrDeferAction = StellaOps.PolicyDsl.PolicyIrDeferAction;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides deterministic compilation for <c>stella-dsl@1</c> policy documents and exposes
|
||||
/// basic statistics consumed by API/CLI surfaces.
|
||||
/// </summary>
|
||||
internal sealed class PolicyCompilationService
|
||||
{
|
||||
private readonly PolicyCompiler compiler;
|
||||
private readonly DslCompiler compiler;
|
||||
private readonly PolicyComplexityAnalyzer complexityAnalyzer;
|
||||
private readonly IOptionsMonitor<PolicyEngineOptions> optionsMonitor;
|
||||
private readonly TimeProvider timeProvider;
|
||||
|
||||
public PolicyCompilationService(
|
||||
PolicyCompiler compiler,
|
||||
DslCompiler compiler,
|
||||
PolicyComplexityAnalyzer complexityAnalyzer,
|
||||
IOptionsMonitor<PolicyEngineOptions> optionsMonitor,
|
||||
TimeProvider timeProvider)
|
||||
@@ -29,30 +41,30 @@ internal sealed class PolicyCompilationService
|
||||
this.optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
|
||||
this.timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public PolicyCompilationResultDto Compile(PolicyCompileRequest request)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
if (request.Dsl is null || string.IsNullOrWhiteSpace(request.Dsl.Source))
|
||||
{
|
||||
throw new ArgumentException("Compilation requires DSL source.", nameof(request));
|
||||
}
|
||||
|
||||
if (!string.Equals(request.Dsl.Syntax, "stella-dsl@1", StringComparison.Ordinal))
|
||||
{
|
||||
|
||||
public PolicyCompilationResultDto Compile(PolicyCompileRequest request)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
if (request.Dsl is null || string.IsNullOrWhiteSpace(request.Dsl.Source))
|
||||
{
|
||||
throw new ArgumentException("Compilation requires DSL source.", nameof(request));
|
||||
}
|
||||
|
||||
if (!string.Equals(request.Dsl.Syntax, "stella-dsl@1", StringComparison.Ordinal))
|
||||
{
|
||||
return PolicyCompilationResultDto.FromFailure(
|
||||
ImmutableArray.Create(PolicyIssue.Error(
|
||||
PolicyDslDiagnosticCodes.UnsupportedSyntaxVersion,
|
||||
DiagnosticCodes.UnsupportedSyntaxVersion,
|
||||
$"Unsupported syntax '{request.Dsl.Syntax ?? "null"}'. Expected 'stella-dsl@1'.",
|
||||
"dsl.syntax")),
|
||||
complexity: null,
|
||||
durationMilliseconds: 0);
|
||||
}
|
||||
|
||||
|
||||
var start = timeProvider.GetTimestamp();
|
||||
var result = compiler.Compile(request.Dsl.Source);
|
||||
var elapsed = timeProvider.GetElapsedTime(start, timeProvider.GetTimestamp());
|
||||
@@ -95,11 +107,11 @@ internal sealed class PolicyCompilationService
|
||||
? ImmutableArray.Create(diagnostic)
|
||||
: diagnostics.Add(diagnostic);
|
||||
}
|
||||
|
||||
internal sealed record PolicyCompileRequest(PolicyDslPayload Dsl);
|
||||
|
||||
internal sealed record PolicyDslPayload(string Syntax, string Source);
|
||||
|
||||
|
||||
internal sealed record PolicyCompileRequest(PolicyDslPayload Dsl);
|
||||
|
||||
public sealed record PolicyDslPayload(string Syntax, string Source);
|
||||
|
||||
internal sealed record PolicyCompilationResultDto(
|
||||
bool Success,
|
||||
string? Digest,
|
||||
@@ -116,7 +128,7 @@ internal sealed record PolicyCompilationResultDto(
|
||||
new(false, null, null, ImmutableArray<byte>.Empty, diagnostics, complexity, durationMilliseconds);
|
||||
|
||||
public static PolicyCompilationResultDto FromSuccess(
|
||||
PolicyCompilationResult compilationResult,
|
||||
DslCompilationResult compilationResult,
|
||||
PolicyComplexityReport complexity,
|
||||
long durationMilliseconds)
|
||||
{
|
||||
@@ -136,45 +148,45 @@ internal sealed record PolicyCompilationResultDto(
|
||||
durationMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record PolicyCompilationStatistics(
|
||||
int RuleCount,
|
||||
ImmutableDictionary<string, int> ActionCounts)
|
||||
{
|
||||
public static PolicyCompilationStatistics Create(PolicyIrDocument document)
|
||||
{
|
||||
var actions = ImmutableDictionary.CreateBuilder<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
void Increment(string key)
|
||||
{
|
||||
actions[key] = actions.TryGetValue(key, out var existing) ? existing + 1 : 1;
|
||||
}
|
||||
|
||||
foreach (var rule in document.Rules)
|
||||
{
|
||||
foreach (var action in rule.ThenActions)
|
||||
{
|
||||
Increment(GetActionKey(action));
|
||||
}
|
||||
|
||||
foreach (var action in rule.ElseActions)
|
||||
{
|
||||
Increment($"else:{GetActionKey(action)}");
|
||||
}
|
||||
}
|
||||
|
||||
return new PolicyCompilationStatistics(document.Rules.Length, actions.ToImmutable());
|
||||
}
|
||||
|
||||
private static string GetActionKey(PolicyIrAction action) => action switch
|
||||
{
|
||||
PolicyIrAssignmentAction => "assign",
|
||||
PolicyIrAnnotateAction => "annotate",
|
||||
PolicyIrIgnoreAction => "ignore",
|
||||
PolicyIrEscalateAction => "escalate",
|
||||
PolicyIrRequireVexAction => "requireVex",
|
||||
PolicyIrWarnAction => "warn",
|
||||
PolicyIrDeferAction => "defer",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed record PolicyCompilationStatistics(
|
||||
int RuleCount,
|
||||
ImmutableDictionary<string, int> ActionCounts)
|
||||
{
|
||||
public static PolicyCompilationStatistics Create(IrDocument document)
|
||||
{
|
||||
var actions = ImmutableDictionary.CreateBuilder<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
void Increment(string key)
|
||||
{
|
||||
actions[key] = actions.TryGetValue(key, out var existing) ? existing + 1 : 1;
|
||||
}
|
||||
|
||||
foreach (var rule in document.Rules)
|
||||
{
|
||||
foreach (var action in rule.ThenActions)
|
||||
{
|
||||
Increment(GetActionKey(action));
|
||||
}
|
||||
|
||||
foreach (var action in rule.ElseActions)
|
||||
{
|
||||
Increment($"else:{GetActionKey(action)}");
|
||||
}
|
||||
}
|
||||
|
||||
return new PolicyCompilationStatistics(document.Rules.Length, actions.ToImmutable());
|
||||
}
|
||||
|
||||
private static string GetActionKey(IrAction action) => action switch
|
||||
{
|
||||
IrAssignmentAction => "assign",
|
||||
IrAnnotateAction => "annotate",
|
||||
IrIgnoreAction => "ignore",
|
||||
IrEscalateAction => "escalate",
|
||||
IrRequireVexAction => "requireVex",
|
||||
IrWarnAction => "warn",
|
||||
IrDeferAction => "defer",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Policy.Engine.Compilation;
|
||||
using StellaOps.PolicyDsl;
|
||||
using StellaOps.Policy.Engine.Evaluation;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Services;
|
||||
@@ -23,19 +23,19 @@ internal sealed partial class PolicyEvaluationService
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
internal PolicyEvaluationResult Evaluate(PolicyIrDocument document, PolicyEvaluationContext context)
|
||||
internal Evaluation.PolicyEvaluationResult Evaluate(PolicyIrDocument document, Evaluation.PolicyEvaluationContext context)
|
||||
{
|
||||
if (document is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(document));
|
||||
}
|
||||
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
throw new ArgumentNullException(nameof(document));
|
||||
}
|
||||
|
||||
var request = new PolicyEvaluationRequest(document, context);
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var request = new Evaluation.PolicyEvaluationRequest(document, context);
|
||||
return evaluator.Evaluate(request);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
||||
</PropertyGroup>
|
||||
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Policy/StellaOps.Policy.csproj" />
|
||||
<ProjectReference Include="../StellaOps.PolicyDsl/StellaOps.PolicyDsl.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user