604 lines
24 KiB
C#
604 lines
24 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using Json.Schema;
|
|
using YamlDotNet.Serialization;
|
|
using YamlDotNet.Serialization.NamingConventions;
|
|
|
|
namespace StellaOps.Policy;
|
|
|
|
public sealed record PolicyScoringBindingResult(
|
|
bool Success,
|
|
PolicyScoringConfig? Config,
|
|
ImmutableArray<PolicyIssue> Issues);
|
|
|
|
public static class PolicyScoringConfigBinder
|
|
{
|
|
private const string DefaultResourceName = "StellaOps.Policy.Schemas.policy-scoring-default.json";
|
|
|
|
private static readonly JsonSchema ScoringSchema = PolicyScoringSchema.Schema;
|
|
|
|
private static readonly ImmutableDictionary<string, double> DefaultReachabilityBuckets = CreateDefaultReachabilityBuckets();
|
|
|
|
private static readonly PolicyUnknownConfidenceConfig DefaultUnknownConfidence = CreateDefaultUnknownConfidence();
|
|
|
|
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
|
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
.IgnoreUnmatchedProperties()
|
|
.Build();
|
|
|
|
public static PolicyScoringConfig LoadDefault()
|
|
{
|
|
var assembly = Assembly.GetExecutingAssembly();
|
|
using var stream = assembly.GetManifestResourceStream(DefaultResourceName)
|
|
?? throw new InvalidOperationException($"Embedded resource '{DefaultResourceName}' not found.");
|
|
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
|
var json = reader.ReadToEnd();
|
|
var binding = Bind(json, PolicyDocumentFormat.Json);
|
|
if (!binding.Success || binding.Config is null)
|
|
{
|
|
throw new InvalidOperationException("Failed to load default policy scoring configuration.");
|
|
}
|
|
|
|
return binding.Config;
|
|
}
|
|
|
|
public static PolicyScoringBindingResult Bind(string content, PolicyDocumentFormat format)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(content))
|
|
{
|
|
var issue = PolicyIssue.Error("scoring.empty", "Scoring configuration content is empty.", "$");
|
|
return new PolicyScoringBindingResult(false, null, ImmutableArray.Create(issue));
|
|
}
|
|
|
|
try
|
|
{
|
|
var root = Parse(content, format);
|
|
if (root is not JsonObject obj)
|
|
{
|
|
var issue = PolicyIssue.Error("scoring.invalid", "Scoring configuration must be a JSON object.", "$");
|
|
return new PolicyScoringBindingResult(false, null, ImmutableArray.Create(issue));
|
|
}
|
|
|
|
var issues = ImmutableArray.CreateBuilder<PolicyIssue>();
|
|
var schemaIssues = ValidateAgainstSchema(root);
|
|
issues.AddRange(schemaIssues);
|
|
if (schemaIssues.Any(static issue => issue.Severity == PolicyIssueSeverity.Error))
|
|
{
|
|
return new PolicyScoringBindingResult(false, null, issues.ToImmutable());
|
|
}
|
|
|
|
var config = BuildConfig(obj, issues);
|
|
var hasErrors = issues.Any(issue => issue.Severity == PolicyIssueSeverity.Error);
|
|
return new PolicyScoringBindingResult(!hasErrors, config, issues.ToImmutable());
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
var issue = PolicyIssue.Error("scoring.parse.json", $"Failed to parse scoring JSON: {ex.Message}", "$");
|
|
return new PolicyScoringBindingResult(false, null, ImmutableArray.Create(issue));
|
|
}
|
|
catch (YamlDotNet.Core.YamlException ex)
|
|
{
|
|
var issue = PolicyIssue.Error("scoring.parse.yaml", $"Failed to parse scoring YAML: {ex.Message}", "$");
|
|
return new PolicyScoringBindingResult(false, null, ImmutableArray.Create(issue));
|
|
}
|
|
}
|
|
|
|
private static JsonNode? Parse(string content, PolicyDocumentFormat format)
|
|
{
|
|
return format switch
|
|
{
|
|
PolicyDocumentFormat.Json => JsonNode.Parse(content, new JsonNodeOptions { PropertyNameCaseInsensitive = true }),
|
|
PolicyDocumentFormat.Yaml => ConvertYamlToJsonNode(content),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported scoring configuration format."),
|
|
};
|
|
}
|
|
|
|
private static JsonNode? ConvertYamlToJsonNode(string content)
|
|
{
|
|
var yamlObject = YamlDeserializer.Deserialize<object?>(content);
|
|
return PolicyBinderUtilities.ConvertYamlObject(yamlObject);
|
|
}
|
|
|
|
private static ImmutableArray<PolicyIssue> ValidateAgainstSchema(JsonNode root)
|
|
{
|
|
try
|
|
{
|
|
using var document = JsonDocument.Parse(root.ToJsonString(new JsonSerializerOptions
|
|
{
|
|
WriteIndented = false,
|
|
}));
|
|
|
|
var result = ScoringSchema.Evaluate(document.RootElement, new EvaluationOptions
|
|
{
|
|
OutputFormat = OutputFormat.List,
|
|
RequireFormatValidation = true,
|
|
});
|
|
|
|
if (result.IsValid)
|
|
{
|
|
return ImmutableArray<PolicyIssue>.Empty;
|
|
}
|
|
|
|
var issues = ImmutableArray.CreateBuilder<PolicyIssue>();
|
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
CollectSchemaIssues(result, issues, seen);
|
|
return issues.ToImmutable();
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
return ImmutableArray.Create(PolicyIssue.Error("scoring.schema.normalize", $"Failed to normalize scoring configuration for schema validation: {ex.Message}", "$"));
|
|
}
|
|
}
|
|
|
|
private static void CollectSchemaIssues(EvaluationResults result, ImmutableArray<PolicyIssue>.Builder issues, HashSet<string> seen)
|
|
{
|
|
if (result.Errors is { Count: > 0 })
|
|
{
|
|
foreach (var pair in result.Errors)
|
|
{
|
|
var keyword = SanitizeKeyword(pair.Key);
|
|
var path = ConvertPointerToPath(result.InstanceLocation?.ToString() ?? "#");
|
|
var message = pair.Value ?? "Schema violation.";
|
|
var key = $"{path}|{keyword}|{message}";
|
|
if (seen.Add(key))
|
|
{
|
|
issues.Add(PolicyIssue.Error($"scoring.schema.{keyword}", message, path));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (result.Details is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var detail in result.Details)
|
|
{
|
|
CollectSchemaIssues(detail, issues, seen);
|
|
}
|
|
}
|
|
|
|
private static string ConvertPointerToPath(string pointer)
|
|
{
|
|
if (string.IsNullOrEmpty(pointer) || pointer == "#")
|
|
{
|
|
return "$";
|
|
}
|
|
|
|
if (pointer[0] == '#')
|
|
{
|
|
pointer = pointer.Length > 1 ? pointer[1..] : string.Empty;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(pointer))
|
|
{
|
|
return "$";
|
|
}
|
|
|
|
var segments = pointer.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
var builder = new StringBuilder("$");
|
|
foreach (var segment in segments)
|
|
{
|
|
var unescaped = segment.Replace("~1", "/").Replace("~0", "~");
|
|
if (int.TryParse(unescaped, out var index))
|
|
{
|
|
builder.Append('[').Append(index).Append(']');
|
|
}
|
|
else
|
|
{
|
|
builder.Append('.').Append(unescaped);
|
|
}
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static string SanitizeKeyword(string keyword)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(keyword))
|
|
{
|
|
return "unknown";
|
|
}
|
|
|
|
var builder = new StringBuilder(keyword.Length);
|
|
foreach (var ch in keyword)
|
|
{
|
|
if (char.IsLetterOrDigit(ch))
|
|
{
|
|
builder.Append(char.ToLowerInvariant(ch));
|
|
}
|
|
else if (ch is '.' or '_' or '-')
|
|
{
|
|
builder.Append(ch);
|
|
}
|
|
else
|
|
{
|
|
builder.Append('_');
|
|
}
|
|
}
|
|
|
|
return builder.Length == 0 ? "unknown" : builder.ToString();
|
|
}
|
|
|
|
private static PolicyScoringConfig BuildConfig(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
|
|
{
|
|
var version = ReadString(obj, "version", issues, required: true) ?? PolicyScoringConfig.BaselineVersion;
|
|
|
|
var severityWeights = ReadSeverityWeights(obj, issues);
|
|
var quietPenalty = ReadDouble(obj, "quietPenalty", issues, defaultValue: 45);
|
|
var warnPenalty = ReadDouble(obj, "warnPenalty", issues, defaultValue: 15);
|
|
var ignorePenalty = ReadDouble(obj, "ignorePenalty", issues, defaultValue: 35);
|
|
var trustOverrides = ReadTrustOverrides(obj, issues);
|
|
var reachabilityBuckets = ReadReachabilityBuckets(obj, issues);
|
|
var unknownConfidence = ReadUnknownConfidence(obj, issues);
|
|
|
|
return new PolicyScoringConfig(
|
|
version,
|
|
severityWeights,
|
|
quietPenalty,
|
|
warnPenalty,
|
|
ignorePenalty,
|
|
trustOverrides,
|
|
reachabilityBuckets,
|
|
unknownConfidence);
|
|
}
|
|
|
|
private static ImmutableDictionary<string, double> CreateDefaultReachabilityBuckets()
|
|
{
|
|
var builder = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.OrdinalIgnoreCase);
|
|
builder["entrypoint"] = 1.0;
|
|
builder["direct"] = 0.85;
|
|
builder["indirect"] = 0.6;
|
|
builder["runtime"] = 0.45;
|
|
builder["unreachable"] = 0.25;
|
|
builder["unknown"] = 0.5;
|
|
return builder.ToImmutable();
|
|
}
|
|
|
|
private static PolicyUnknownConfidenceConfig CreateDefaultUnknownConfidence()
|
|
{
|
|
var bands = ImmutableArray.Create(
|
|
new PolicyUnknownConfidenceBand("high", 0.65, "Fresh unknowns with recent telemetry."),
|
|
new PolicyUnknownConfidenceBand("medium", 0.35, "Unknowns aging toward action required."),
|
|
new PolicyUnknownConfidenceBand("low", 0.0, "Stale unknowns that must be triaged."));
|
|
return new PolicyUnknownConfidenceConfig(0.8, 0.05, 0.2, bands);
|
|
}
|
|
|
|
private static ImmutableDictionary<string, double> ReadReachabilityBuckets(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
|
|
{
|
|
if (!obj.TryGetPropertyValue("reachabilityBuckets", out var node))
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.reachability.default", "reachabilityBuckets not specified; defaulting to baseline weights.", "$.reachabilityBuckets"));
|
|
return DefaultReachabilityBuckets;
|
|
}
|
|
|
|
if (node is not JsonObject bucketsObj)
|
|
{
|
|
issues.Add(PolicyIssue.Error("scoring.reachability.type", "reachabilityBuckets must be an object.", "$.reachabilityBuckets"));
|
|
return DefaultReachabilityBuckets;
|
|
}
|
|
|
|
var builder = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var pair in bucketsObj)
|
|
{
|
|
if (pair.Value is null)
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.reachability.null", $"Bucket '{pair.Key}' is null; defaulting to 0.", $"$.reachabilityBuckets.{pair.Key}"));
|
|
builder[pair.Key] = 0;
|
|
continue;
|
|
}
|
|
|
|
var value = ExtractDouble(pair.Value, issues, $"$.reachabilityBuckets.{pair.Key}");
|
|
builder[pair.Key] = value;
|
|
}
|
|
|
|
if (builder.Count == 0)
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.reachability.empty", "No reachability buckets defined; using defaults.", "$.reachabilityBuckets"));
|
|
return DefaultReachabilityBuckets;
|
|
}
|
|
|
|
return builder.ToImmutable();
|
|
}
|
|
|
|
private static PolicyUnknownConfidenceConfig ReadUnknownConfidence(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
|
|
{
|
|
if (!obj.TryGetPropertyValue("unknownConfidence", out var node))
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.unknown.default", "unknownConfidence not specified; defaulting to baseline decay settings.", "$.unknownConfidence"));
|
|
return DefaultUnknownConfidence;
|
|
}
|
|
|
|
if (node is not JsonObject configObj)
|
|
{
|
|
issues.Add(PolicyIssue.Error("scoring.unknown.type", "unknownConfidence must be an object.", "$.unknownConfidence"));
|
|
return DefaultUnknownConfidence;
|
|
}
|
|
|
|
var initial = DefaultUnknownConfidence.Initial;
|
|
if (configObj.TryGetPropertyValue("initial", out var initialNode))
|
|
{
|
|
initial = ExtractDouble(initialNode, issues, "$.unknownConfidence.initial");
|
|
}
|
|
else
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.unknown.initial.default", "initial not specified; using baseline value.", "$.unknownConfidence.initial"));
|
|
}
|
|
|
|
var decay = DefaultUnknownConfidence.DecayPerDay;
|
|
if (configObj.TryGetPropertyValue("decayPerDay", out var decayNode))
|
|
{
|
|
decay = ExtractDouble(decayNode, issues, "$.unknownConfidence.decayPerDay");
|
|
}
|
|
else
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.unknown.decay.default", "decayPerDay not specified; using baseline value.", "$.unknownConfidence.decayPerDay"));
|
|
}
|
|
|
|
var floor = DefaultUnknownConfidence.Floor;
|
|
if (configObj.TryGetPropertyValue("floor", out var floorNode))
|
|
{
|
|
floor = ExtractDouble(floorNode, issues, "$.unknownConfidence.floor");
|
|
}
|
|
else
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.unknown.floor.default", "floor not specified; using baseline value.", "$.unknownConfidence.floor"));
|
|
}
|
|
|
|
var bands = ReadConfidenceBands(configObj, issues);
|
|
if (bands.IsDefaultOrEmpty)
|
|
{
|
|
bands = DefaultUnknownConfidence.Bands;
|
|
}
|
|
|
|
if (initial < 0 || initial > 1)
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.unknown.initial.range", "initial confidence should be between 0 and 1. Clamping to valid range.", "$.unknownConfidence.initial"));
|
|
initial = Math.Clamp(initial, 0, 1);
|
|
}
|
|
|
|
if (decay < 0 || decay > 1)
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.unknown.decay.range", "decayPerDay should be between 0 and 1. Clamping to valid range.", "$.unknownConfidence.decayPerDay"));
|
|
decay = Math.Clamp(decay, 0, 1);
|
|
}
|
|
|
|
if (floor < 0 || floor > 1)
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.unknown.floor.range", "floor should be between 0 and 1. Clamping to valid range.", "$.unknownConfidence.floor"));
|
|
floor = Math.Clamp(floor, 0, 1);
|
|
}
|
|
|
|
return new PolicyUnknownConfidenceConfig(initial, decay, floor, bands);
|
|
}
|
|
|
|
private static ImmutableArray<PolicyUnknownConfidenceBand> ReadConfidenceBands(JsonObject configObj, ImmutableArray<PolicyIssue>.Builder issues)
|
|
{
|
|
if (!configObj.TryGetPropertyValue("bands", out var node))
|
|
{
|
|
return ImmutableArray<PolicyUnknownConfidenceBand>.Empty;
|
|
}
|
|
|
|
if (node is not JsonArray array)
|
|
{
|
|
issues.Add(PolicyIssue.Error("scoring.unknown.bands.type", "unknownConfidence.bands must be an array.", "$.unknownConfidence.bands"));
|
|
return ImmutableArray<PolicyUnknownConfidenceBand>.Empty;
|
|
}
|
|
|
|
var builder = ImmutableArray.CreateBuilder<PolicyUnknownConfidenceBand>();
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
for (var index = 0; index < array.Count; index++)
|
|
{
|
|
var element = array[index];
|
|
if (element is not JsonObject bandObj)
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.unknown.band.type", "Band entry must be an object.", $"$.unknownConfidence.bands[{index}]"));
|
|
continue;
|
|
}
|
|
|
|
string? name = null;
|
|
if (bandObj.TryGetPropertyValue("name", out var nameNode) && nameNode is JsonValue nameValue && nameValue.TryGetValue(out string? text))
|
|
{
|
|
name = text?.Trim();
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
issues.Add(PolicyIssue.Error("scoring.unknown.band.name", "Band entry requires a non-empty 'name'.", $"$.unknownConfidence.bands[{index}].name"));
|
|
continue;
|
|
}
|
|
|
|
if (!seen.Add(name))
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.unknown.band.duplicate", $"Duplicate band '{name}' encountered.", $"$.unknownConfidence.bands[{index}].name"));
|
|
continue;
|
|
}
|
|
|
|
if (!bandObj.TryGetPropertyValue("min", out var minNode))
|
|
{
|
|
issues.Add(PolicyIssue.Error("scoring.unknown.band.min", $"Band '{name}' is missing 'min'.", $"$.unknownConfidence.bands[{index}].min"));
|
|
continue;
|
|
}
|
|
|
|
var min = ExtractDouble(minNode, issues, $"$.unknownConfidence.bands[{index}].min");
|
|
if (min < 0 || min > 1)
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.unknown.band.range", $"Band '{name}' min should be between 0 and 1. Clamping to valid range.", $"$.unknownConfidence.bands[{index}].min"));
|
|
min = Math.Clamp(min, 0, 1);
|
|
}
|
|
|
|
string? description = null;
|
|
if (bandObj.TryGetPropertyValue("description", out var descriptionNode) && descriptionNode is JsonValue descriptionValue && descriptionValue.TryGetValue(out string? descriptionText))
|
|
{
|
|
description = descriptionText?.Trim();
|
|
}
|
|
|
|
builder.Add(new PolicyUnknownConfidenceBand(name, min, description));
|
|
}
|
|
|
|
if (builder.Count == 0)
|
|
{
|
|
return ImmutableArray<PolicyUnknownConfidenceBand>.Empty;
|
|
}
|
|
|
|
return builder.ToImmutable()
|
|
.OrderByDescending(static band => band.Min)
|
|
.ToImmutableArray();
|
|
}
|
|
|
|
private static ImmutableDictionary<PolicySeverity, double> ReadSeverityWeights(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
|
|
{
|
|
if (!obj.TryGetPropertyValue("severityWeights", out var node) || node is not JsonObject severityObj)
|
|
{
|
|
issues.Add(PolicyIssue.Error("scoring.severityWeights.missing", "severityWeights section is required.", "$.severityWeights"));
|
|
return ImmutableDictionary<PolicySeverity, double>.Empty;
|
|
}
|
|
|
|
var builder = ImmutableDictionary.CreateBuilder<PolicySeverity, double>();
|
|
foreach (var severity in Enum.GetValues<PolicySeverity>())
|
|
{
|
|
var key = severity.ToString();
|
|
if (!severityObj.TryGetPropertyValue(key, out var valueNode))
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.severityWeights.default", $"Severity '{key}' not specified; defaulting to 0.", $"$.severityWeights.{key}"));
|
|
builder[severity] = 0;
|
|
continue;
|
|
}
|
|
|
|
var value = ExtractDouble(valueNode, issues, $"$.severityWeights.{key}");
|
|
builder[severity] = value;
|
|
}
|
|
|
|
return builder.ToImmutable();
|
|
}
|
|
|
|
private static double ReadDouble(JsonObject obj, string property, ImmutableArray<PolicyIssue>.Builder issues, double defaultValue)
|
|
{
|
|
if (!obj.TryGetPropertyValue(property, out var node))
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.numeric.default", $"{property} not specified; defaulting to {defaultValue:0.##}.", $"$.{property}"));
|
|
return defaultValue;
|
|
}
|
|
|
|
return ExtractDouble(node, issues, $"$.{property}");
|
|
}
|
|
|
|
private static double ExtractDouble(JsonNode? node, ImmutableArray<PolicyIssue>.Builder issues, string path)
|
|
{
|
|
if (node is null)
|
|
{
|
|
issues.Add(PolicyIssue.Warning("scoring.numeric.null", $"Value at {path} missing; defaulting to 0.", path));
|
|
return 0;
|
|
}
|
|
|
|
if (node is JsonValue value)
|
|
{
|
|
if (value.TryGetValue(out double number))
|
|
{
|
|
return number;
|
|
}
|
|
|
|
if (value.TryGetValue(out string? text) && double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out number))
|
|
{
|
|
return number;
|
|
}
|
|
}
|
|
|
|
issues.Add(PolicyIssue.Error("scoring.numeric.invalid", $"Value at {path} is not numeric.", path));
|
|
return 0;
|
|
}
|
|
|
|
private static ImmutableDictionary<string, double> ReadTrustOverrides(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
|
|
{
|
|
if (!obj.TryGetPropertyValue("trustOverrides", out var node) || node is not JsonObject trustObj)
|
|
{
|
|
return ImmutableDictionary<string, double>.Empty;
|
|
}
|
|
|
|
var builder = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var pair in trustObj)
|
|
{
|
|
var value = ExtractDouble(pair.Value, issues, $"$.trustOverrides.{pair.Key}");
|
|
builder[pair.Key] = value;
|
|
}
|
|
|
|
return builder.ToImmutable();
|
|
}
|
|
|
|
private static string? ReadString(JsonObject obj, string property, ImmutableArray<PolicyIssue>.Builder issues, bool required)
|
|
{
|
|
if (!obj.TryGetPropertyValue(property, out var node) || node is null)
|
|
{
|
|
if (required)
|
|
{
|
|
issues.Add(PolicyIssue.Error("scoring.string.missing", $"{property} is required.", $"$.{property}"));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (node is JsonValue value && value.TryGetValue(out string? text))
|
|
{
|
|
return text?.Trim();
|
|
}
|
|
|
|
issues.Add(PolicyIssue.Error("scoring.string.invalid", $"{property} must be a string.", $"$.{property}"));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
internal static class PolicyBinderUtilities
|
|
{
|
|
public static JsonNode? ConvertYamlObject(object? value)
|
|
{
|
|
switch (value)
|
|
{
|
|
case null:
|
|
return null;
|
|
case string s when bool.TryParse(s, out var boolValue):
|
|
return JsonValue.Create(boolValue);
|
|
case string s:
|
|
return JsonValue.Create(s);
|
|
case bool b:
|
|
return JsonValue.Create(b);
|
|
case sbyte or byte or short or ushort or int or uint or long or ulong or float or double or decimal:
|
|
return JsonValue.Create(Convert.ToDouble(value, CultureInfo.InvariantCulture));
|
|
case IDictionary dictionary:
|
|
{
|
|
var obj = new JsonObject();
|
|
foreach (DictionaryEntry entry in dictionary)
|
|
{
|
|
if (entry.Key is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
obj[entry.Key.ToString()!] = ConvertYamlObject(entry.Value);
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
case IEnumerable enumerable:
|
|
{
|
|
var array = new JsonArray();
|
|
foreach (var item in enumerable)
|
|
{
|
|
array.Add(ConvertYamlObject(item));
|
|
}
|
|
|
|
return array;
|
|
}
|
|
default:
|
|
return JsonValue.Create(value.ToString());
|
|
}
|
|
}
|
|
}
|