Files
git.stella-ops.org/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/PolicyBundle.cs
2026-01-08 08:38:27 +02:00

291 lines
8.5 KiB
C#

/**
* PolicyBundle - Policy configuration for trust evaluation.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Task: TRUST-014
* Update: SPRINT_4300_0002_0001 (BUDGET-002) - Added UnknownBudgets support.
*
* Defines trust roots, trust requirements, selection rule overrides, and unknown budgets.
*/
using System.Collections.Immutable;
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// A trust root defines a trusted principal and its authority scope.
/// </summary>
public sealed record TrustRoot
{
/// <summary>
/// The trusted principal.
/// </summary>
public required Principal Principal { get; init; }
/// <summary>
/// The authority scope for this principal.
/// </summary>
public required AuthorityScope Scope { get; init; }
/// <summary>
/// Maximum assurance level granted to this principal.
/// </summary>
public AssuranceLevel MaxAssurance { get; init; } = AssuranceLevel.A3_ProvenanceBound;
/// <summary>
/// Whether this root is currently active.
/// </summary>
public bool IsActive { get; init; } = true;
/// <summary>
/// Expiration time for this trust root.
/// </summary>
public DateTimeOffset? ExpiresAt { get; init; }
}
/// <summary>
/// Trust requirements for disposition decisions.
/// </summary>
public sealed record TrustRequirements
{
/// <summary>
/// Minimum assurance level required for "resolved" dispositions.
/// </summary>
public AssuranceLevel MinResolvedAssurance { get; init; } = AssuranceLevel.A2_VerifiedIdentity;
/// <summary>
/// Minimum assurance level required for "resolved_with_pedigree".
/// </summary>
public AssuranceLevel MinPedigreeAssurance { get; init; } = AssuranceLevel.A3_ProvenanceBound;
/// <summary>
/// Minimum evidence class for certain atom types.
/// </summary>
public EvidenceClass MinEvidenceClass { get; init; } = EvidenceClass.E1_SbomLinkage;
/// <summary>
/// Maximum age for fresh claims (null = no limit).
/// </summary>
public TimeSpan? MaxClaimAge { get; init; }
/// <summary>
/// Whether to require signature verification for all claims.
/// </summary>
public bool RequireSignatures { get; init; } = false;
}
/// <summary>
/// Unknown budget rule for policy bundles.
/// Sprint: SPRINT_4300_0002_0001 (BUDGET-002)
/// </summary>
public sealed record PolicyBundleUnknownBudget
{
/// <summary>
/// Budget name identifier.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Environment filter: "production", "staging", "dev", or "*" for all.
/// </summary>
public string Environment { get; init; } = "*";
/// <summary>
/// Maximum unknown tier allowed (T1=strict, T4=permissive).
/// Null means no tier restriction.
/// </summary>
public int? TierMax { get; init; }
/// <summary>
/// Maximum total unknown count allowed.
/// Null means no count restriction.
/// </summary>
public int? CountMax { get; init; }
/// <summary>
/// Maximum mean entropy allowed (0.0-1.0).
/// Null means no entropy restriction.
/// </summary>
public double? EntropyMax { get; init; }
/// <summary>
/// Per-reason-code limits.
/// Keys are reason code names (e.g., "Reachability", "Identity").
/// </summary>
public ImmutableDictionary<string, int> ReasonLimits { get; init; } =
ImmutableDictionary<string, int>.Empty;
/// <summary>
/// Action to take when budget is exceeded: "block" or "warn".
/// </summary>
public string Action { get; init; } = "warn";
/// <summary>
/// Custom message to display when budget is exceeded.
/// </summary>
public string? Message { get; init; }
}
/// <summary>
/// Conflict resolution strategy.
/// </summary>
public enum ConflictResolution
{
/// <summary>
/// Report conflict, let human decide (default).
/// </summary>
ReportConflict,
/// <summary>
/// Use highest trust value.
/// </summary>
PreferHigherTrust,
/// <summary>
/// Use most recent claim.
/// </summary>
PreferMostRecent,
/// <summary>
/// Conservative: assume worst case.
/// </summary>
PreferConservative,
}
/// <summary>
/// Policy bundle configuration for the trust lattice engine.
/// </summary>
public sealed record PolicyBundle
{
/// <summary>
/// Policy bundle identifier.
/// </summary>
public string? Id { get; init; }
/// <summary>
/// Human-readable name.
/// </summary>
public string? Name { get; init; }
/// <summary>
/// Policy bundle version.
/// </summary>
public string Version { get; init; } = "1.0.0";
/// <summary>
/// Trusted principals (trust roots).
/// </summary>
public IReadOnlyList<TrustRoot> TrustRoots { get; init; } = [];
/// <summary>
/// Trust requirements for dispositions.
/// </summary>
public TrustRequirements TrustRequirements { get; init; } = new();
/// <summary>
/// Custom selection rules (merged with baseline).
/// </summary>
public IReadOnlyList<SelectionRule> CustomRules { get; init; } = [];
/// <summary>
/// Conflict resolution strategy.
/// </summary>
public ConflictResolution ConflictResolution { get; init; } = ConflictResolution.ReportConflict;
/// <summary>
/// Whether to assume reachability when unknown.
/// </summary>
public bool AssumeReachableWhenUnknown { get; init; } = true;
/// <summary>
/// VEX formats to accept.
/// </summary>
public IReadOnlyList<string> AcceptedVexFormats { get; init; } =
["CycloneDX/ECMA-424", "OpenVEX", "CSAF"];
/// <summary>
/// Unknown budget rules for environment-scoped enforcement.
/// Sprint: SPRINT_4300_0002_0001 (BUDGET-002)
/// </summary>
public IReadOnlyList<PolicyBundleUnknownBudget> UnknownBudgets { get; init; } = [];
/// <summary>
/// Gets the merged selection rules (custom + baseline).
/// </summary>
public IReadOnlyList<SelectionRule> GetEffectiveRules()
{
var baseline = DispositionSelector.GetBaselineRules().ToList();
// Custom rules override baseline rules with same name
var customByName = CustomRules.ToDictionary(r => r.Name);
for (int i = 0; i < baseline.Count; i++)
{
if (customByName.TryGetValue(baseline[i].Name, out var custom))
{
baseline[i] = custom;
}
}
// Add new custom rules
var baselineNames = baseline.Select(r => r.Name).ToHashSet();
baseline.AddRange(CustomRules.Where(r => !baselineNames.Contains(r.Name)));
return baseline.OrderBy(r => r.Priority).ToList();
}
/// <summary>
/// Checks if a principal is trusted for a given scope.
/// </summary>
/// <param name="principal">The principal to check.</param>
/// <param name="asOf">Timestamp for trust evaluation. Allows deterministic testing.</param>
/// <param name="requiredScope">Optional required authority scope.</param>
public bool IsTrusted(Principal principal, DateTimeOffset asOf, AuthorityScope? requiredScope = null)
{
var now = asOf;
foreach (var root in TrustRoots)
{
if (!root.IsActive) continue;
if (root.ExpiresAt.HasValue && root.ExpiresAt.Value < now) continue;
if (root.Principal.Id != principal.Id) continue;
if (requiredScope is null || root.Scope.Covers(requiredScope))
{
return true;
}
}
return false;
}
/// <summary>
/// Gets the maximum assurance level for a principal.
/// </summary>
/// <param name="principal">The principal to check.</param>
/// <param name="asOf">Timestamp for trust evaluation. Allows deterministic testing.</param>
public AssuranceLevel? GetMaxAssurance(Principal principal, DateTimeOffset asOf)
{
var now = asOf;
foreach (var root in TrustRoots)
{
if (!root.IsActive) continue;
if (root.ExpiresAt.HasValue && root.ExpiresAt.Value < now) continue;
if (root.Principal.Id != principal.Id) continue;
return root.MaxAssurance;
}
return null;
}
/// <summary>
/// Creates a default policy bundle with no trust roots.
/// </summary>
public static PolicyBundle Default => new()
{
Id = "default",
Name = "Default Policy",
Version = "1.0.0",
};
}