audit, advisories and doctors/setup work

This commit is contained in:
master
2026-01-13 18:53:39 +02:00
parent 9ca7cb183e
commit d7be6ba34b
811 changed files with 54242 additions and 4056 deletions

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Policy.ToolLattice;
public sealed record ToolAccessContext
{
public required string TenantId { get; init; }
public required string Tool { get; init; }
public string? Action { get; init; }
public string? Resource { get; init; }
public IReadOnlyList<string> Scopes { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> Roles { get; init; } = Array.Empty<string>();
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Policy.ToolLattice;
public sealed record ToolAccessDecision
{
public required bool Allowed { get; init; }
public required string Reason { get; init; }
public string? RuleId { get; init; }
public ToolAccessEffect? RuleEffect { get; init; }
public IReadOnlyList<string> RequiredScopes { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> RequiredRoles { get; init; } = Array.Empty<string>();
}

View File

@@ -0,0 +1,7 @@
namespace StellaOps.Policy.ToolLattice;
public enum ToolAccessEffect
{
Allow,
Deny
}

View File

@@ -0,0 +1,433 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Options;
namespace StellaOps.Policy.ToolLattice;
public interface IToolAccessEvaluator
{
ToolAccessDecision Evaluate(ToolAccessContext context);
}
public sealed class ToolAccessEvaluator : IToolAccessEvaluator
{
private readonly ToolLatticeOptions options;
private readonly IReadOnlyList<NormalizedToolAccessRule> rules;
public ToolAccessEvaluator(IOptions<ToolLatticeOptions> options)
{
ArgumentNullException.ThrowIfNull(options);
this.options = options.Value ?? throw new ArgumentNullException(nameof(options));
rules = ToolLatticeRuleSet.Build(this.options);
}
public ToolAccessDecision Evaluate(ToolAccessContext context)
{
ArgumentNullException.ThrowIfNull(context);
if (!options.Enabled)
{
return new ToolAccessDecision
{
Allowed = true,
Reason = "tool_lattice_disabled"
};
}
var normalized = ToolLatticeContext.Normalize(context);
foreach (var rule in rules)
{
if (ToolLatticeRuleMatcher.Matches(rule, normalized))
{
var allowed = rule.Effect == ToolAccessEffect.Allow;
return new ToolAccessDecision
{
Allowed = allowed,
Reason = allowed ? "rule_allow" : "rule_deny",
RuleId = rule.Id,
RuleEffect = rule.Effect,
RequiredScopes = rule.Scopes,
RequiredRoles = rule.Roles
};
}
}
return options.AllowByDefault
? new ToolAccessDecision
{
Allowed = true,
Reason = "default_allow"
}
: new ToolAccessDecision
{
Allowed = false,
Reason = "default_deny"
};
}
}
internal sealed record NormalizedToolAccessRule(
string Id,
string Tool,
string? Action,
string? Resource,
ToolAccessEffect Effect,
int Priority,
IReadOnlyList<string> Scopes,
IReadOnlyList<string> Roles,
IReadOnlyList<string> Tenants,
int Order);
internal sealed record NormalizedToolAccessContext(
string TenantId,
string Tool,
string? Action,
string? Resource,
IReadOnlyList<string> Scopes,
IReadOnlyList<string> Roles);
internal static class ToolLatticeRuleSet
{
public static IReadOnlyList<NormalizedToolAccessRule> Build(ToolLatticeOptions options)
{
if (!options.Enabled)
{
return Array.Empty<NormalizedToolAccessRule>();
}
var defaults = options.UseDefaultRules
? ToolLatticeDefaults.CreateDefaults()
: Array.Empty<ToolAccessRule>();
var rules = new List<ToolAccessRule>(defaults.Count + options.Rules.Count);
rules.AddRange(defaults);
rules.AddRange(options.Rules);
var normalized = new List<NormalizedToolAccessRule>(rules.Count);
var order = 0;
foreach (var rule in defaults)
{
ValidateRule(rule, "default");
var id = NormalizeScalar(rule.Id);
if (string.IsNullOrWhiteSpace(id))
{
id = $"rule-{order}";
}
var tool = NormalizeScalar(rule.Tool) ?? string.Empty;
var action = NormalizeScalar(rule.Action);
var resource = NormalizeScalar(rule.Resource);
var scopes = NormalizeList(rule.Scopes);
var roles = NormalizeList(rule.Roles);
var tenants = NormalizeList(rule.Tenants);
normalized.Add(new NormalizedToolAccessRule(
id,
tool,
action,
resource,
rule.Effect,
rule.Priority,
scopes,
roles,
tenants,
order));
order++;
}
foreach (var rule in options.Rules)
{
ValidateRule(rule, "custom");
var id = NormalizeScalar(rule.Id);
if (string.IsNullOrWhiteSpace(id))
{
id = $"rule-{order}";
}
var tool = NormalizeScalar(rule.Tool) ?? string.Empty;
var action = NormalizeScalar(rule.Action);
var resource = NormalizeScalar(rule.Resource);
var scopes = NormalizeList(rule.Scopes);
var roles = NormalizeList(rule.Roles);
var tenants = NormalizeList(rule.Tenants);
normalized.Add(new NormalizedToolAccessRule(
id,
tool,
action,
resource,
rule.Effect,
rule.Priority,
scopes,
roles,
tenants,
order));
order++;
}
return normalized
.OrderByDescending(static rule => rule.Priority)
.ThenBy(static rule => rule.Order)
.ToArray();
}
public static void ValidateRule(ToolAccessRule rule, string source)
{
if (rule is null)
{
throw new InvalidOperationException($"Tool lattice {source} rule cannot be null.");
}
if (string.IsNullOrWhiteSpace(rule.Tool))
{
throw new InvalidOperationException($"Tool lattice {source} rule requires a tool pattern.");
}
if (!Enum.IsDefined(typeof(ToolAccessEffect), rule.Effect))
{
throw new InvalidOperationException($"Tool lattice {source} rule has invalid effect '{rule.Effect}'.");
}
}
private static string? NormalizeScalar(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return value.Trim().ToLowerInvariant();
}
private static IReadOnlyList<string> NormalizeList(IList<string> values)
{
if (values is null || values.Count == 0)
{
return Array.Empty<string>();
}
var normalized = new HashSet<string>(StringComparer.Ordinal);
foreach (var value in values)
{
var scalar = NormalizeScalar(value);
if (string.IsNullOrWhiteSpace(scalar))
{
continue;
}
normalized.Add(scalar);
}
return normalized.Count == 0
? Array.Empty<string>()
: normalized.OrderBy(static item => item, StringComparer.Ordinal).ToArray();
}
}
internal static class ToolLatticeContext
{
public static NormalizedToolAccessContext Normalize(ToolAccessContext context)
{
if (string.IsNullOrWhiteSpace(context.TenantId))
{
throw new InvalidOperationException("Tool lattice context requires a tenant id.");
}
if (string.IsNullOrWhiteSpace(context.Tool))
{
throw new InvalidOperationException("Tool lattice context requires a tool name.");
}
var tenant = context.TenantId.Trim().ToLowerInvariant();
var tool = context.Tool.Trim().ToLowerInvariant();
var action = NormalizeScalar(context.Action);
var resource = NormalizeScalar(context.Resource);
var scopes = NormalizeList(context.Scopes);
var roles = NormalizeList(context.Roles);
return new NormalizedToolAccessContext(
tenant,
tool,
action,
resource,
scopes,
roles);
}
private static string? NormalizeScalar(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return value.Trim().ToLowerInvariant();
}
private static IReadOnlyList<string> NormalizeList(IReadOnlyList<string> values)
{
if (values is null || values.Count == 0)
{
return Array.Empty<string>();
}
var normalized = new HashSet<string>(StringComparer.Ordinal);
foreach (var value in values)
{
var scalar = NormalizeScalar(value);
if (string.IsNullOrWhiteSpace(scalar))
{
continue;
}
normalized.Add(scalar);
}
return normalized.Count == 0
? Array.Empty<string>()
: normalized.OrderBy(static item => item, StringComparer.Ordinal).ToArray();
}
}
internal static class ToolLatticeRuleMatcher
{
public static bool Matches(NormalizedToolAccessRule rule, NormalizedToolAccessContext context)
{
if (!MatchesPattern(context.Tool, rule.Tool))
{
return false;
}
if (!MatchesPattern(context.Action, rule.Action))
{
return false;
}
if (!MatchesPattern(context.Resource, rule.Resource))
{
return false;
}
if (!MatchesValue(context.TenantId, rule.Tenants))
{
return false;
}
if (!MatchesAny(context.Roles, rule.Roles))
{
return false;
}
if (!MatchesAny(context.Scopes, rule.Scopes))
{
return false;
}
return true;
}
private static bool MatchesAny(IReadOnlyList<string> values, IReadOnlyList<string> patterns)
{
if (patterns.Count == 0)
{
return true;
}
if (values.Count == 0)
{
return false;
}
foreach (var value in values)
{
if (MatchesValue(value, patterns))
{
return true;
}
}
return false;
}
private static bool MatchesValue(string? value, IReadOnlyList<string> patterns)
{
if (patterns.Count == 0)
{
return true;
}
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
foreach (var pattern in patterns)
{
if (MatchesPattern(value, pattern))
{
return true;
}
}
return false;
}
private static bool MatchesPattern(string? value, string? pattern)
{
if (string.IsNullOrWhiteSpace(pattern))
{
return true;
}
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
if (pattern == "*")
{
return true;
}
if (!pattern.Contains('*'))
{
return string.Equals(value, pattern, StringComparison.Ordinal);
}
var segments = pattern.Split('*', StringSplitOptions.RemoveEmptyEntries);
if (segments.Length == 0)
{
return true;
}
if (pattern[0] != '*' &&
!value.StartsWith(segments[0], StringComparison.Ordinal))
{
return false;
}
var index = 0;
foreach (var segment in segments)
{
var matchIndex = value.IndexOf(segment, index, StringComparison.Ordinal);
if (matchIndex < 0)
{
return false;
}
index = matchIndex + segment.Length;
}
if (pattern[^1] != '*' &&
!value.EndsWith(segments[^1], StringComparison.Ordinal))
{
return false;
}
return true;
}
}

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace StellaOps.Policy.ToolLattice;
public sealed class ToolAccessRule
{
public string Id { get; set; } = string.Empty;
public string Tool { get; set; } = string.Empty;
public string? Action { get; set; }
public string? Resource { get; set; }
public ToolAccessEffect Effect { get; set; } = ToolAccessEffect.Allow;
public int Priority { get; set; }
public IList<string> Scopes { get; } = new List<string>();
public IList<string> Roles { get; } = new List<string>();
public IList<string> Tenants { get; } = new List<string>();
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
namespace StellaOps.Policy.ToolLattice;
internal static class ToolLatticeDefaults
{
public static IReadOnlyList<ToolAccessRule> CreateDefaults()
{
return new[]
{
new ToolAccessRule
{
Id = "default-vex-query",
Tool = "vex.query",
Action = "read",
Effect = ToolAccessEffect.Allow,
Priority = -100,
Scopes = { "vex:read" }
},
new ToolAccessRule
{
Id = "default-sbom-read",
Tool = "sbom.read",
Action = "read",
Effect = ToolAccessEffect.Allow,
Priority = -100,
Scopes = { "sbom:read" }
},
new ToolAccessRule
{
Id = "default-scanner-findings-topk",
Tool = "scanner.findings.topk",
Action = "read",
Effect = ToolAccessEffect.Allow,
Priority = -100,
Scopes = { "scanner:read", "findings:read" }
}
};
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Policy.ToolLattice;
public sealed class ToolLatticeOptions
{
public const string SectionName = "ToolLattice";
public bool Enabled { get; set; } = true;
public bool AllowByDefault { get; set; }
public bool UseDefaultRules { get; set; } = true;
public IList<ToolAccessRule> Rules { get; } = new List<ToolAccessRule>();
public void Validate()
{
if (!Enabled)
{
return;
}
foreach (var rule in Rules)
{
// TODO: Re-enable when ToolLatticeRuleSet is implemented
// ToolLatticeRuleSet.ValidateRule(rule, "custom");
}
}
}