up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-28 19:23:54 +02:00
parent d1cbb905f8
commit d040c001ac
36 changed files with 4668 additions and 9 deletions

View File

@@ -115,6 +115,11 @@ internal sealed class PolicyExpressionEvaluator
return rubyScope.Get(member.Member);
}
if (raw is MacOsComponentScope macosScope)
{
return macosScope.Get(member.Member);
}
if (raw is ImmutableDictionary<string, object?> dict && dict.TryGetValue(member.Member, out var value))
{
return new EvaluationValue(value);
@@ -155,6 +160,11 @@ internal sealed class PolicyExpressionEvaluator
return rubyScope.Invoke(member.Member, invocation.Arguments, scope, this);
}
if (targetRaw is MacOsComponentScope macosScope)
{
return macosScope.Invoke(member.Member, invocation.Arguments, scope, this);
}
if (targetRaw is ComponentScope componentScope)
{
return componentScope.Invoke(member.Member, invocation.Arguments, scope, this);
@@ -497,6 +507,14 @@ internal sealed class PolicyExpressionEvaluator
locals["ruby"] = new RubyComponentScope(component);
}
// Add macOS scope for brew packages, pkgutil receipts, and macOS bundles
if (component.Type.Equals("brew", StringComparison.OrdinalIgnoreCase) ||
component.Metadata.ContainsKey("macos:bundle_id") ||
component.Metadata.ContainsKey("pkgutil:identifier"))
{
locals["macos"] = new MacOsComponentScope(component);
}
var nestedScope = EvaluationScope.FromLocals(scope.Globals, locals);
if (evaluator.EvaluateBoolean(predicate, nestedScope))
{
@@ -865,4 +883,227 @@ internal sealed class PolicyExpressionEvaluator
_ => EvaluationValue.Null,
};
}
/// <summary>
/// SPL scope for macOS component predicates.
/// Provides access to bundle signing, entitlements, sandboxing, and package receipt information.
/// </summary>
/// <example>
/// SPL predicates supported:
/// - macos.signed == true
/// - macos.sandboxed == true
/// - macos.hardened_runtime == true
/// - macos.team_id == "ABCD1234"
/// - macos.bundle_id == "com.apple.Safari"
/// - macos.entitlement("com.apple.security.network.client")
/// - macos.entitlement_any(["com.apple.security.device.camera", "com.apple.security.device.microphone"])
/// - macos.high_risk_entitlements == true
/// - macos.pkg_receipt("com.apple.pkg.Safari")
/// </example>
private sealed class MacOsComponentScope
{
private readonly PolicyEvaluationComponent component;
private readonly ImmutableHashSet<string> entitlementCategories;
private readonly ImmutableHashSet<string> highRiskEntitlements;
public MacOsComponentScope(PolicyEvaluationComponent component)
{
this.component = component;
entitlementCategories = ParseDelimitedSet(component.Metadata, "macos:capability_categories");
highRiskEntitlements = ParseDelimitedSet(component.Metadata, "macos:high_risk_entitlements");
}
public EvaluationValue Get(string member)
{
return member.ToLowerInvariant() switch
{
"signed" => new EvaluationValue(IsSigned()),
"sandboxed" => new EvaluationValue(IsTruthy(GetMetadata("macos:sandboxed"))),
"hardened_runtime" or "hardenedruntime" => new EvaluationValue(IsTruthy(GetMetadata("macos:hardened_runtime"))),
"team_id" or "teamid" => new EvaluationValue(GetMetadata("macos:team_id")),
"bundle_id" or "bundleid" => new EvaluationValue(GetMetadata("macos:bundle_id")),
"bundle_type" or "bundletype" => new EvaluationValue(GetMetadata("macos:bundle_type")),
"min_os_version" or "minosversion" => new EvaluationValue(GetMetadata("macos:min_os_version")),
"high_risk_entitlements" or "highriskentitlements" => new EvaluationValue(!highRiskEntitlements.IsEmpty),
"entitlement_categories" or "entitlementcategories" => new EvaluationValue(entitlementCategories.Select(c => (object?)c).ToImmutableArray()),
"pkg_identifier" or "pkgidentifier" => new EvaluationValue(GetMetadata("pkgutil:identifier")),
_ => 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 "entitlement":
{
var name = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
return new EvaluationValue(HasEntitlement(name));
}
case "entitlement_any":
{
var entitlements = EvaluateAsStringSet(arguments, scope, evaluator);
return new EvaluationValue(entitlements.Any(HasEntitlement));
}
case "category":
{
var name = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
return new EvaluationValue(name is not null && entitlementCategories.Contains(name));
}
case "category_any":
{
var categories = EvaluateAsStringSet(arguments, scope, evaluator);
return new EvaluationValue(categories.Any(c => entitlementCategories.Contains(c)));
}
case "signed":
{
if (arguments.Length == 0)
{
return new EvaluationValue(IsSigned());
}
// Check for specific team ID or hardened runtime
var teamId = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
var requireHardened = arguments.Length > 1 && evaluator.Evaluate(arguments[1], scope).AsBoolean();
var isSigned = IsSigned();
if (!isSigned)
{
return EvaluationValue.False;
}
if (!string.IsNullOrWhiteSpace(teamId))
{
var actualTeamId = GetMetadata("macos:team_id");
if (!string.Equals(actualTeamId, teamId, StringComparison.OrdinalIgnoreCase))
{
return EvaluationValue.False;
}
}
if (requireHardened && !IsTruthy(GetMetadata("macos:hardened_runtime")))
{
return EvaluationValue.False;
}
return EvaluationValue.True;
}
case "pkg_receipt":
{
var identifier = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
if (string.IsNullOrWhiteSpace(identifier))
{
return EvaluationValue.False;
}
var pkgId = GetMetadata("pkgutil:identifier");
if (string.IsNullOrWhiteSpace(pkgId))
{
return EvaluationValue.False;
}
if (arguments.Length > 1)
{
var version = evaluator.Evaluate(arguments[1], scope).AsString();
if (!string.IsNullOrWhiteSpace(version))
{
var pkgVersion = component.Version;
return new EvaluationValue(
string.Equals(pkgId, identifier, StringComparison.OrdinalIgnoreCase) &&
string.Equals(pkgVersion, version, StringComparison.Ordinal));
}
}
return new EvaluationValue(string.Equals(pkgId, identifier, StringComparison.OrdinalIgnoreCase));
}
default:
return EvaluationValue.Null;
}
}
private bool IsSigned()
{
// Consider signed if team_id is present or code_resources_hash exists
var teamId = GetMetadata("macos:team_id");
var codeResourcesHash = GetMetadata("macos:code_resources_hash");
return !string.IsNullOrWhiteSpace(teamId) || !string.IsNullOrWhiteSpace(codeResourcesHash);
}
private bool HasEntitlement(string? name)
{
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
// Check high risk entitlements first
if (highRiskEntitlements.Contains(name))
{
return true;
}
// Check capability categories for short names
if (entitlementCategories.Contains(name))
{
return true;
}
return false;
}
private string? GetMetadata(string key)
{
return component.Metadata.TryGetValue(key, out var value) ? value : null;
}
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> ParseDelimitedSet(ImmutableDictionary<string, string> metadata, string key)
{
if (!metadata.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
{
return ImmutableHashSet<string>.Empty;
}
return value
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(static v => !string.IsNullOrWhiteSpace(v))
.ToImmutableHashSet(StringComparer.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();
}
}
}