feat: Add VEX Status Chip component and integration tests for reachability drift detection
- Introduced `VexStatusChipComponent` to display VEX status with color coding and tooltips. - Implemented integration tests for reachability drift detection, covering various scenarios including drift detection, determinism, and error handling. - Enhanced `ScannerToSignalsReachabilityTests` with a null implementation of `ICallGraphSyncService` for better test isolation. - Updated project references to include the new Reachability Drift library.
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
/**
|
||||
* 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>
|
||||
/// 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>
|
||||
/// 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user