using System; using System.Collections.Immutable; using System.Linq; namespace StellaOps.Policy; /// /// Canonical representation of a StellaOps policy document. /// public sealed record PolicyDocument( string Version, ImmutableArray Rules, ImmutableDictionary Metadata, PolicyExceptionConfiguration Exceptions) { public static PolicyDocument Empty { get; } = new( PolicySchema.CurrentVersion, ImmutableArray.Empty, ImmutableDictionary.Empty, PolicyExceptionConfiguration.Empty); } public static class PolicySchema { public const string SchemaId = "https://schemas.stella-ops.org/policy/policy-schema@1.json"; public const string CurrentVersion = "1.0"; public static PolicyDocumentFormat DetectFormat(string fileName) { if (fileName is null) { throw new ArgumentNullException(nameof(fileName)); } var lower = fileName.Trim().ToLowerInvariant(); if (lower.EndsWith(".yaml", StringComparison.Ordinal) || lower.EndsWith(".yml", StringComparison.Ordinal) || lower.EndsWith(".stella", StringComparison.Ordinal)) { return PolicyDocumentFormat.Yaml; } return PolicyDocumentFormat.Json; } } public sealed record PolicyRule( string Name, string? Identifier, string? Description, PolicyAction Action, ImmutableArray Severities, ImmutableArray Environments, ImmutableArray Sources, ImmutableArray Vendors, ImmutableArray Licenses, ImmutableArray Tags, PolicyRuleMatchCriteria Match, DateTimeOffset? Expires, string? Justification, ImmutableDictionary Metadata) { public static PolicyRuleMatchCriteria EmptyMatch { get; } = new( ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty); public static PolicyRule Create( string name, PolicyAction action, ImmutableArray severities, ImmutableArray environments, ImmutableArray sources, ImmutableArray vendors, ImmutableArray licenses, ImmutableArray tags, PolicyRuleMatchCriteria match, DateTimeOffset? expires, string? justification, string? identifier = null, string? description = null, ImmutableDictionary? metadata = null) { metadata ??= ImmutableDictionary.Empty; return new PolicyRule( name, identifier, description, action, severities, environments, sources, vendors, licenses, tags, match, expires, justification, metadata); } public bool MatchesAnyEnvironment => Environments.IsDefaultOrEmpty; } public sealed record PolicyRuleMatchCriteria( ImmutableArray Images, ImmutableArray Repositories, ImmutableArray Packages, ImmutableArray Purls, ImmutableArray Cves, ImmutableArray Paths, ImmutableArray LayerDigests, ImmutableArray UsedByEntrypoint) { public static PolicyRuleMatchCriteria Create( ImmutableArray images, ImmutableArray repositories, ImmutableArray packages, ImmutableArray purls, ImmutableArray cves, ImmutableArray paths, ImmutableArray layerDigests, ImmutableArray usedByEntrypoint) => new( images, repositories, packages, purls, cves, paths, layerDigests, usedByEntrypoint); public static PolicyRuleMatchCriteria Empty { get; } = new( ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty); public bool IsEmpty => Images.IsDefaultOrEmpty && Repositories.IsDefaultOrEmpty && Packages.IsDefaultOrEmpty && Purls.IsDefaultOrEmpty && Cves.IsDefaultOrEmpty && Paths.IsDefaultOrEmpty && LayerDigests.IsDefaultOrEmpty && UsedByEntrypoint.IsDefaultOrEmpty; } public sealed record PolicyAction( PolicyActionType Type, PolicyIgnoreOptions? Ignore, PolicyEscalateOptions? Escalate, PolicyRequireVexOptions? RequireVex, bool Quiet); public enum PolicyActionType { Block, Ignore, Warn, Defer, Escalate, RequireVex, } public sealed record PolicyIgnoreOptions(DateTimeOffset? Until, string? Justification); public sealed record PolicyEscalateOptions( PolicySeverity? MinimumSeverity, bool RequireKev, double? MinimumEpss); public sealed record PolicyRequireVexOptions( ImmutableArray Vendors, ImmutableArray Justifications); public enum PolicySeverity { Critical, High, Medium, Low, Informational, None, Unknown, } public sealed record PolicyExceptionConfiguration( ImmutableArray Effects, ImmutableArray RoutingTemplates) { public static PolicyExceptionConfiguration Empty { get; } = new( ImmutableArray.Empty, ImmutableArray.Empty); public PolicyExceptionEffect? FindEffect(string effectId) { if (string.IsNullOrWhiteSpace(effectId) || Effects.IsDefaultOrEmpty) { return null; } return Effects.FirstOrDefault(effect => string.Equals(effect.Id, effectId, StringComparison.OrdinalIgnoreCase)); } } public sealed record PolicyExceptionEffect( string Id, string? Name, PolicyExceptionEffectType Effect, PolicySeverity? DowngradeSeverity, string? RequiredControlId, string? RoutingTemplate, int? MaxDurationDays, string? Description); public enum PolicyExceptionEffectType { Suppress, Defer, Downgrade, RequireControl, } public sealed record PolicyExceptionRoutingTemplate( string Id, string AuthorityRouteId, bool RequireMfa, string? Description);