291 lines
8.5 KiB
C#
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",
|
|
};
|
|
}
|