- Fix PredicateSchemaValidator to use static Lazy initialization avoiding JsonSchema.Net global registry conflicts in tests - Add IContextPolicyGate interface for gates without MergeResult - Rename ICveGate/IAttestationGate to avoid conflicts with IPolicyGate - Add static Pass/Fail helper methods to GateResult record - Unseal PolicyGateContext to allow ExtendedPolicyGateContext - Add missing Type/Constraint properties to AuthorityScope and Principal - Fix PolicyBundle to use ConditionDescription instead of Condition func - Rename ExceptionResult to ExceptionCheckResult to avoid duplicate - Rename GateResult static helper class to GateResultFactory - Temporarily exclude 9 incomplete gate files with missing contracts - Add AttestationContextExtensions for GetAttestation/GetVexSummary etc All 216 Attestor.Core tests pass.
458 lines
13 KiB
C#
458 lines
13 KiB
C#
/**
|
|
* Trust Label and Principal - Trust algebra components.
|
|
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
|
|
* Tasks: TRUST-005, TRUST-006
|
|
*
|
|
* Trust is not a single number; it must represent:
|
|
* - Cryptographic verification
|
|
* - Identity assurance
|
|
* - Authority scope
|
|
* - Freshness/revocation
|
|
* - Evidence strength
|
|
*
|
|
* These models enable policy-driven trust evaluation that is
|
|
* deterministic and explainable.
|
|
*/
|
|
|
|
namespace StellaOps.Policy.TrustLattice;
|
|
|
|
/// <summary>
|
|
/// Assurance level for cryptographic and identity verification.
|
|
/// Increasing levels from A0 (weakest) to A4 (strongest).
|
|
/// </summary>
|
|
public enum AssuranceLevel
|
|
{
|
|
/// <summary>
|
|
/// A0: Unsigned or unverifiable assertion.
|
|
/// No cryptographic backing.
|
|
/// </summary>
|
|
A0_Unsigned = 0,
|
|
|
|
/// <summary>
|
|
/// A1: Signed, but weak identity binding.
|
|
/// Key is known but identity not strongly verified.
|
|
/// </summary>
|
|
A1_WeakIdentity = 1,
|
|
|
|
/// <summary>
|
|
/// A2: Signed with verified identity.
|
|
/// Certificate chain or keyless identity (OIDC) verified.
|
|
/// </summary>
|
|
A2_VerifiedIdentity = 2,
|
|
|
|
/// <summary>
|
|
/// A3: Signed with provenance binding.
|
|
/// Signature bound to artifact digest via attestation.
|
|
/// </summary>
|
|
A3_ProvenanceBound = 3,
|
|
|
|
/// <summary>
|
|
/// A4: Full transparency log inclusion.
|
|
/// Signed + provenance + Rekor/transparency log entry.
|
|
/// </summary>
|
|
A4_TransparencyLog = 4,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Freshness class for temporal validity of assertions.
|
|
/// </summary>
|
|
public enum FreshnessClass
|
|
{
|
|
/// <summary>
|
|
/// Unknown or missing timestamp.
|
|
/// </summary>
|
|
Unknown = 0,
|
|
|
|
/// <summary>
|
|
/// Expired assertion (past valid_until).
|
|
/// </summary>
|
|
Expired = 1,
|
|
|
|
/// <summary>
|
|
/// Stale assertion (older than freshness threshold).
|
|
/// </summary>
|
|
Stale = 2,
|
|
|
|
/// <summary>
|
|
/// Fresh assertion (within freshness threshold).
|
|
/// </summary>
|
|
Fresh = 3,
|
|
|
|
/// <summary>
|
|
/// Live assertion (just issued or real-time).
|
|
/// </summary>
|
|
Live = 4,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evidence class describing the strength of supporting evidence.
|
|
/// </summary>
|
|
public enum EvidenceClass
|
|
{
|
|
/// <summary>
|
|
/// E0: Statement only (no supporting evidence refs).
|
|
/// </summary>
|
|
E0_StatementOnly = 0,
|
|
|
|
/// <summary>
|
|
/// E1: SBOM linkage evidence.
|
|
/// Component present + version evidence.
|
|
/// </summary>
|
|
E1_SbomLinkage = 1,
|
|
|
|
/// <summary>
|
|
/// E2: Reachability/mitigation evidence.
|
|
/// Call paths, config snapshots, runtime proofs.
|
|
/// </summary>
|
|
E2_ReachabilityMitigation = 2,
|
|
|
|
/// <summary>
|
|
/// E3: Remediation evidence.
|
|
/// Patch diffs, pedigree/commit chain, fix verification.
|
|
/// </summary>
|
|
E3_Remediation = 3,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Role that a principal can play in the trust model.
|
|
/// </summary>
|
|
[Flags]
|
|
public enum PrincipalRole
|
|
{
|
|
None = 0,
|
|
|
|
/// <summary>
|
|
/// Vendor: Original software vendor.
|
|
/// Authoritative for their own products.
|
|
/// </summary>
|
|
Vendor = 1 << 0,
|
|
|
|
/// <summary>
|
|
/// Distributor: OS/distro package maintainer.
|
|
/// Authoritative for packages in their repositories.
|
|
/// </summary>
|
|
Distributor = 1 << 1,
|
|
|
|
/// <summary>
|
|
/// Scanner: Automated vulnerability scanner.
|
|
/// Provides detection evidence.
|
|
/// </summary>
|
|
Scanner = 1 << 2,
|
|
|
|
/// <summary>
|
|
/// Auditor: Security auditor or penetration tester.
|
|
/// Provides expert assessment evidence.
|
|
/// </summary>
|
|
Auditor = 1 << 3,
|
|
|
|
/// <summary>
|
|
/// InternalSecurity: Internal security team.
|
|
/// Authoritative for internal artifact reachability/mitigation.
|
|
/// </summary>
|
|
InternalSecurity = 1 << 4,
|
|
|
|
/// <summary>
|
|
/// BuildSystem: CI/CD build system.
|
|
/// Provides provenance and build evidence.
|
|
/// </summary>
|
|
BuildSystem = 1 << 5,
|
|
|
|
/// <summary>
|
|
/// RuntimeMonitor: Runtime observability system.
|
|
/// Provides runtime behavior evidence.
|
|
/// </summary>
|
|
RuntimeMonitor = 1 << 6,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Authority scope defining what subjects a principal is authoritative for.
|
|
/// </summary>
|
|
public sealed record AuthorityScope
|
|
{
|
|
/// <summary>
|
|
/// Scope type for canonical serialization.
|
|
/// </summary>
|
|
public string Type { get; init; } = "default";
|
|
|
|
/// <summary>
|
|
/// Constraint expression for the scope.
|
|
/// </summary>
|
|
public string? Constraint { get; init; }
|
|
|
|
/// <summary>
|
|
/// Product namespace patterns (e.g., "vendor.example/*").
|
|
/// Principal is authoritative for these products.
|
|
/// </summary>
|
|
public IReadOnlyList<string>? Products { get; init; }
|
|
|
|
/// <summary>
|
|
/// Package namespace patterns (e.g., "pkg:npm/*", "pkg:maven/org.example/*").
|
|
/// </summary>
|
|
public IReadOnlyList<string>? Packages { get; init; }
|
|
|
|
/// <summary>
|
|
/// Artifact digest patterns (e.g., "sha256:*" for internal artifacts).
|
|
/// </summary>
|
|
public IReadOnlyList<string>? Artifacts { get; init; }
|
|
|
|
/// <summary>
|
|
/// Vulnerability source patterns (e.g., "nvd", "osv").
|
|
/// </summary>
|
|
public IReadOnlyList<string>? VulnerabilitySources { get; init; }
|
|
|
|
/// <summary>
|
|
/// Checks if this scope covers a given subject.
|
|
/// </summary>
|
|
public bool Covers(Subject subject)
|
|
{
|
|
// Check artifacts
|
|
if (Artifacts is { Count: > 0 })
|
|
{
|
|
if (!MatchesAny(subject.Artifact.Digest, Artifacts))
|
|
return false;
|
|
}
|
|
|
|
// Check packages
|
|
if (Packages is { Count: > 0 })
|
|
{
|
|
var componentId = subject.Component.Purl ?? subject.Component.Id;
|
|
if (!MatchesAny(componentId, Packages))
|
|
return false;
|
|
}
|
|
|
|
// Check vulnerability sources
|
|
if (VulnerabilitySources is { Count: > 0 })
|
|
{
|
|
var source = subject.Vulnerability.Source ?? "";
|
|
if (!MatchesAny(source, VulnerabilitySources))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if this scope covers (is a superset of) another scope.
|
|
/// </summary>
|
|
public bool Covers(AuthorityScope other)
|
|
{
|
|
// A scope covers another if all patterns in other are covered by patterns in this scope
|
|
// Universal scope (*) covers everything
|
|
if (Artifacts is { Count: > 0 } && Artifacts.Contains("*"))
|
|
return true;
|
|
|
|
// Check that we cover all artifact patterns from the other scope
|
|
if (other.Artifacts is { Count: > 0 })
|
|
{
|
|
if (Artifacts is null || Artifacts.Count == 0)
|
|
return false;
|
|
foreach (var pattern in other.Artifacts)
|
|
{
|
|
if (!Artifacts.Any(a => PatternCovers(a, pattern)))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check that we cover all package patterns from the other scope
|
|
if (other.Packages is { Count: > 0 })
|
|
{
|
|
if (Packages is null || Packages.Count == 0)
|
|
return false;
|
|
foreach (var pattern in other.Packages)
|
|
{
|
|
if (!Packages.Any(p => PatternCovers(p, pattern)))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check vulnerability sources
|
|
if (other.VulnerabilitySources is { Count: > 0 })
|
|
{
|
|
if (VulnerabilitySources is null || VulnerabilitySources.Count == 0)
|
|
return false;
|
|
foreach (var source in other.VulnerabilitySources)
|
|
{
|
|
if (!VulnerabilitySources.Any(s => PatternCovers(s, source)))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool PatternCovers(string coveringPattern, string coveredPattern)
|
|
{
|
|
// Universal pattern covers everything
|
|
if (coveringPattern == "*") return true;
|
|
|
|
// Exact match
|
|
if (coveringPattern.Equals(coveredPattern, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
|
|
// Prefix pattern (e.g., "pkg:npm/*" covers "pkg:npm/express")
|
|
if (coveringPattern.EndsWith("/*"))
|
|
{
|
|
var prefix = coveringPattern[..^1];
|
|
if (coveredPattern.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
// Also check if covered pattern is a more specific prefix pattern
|
|
if (coveredPattern.EndsWith("/*"))
|
|
{
|
|
var otherPrefix = coveredPattern[..^1];
|
|
if (otherPrefix.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool MatchesAny(string value, IReadOnlyList<string> patterns)
|
|
{
|
|
foreach (var pattern in patterns)
|
|
{
|
|
if (pattern == "*") return true;
|
|
if (pattern.EndsWith("/*"))
|
|
{
|
|
var prefix = pattern[..^1];
|
|
if (value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
}
|
|
else if (pattern.Equals(value, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Universal scope that covers all subjects.
|
|
/// </summary>
|
|
public static AuthorityScope Universal { get; } = new()
|
|
{
|
|
Artifacts = ["*"],
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// A principal is an issuer identity with verifiable keys.
|
|
/// </summary>
|
|
public sealed record Principal
|
|
{
|
|
/// <summary>
|
|
/// Principal identifier (URI-like, e.g., "did:web:vendor.example").
|
|
/// </summary>
|
|
public required string Id { get; init; }
|
|
|
|
/// <summary>
|
|
/// Principal type for canonical serialization.
|
|
/// </summary>
|
|
public string Type { get; init; } = "identity";
|
|
|
|
/// <summary>
|
|
/// Key identifiers for verification.
|
|
/// </summary>
|
|
public IReadOnlyList<string>? KeyIds { get; init; }
|
|
|
|
/// <summary>
|
|
/// Identity claims (e.g., cert SANs, OIDC subject, org, repo).
|
|
/// </summary>
|
|
public IReadOnlyDictionary<string, string>? IdentityClaims { get; init; }
|
|
|
|
/// <summary>
|
|
/// Roles this principal can play.
|
|
/// </summary>
|
|
public PrincipalRole Roles { get; init; } = PrincipalRole.None;
|
|
|
|
/// <summary>
|
|
/// Display name for human readability.
|
|
/// </summary>
|
|
public string? DisplayName { get; init; }
|
|
|
|
/// <summary>
|
|
/// An unknown principal used as a fallback when no issuer is specified.
|
|
/// </summary>
|
|
public static Principal Unknown { get; } = new Principal
|
|
{
|
|
Id = "urn:stellaops:principal:unknown",
|
|
DisplayName = "Unknown"
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trust label computed from policy and verification.
|
|
/// Affects decision selection without destroying underlying knowledge.
|
|
/// </summary>
|
|
public sealed record TrustLabel : IComparable<TrustLabel>
|
|
{
|
|
/// <summary>
|
|
/// Cryptographic and identity verification strength.
|
|
/// </summary>
|
|
public required AssuranceLevel AssuranceLevel { get; init; }
|
|
|
|
/// <summary>
|
|
/// Scope of subjects this trust applies to.
|
|
/// </summary>
|
|
public required AuthorityScope AuthorityScope { get; init; }
|
|
|
|
/// <summary>
|
|
/// Temporal validity of the assertion.
|
|
/// </summary>
|
|
public required FreshnessClass Freshness { get; init; }
|
|
|
|
/// <summary>
|
|
/// Strength of attached evidence.
|
|
/// </summary>
|
|
public required EvidenceClass EvidenceClass { get; init; }
|
|
|
|
/// <summary>
|
|
/// The principal providing this trust.
|
|
/// </summary>
|
|
public Principal? Principal { get; init; }
|
|
|
|
/// <summary>
|
|
/// Computes an overall trust score for ordering.
|
|
/// Higher is more trustworthy.
|
|
/// </summary>
|
|
public int ComputeScore()
|
|
{
|
|
// Weighted combination (can be policy-configurable)
|
|
return (int)AssuranceLevel * 100
|
|
+ (int)EvidenceClass * 10
|
|
+ (int)Freshness;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compares trust labels by overall score.
|
|
/// </summary>
|
|
public int CompareTo(TrustLabel? other)
|
|
{
|
|
if (other is null) return 1;
|
|
return ComputeScore().CompareTo(other.ComputeScore());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the higher trust label (join operation).
|
|
/// </summary>
|
|
public static TrustLabel Max(TrustLabel a, TrustLabel b)
|
|
=> a.CompareTo(b) >= 0 ? a : b;
|
|
|
|
/// <summary>
|
|
/// Returns the lower trust label (meet operation).
|
|
/// </summary>
|
|
public static TrustLabel Min(TrustLabel a, TrustLabel b)
|
|
=> a.CompareTo(b) <= 0 ? a : b;
|
|
|
|
/// <summary>
|
|
/// Creates a minimal trust label (unsigned, no evidence).
|
|
/// </summary>
|
|
public static TrustLabel Minimal { get; } = new()
|
|
{
|
|
AssuranceLevel = AssuranceLevel.A0_Unsigned,
|
|
AuthorityScope = new AuthorityScope(),
|
|
Freshness = FreshnessClass.Unknown,
|
|
EvidenceClass = EvidenceClass.E0_StatementOnly,
|
|
};
|
|
}
|