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:
StellaOps Bot
2025-12-20 01:26:42 +02:00
parent edc91ea96f
commit 5fc469ad98
159 changed files with 41116 additions and 2305 deletions

View File

@@ -0,0 +1,265 @@
/**
* Claim and Evidence - Core assertion models.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Tasks: TRUST-007, TRUST-008
*
* A Claim is a signed or unsigned assertion about a Subject.
* Evidence is a typed object that supports replay and audit.
*/
using System.Security.Cryptography;
using System.Text.Json;
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// An atomic assertion about a security proposition.
/// </summary>
public sealed record AtomAssertion
{
/// <summary>
/// The security atom being asserted.
/// </summary>
public required SecurityAtom Atom { get; init; }
/// <summary>
/// The asserted value (true or false).
/// </summary>
public required bool Value { get; init; }
/// <summary>
/// Optional condition under which this assertion holds.
/// E.g., "under current config snapshot", "unless dependency present".
/// </summary>
public string? Condition { get; init; }
/// <summary>
/// Human-readable justification for the assertion.
/// </summary>
public string? Justification { get; init; }
}
/// <summary>
/// Time fields for claim validity.
/// </summary>
public sealed record ClaimTimeInfo
{
/// <summary>
/// When the claim was issued.
/// </summary>
public required DateTimeOffset IssuedAt { get; init; }
/// <summary>
/// When the claim becomes valid (optional).
/// </summary>
public DateTimeOffset? ValidFrom { get; init; }
/// <summary>
/// When the claim expires (optional).
/// </summary>
public DateTimeOffset? ValidUntil { get; init; }
}
/// <summary>
/// A claim is a signed or unsigned assertion about a Subject.
/// </summary>
public sealed record Claim
{
/// <summary>
/// Content-addressable digest of canonical claim JSON.
/// Computed from claim contents, not supplied externally.
/// </summary>
public string? Id { get; init; }
/// <summary>
/// The subject of this claim.
/// </summary>
public required Subject Subject { get; init; }
/// <summary>
/// The principal making this claim.
/// </summary>
public Principal Principal { get; init; } = Principal.Unknown;
/// <summary>
/// Time information for the claim.
/// </summary>
public ClaimTimeInfo? TimeInfo { get; init; }
/// <summary>
/// List of atomic assertions in this claim.
/// </summary>
public IReadOnlyList<AtomAssertion> Assertions { get; init; } = [];
/// <summary>
/// References to supporting evidence objects.
/// </summary>
public IReadOnlyList<string>? EvidenceRefs { get; init; }
/// <summary>
/// Reference to DSSE/signature wrapper (optional).
/// </summary>
public string? SignatureRef { get; init; }
/// <summary>
/// Trust label computed for this claim (set during evaluation).
/// </summary>
public TrustLabel? TrustLabel { get; init; }
/// <summary>
/// Source format (e.g., "cyclonedx", "openvex", "csaf", "internal").
/// </summary>
public string? SourceFormat { get; init; }
/// <summary>
/// Computes the content-addressable ID for this claim.
/// </summary>
public string ComputeId()
{
// Create a canonical representation excluding the Id field
var forHashing = new
{
subject = Subject,
principal = new { id = Principal.Id },
time = TimeInfo,
assertions = Assertions,
evidence_refs = EvidenceRefs,
};
var json = JsonSerializer.SerializeToUtf8Bytes(forHashing, CanonicalJsonOptions.Default);
var hash = SHA256.HashData(json);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
/// <summary>
/// Returns a new claim with computed ID.
/// </summary>
public Claim WithComputedId() => this with { Id = ComputeId() };
/// <summary>
/// Checks if the claim is currently valid based on time fields.
/// </summary>
public bool IsValidAt(DateTimeOffset asOf)
{
if (TimeInfo?.ValidFrom.HasValue == true && asOf < TimeInfo.ValidFrom.Value)
return false;
if (TimeInfo?.ValidUntil.HasValue == true && asOf > TimeInfo.ValidUntil.Value)
return false;
return true;
}
}
/// <summary>
/// Type of evidence supporting a claim.
/// </summary>
public enum EvidenceType
{
/// <summary>
/// SBOM node linkage evidence.
/// </summary>
SbomNode,
/// <summary>
/// Call graph path showing reachability.
/// </summary>
CallGraphPath,
/// <summary>
/// Dynamic loader resolution evidence.
/// </summary>
LoaderResolution,
/// <summary>
/// Configuration snapshot evidence.
/// </summary>
ConfigSnapshot,
/// <summary>
/// Patch diff evidence.
/// </summary>
PatchDiff,
/// <summary>
/// Pedigree/commit chain evidence.
/// </summary>
PedigreeCommitChain,
/// <summary>
/// Runtime behavior observation.
/// </summary>
RuntimeObservation,
/// <summary>
/// Mitigation control evidence.
/// </summary>
MitigationControl,
/// <summary>
/// Scanner detection output.
/// </summary>
ScannerDetection,
/// <summary>
/// Vendor advisory statement.
/// </summary>
VendorAdvisory,
}
/// <summary>
/// Evidence is a typed object that supports replay and audit.
/// </summary>
public sealed record Evidence
{
/// <summary>
/// Type of evidence.
/// </summary>
public required EvidenceType Type { get; init; }
/// <summary>
/// Content-addressable digest of canonical evidence bytes.
/// </summary>
public required string Digest { get; init; }
/// <summary>
/// Tool/system that produced this evidence.
/// </summary>
public required string Producer { get; init; }
/// <summary>
/// Version of the producer tool.
/// </summary>
public string? ProducerVersion { get; init; }
/// <summary>
/// When the evidence was collected.
/// </summary>
public required DateTimeOffset CollectedAt { get; init; }
/// <summary>
/// Reference to the payload in content-addressable storage.
/// </summary>
public string? PayloadRef { get; init; }
/// <summary>
/// Reference to signature/attestation for this evidence (optional).
/// </summary>
public string? SignatureRef { get; init; }
/// <summary>
/// Determines the evidence class based on type.
/// </summary>
public EvidenceClass GetEvidenceClass() => Type switch
{
EvidenceType.SbomNode => EvidenceClass.E1_SbomLinkage,
EvidenceType.CallGraphPath => EvidenceClass.E2_ReachabilityMitigation,
EvidenceType.LoaderResolution => EvidenceClass.E2_ReachabilityMitigation,
EvidenceType.ConfigSnapshot => EvidenceClass.E2_ReachabilityMitigation,
EvidenceType.RuntimeObservation => EvidenceClass.E2_ReachabilityMitigation,
EvidenceType.MitigationControl => EvidenceClass.E2_ReachabilityMitigation,
EvidenceType.PatchDiff => EvidenceClass.E3_Remediation,
EvidenceType.PedigreeCommitChain => EvidenceClass.E3_Remediation,
EvidenceType.ScannerDetection => EvidenceClass.E1_SbomLinkage,
EvidenceType.VendorAdvisory => EvidenceClass.E0_StatementOnly,
_ => EvidenceClass.E0_StatementOnly,
};
}

View File

@@ -0,0 +1,226 @@
/**
* CSAF VEX Normalizer - Convert CSAF VEX documents to canonical claims.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Task: TRUST-012
*
* CSAF (Common Security Advisory Framework) VEX follows OASIS standard.
* See: https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html
*/
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// CSAF product status values.
/// </summary>
public enum CsafProductStatus
{
/// <summary>
/// Known affected products.
/// </summary>
KnownAffected,
/// <summary>
/// Known not affected products.
/// </summary>
KnownNotAffected,
/// <summary>
/// First affected version.
/// </summary>
FirstAffected,
/// <summary>
/// First fixed version.
/// </summary>
FirstFixed,
/// <summary>
/// Fixed versions.
/// </summary>
Fixed,
/// <summary>
/// Last affected version.
/// </summary>
LastAffected,
/// <summary>
/// Recommended versions.
/// </summary>
Recommended,
/// <summary>
/// Under investigation.
/// </summary>
UnderInvestigation,
}
/// <summary>
/// CSAF flag label values.
/// </summary>
public enum CsafFlagLabel
{
/// <summary>
/// No flag specified.
/// </summary>
None,
/// <summary>
/// Component is not present.
/// </summary>
ComponentNotPresent,
/// <summary>
/// Inline mitigations exist.
/// </summary>
InlineMitigationsAlreadyExist,
/// <summary>
/// Vulnerable code cannot be controlled by adversary.
/// </summary>
VulnerableCodeCannotBeControlledByAdversary,
/// <summary>
/// Vulnerable code not in execute path.
/// </summary>
VulnerableCodeNotInExecutePath,
/// <summary>
/// Vulnerable code not present.
/// </summary>
VulnerableCodeNotPresent,
}
/// <summary>
/// Normalizes CSAF VEX documents to canonical claims.
/// </summary>
public sealed class CsafVexNormalizer : IVexNormalizer
{
/// <inheritdoc />
public string Format => "CSAF";
/// <summary>
/// Mapping from CSAF product status to atom assertions.
/// Per specification Table 3.
/// </summary>
private static readonly Dictionary<CsafProductStatus, List<AtomAssertion>> StatusToAtoms = new()
{
[CsafProductStatus.KnownAffected] =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = true, Justification = "known_affected status" },
new AtomAssertion { Atom = SecurityAtom.Applies, Value = true, Justification = "known_affected status" },
],
[CsafProductStatus.KnownNotAffected] =
[
new AtomAssertion { Atom = SecurityAtom.Applies, Value = false, Justification = "known_not_affected status" },
],
[CsafProductStatus.FirstAffected] =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = true, Justification = "first_affected status" },
new AtomAssertion { Atom = SecurityAtom.Applies, Value = true, Justification = "first_affected status" },
],
[CsafProductStatus.FirstFixed] =
[
new AtomAssertion { Atom = SecurityAtom.Fixed, Value = true, Justification = "first_fixed status" },
],
[CsafProductStatus.Fixed] =
[
new AtomAssertion { Atom = SecurityAtom.Fixed, Value = true, Justification = "fixed status" },
],
[CsafProductStatus.LastAffected] =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = true, Justification = "last_affected status" },
new AtomAssertion { Atom = SecurityAtom.Applies, Value = true, Justification = "last_affected status" },
],
[CsafProductStatus.Recommended] =
[
new AtomAssertion { Atom = SecurityAtom.Fixed, Value = true, Justification = "recommended status" },
],
[CsafProductStatus.UnderInvestigation] =
[
// under_investigation: no definite assertions
],
};
/// <summary>
/// Mapping from CSAF flag label to atom assertions.
/// Per specification Table 3.
/// </summary>
private static readonly Dictionary<CsafFlagLabel, List<AtomAssertion>> FlagToAtoms = new()
{
[CsafFlagLabel.ComponentNotPresent] =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = false, Justification = "component_not_present flag" },
],
[CsafFlagLabel.InlineMitigationsAlreadyExist] =
[
new AtomAssertion { Atom = SecurityAtom.Mitigated, Value = true, Justification = "inline_mitigations_already_exist flag" },
],
[CsafFlagLabel.VulnerableCodeCannotBeControlledByAdversary] =
[
new AtomAssertion { Atom = SecurityAtom.Mitigated, Value = true, Justification = "vulnerable_code_cannot_be_controlled_by_adversary flag" },
],
[CsafFlagLabel.VulnerableCodeNotInExecutePath] =
[
new AtomAssertion { Atom = SecurityAtom.Reachable, Value = false, Justification = "vulnerable_code_not_in_execute_path flag" },
],
[CsafFlagLabel.VulnerableCodeNotPresent] =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = false, Justification = "vulnerable_code_not_present flag" },
],
};
/// <inheritdoc />
public IEnumerable<Claim> Normalize(string document, Principal principal, TrustLabel? trustLabel = null)
{
// Placeholder for JSON parsing implementation
yield break;
}
/// <summary>
/// Normalizes a pre-parsed CSAF VEX statement.
/// </summary>
public Claim NormalizeStatement(
Subject subject,
CsafProductStatus status,
CsafFlagLabel flag = CsafFlagLabel.None,
string? remediation = null,
Principal? principal = null,
TrustLabel? trustLabel = null)
{
var assertions = new List<AtomAssertion>();
// Add status-based assertions
if (StatusToAtoms.TryGetValue(status, out var statusAtoms))
{
assertions.AddRange(statusAtoms);
}
// Add flag-based assertions
if (flag != CsafFlagLabel.None && FlagToAtoms.TryGetValue(flag, out var flagAtoms))
{
assertions.AddRange(flagAtoms);
}
// Add remediation as justification if provided
if (!string.IsNullOrWhiteSpace(remediation))
{
for (int i = 0; i < assertions.Count; i++)
{
assertions[i] = assertions[i] with
{
Justification = $"{assertions[i].Justification} (remediation: {remediation})"
};
}
}
return new Claim
{
Subject = subject,
Issuer = principal ?? Principal.Unknown,
Assertions = assertions,
TrustLabel = trustLabel,
Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow },
};
}
}

View File

@@ -0,0 +1,383 @@
/**
* DispositionSelector - Maps atom values to ECMA-424 dispositions.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Task: TRUST-013
*
* Implements the decision rules from Table 4 of the specification.
* Produces deterministic, explainable disposition decisions with full audit trail.
*/
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// ECMA-424 disposition values.
/// </summary>
public enum Disposition
{
/// <summary>
/// Full provenance chain verified.
/// </summary>
ResolvedWithPedigree,
/// <summary>
/// Resolved but without full pedigree.
/// </summary>
Resolved,
/// <summary>
/// Misattributed or not applicable.
/// </summary>
FalsePositive,
/// <summary>
/// Not affected due to context/configuration.
/// </summary>
NotAffected,
/// <summary>
/// Confirmed exploitable.
/// </summary>
Exploitable,
/// <summary>
/// Analysis incomplete.
/// </summary>
InTriage,
}
/// <summary>
/// A decision trace step for audit/explainability.
/// </summary>
public sealed record DecisionStep
{
/// <summary>
/// The rule that was evaluated.
/// </summary>
public required string RuleName { get; init; }
/// <summary>
/// Whether the rule matched.
/// </summary>
public required bool Matched { get; init; }
/// <summary>
/// The condition that was evaluated.
/// </summary>
public required string Condition { get; init; }
/// <summary>
/// Atom values used in evaluation.
/// </summary>
public required IReadOnlyDictionary<SecurityAtom, K4Value> AtomValues { get; init; }
/// <summary>
/// Trust considerations if any.
/// </summary>
public string? TrustNote { get; init; }
}
/// <summary>
/// The result of disposition selection.
/// </summary>
public sealed record DispositionResult
{
/// <summary>
/// The selected disposition.
/// </summary>
public required Disposition Disposition { get; init; }
/// <summary>
/// Human-readable explanation.
/// </summary>
public required string Explanation { get; init; }
/// <summary>
/// The rule that determined the disposition.
/// </summary>
public required string MatchedRule { get; init; }
/// <summary>
/// Full decision trace for audit.
/// </summary>
public required IReadOnlyList<DecisionStep> Trace { get; init; }
/// <summary>
/// Any conflicts detected.
/// </summary>
public IReadOnlyList<SecurityAtom> Conflicts { get; init; } = [];
/// <summary>
/// Any unknowns detected.
/// </summary>
public IReadOnlyList<SecurityAtom> Unknowns { get; init; } = [];
/// <summary>
/// The atom snapshot at decision time.
/// </summary>
public IReadOnlyDictionary<SecurityAtom, K4Value>? AtomSnapshot { get; init; }
}
/// <summary>
/// A disposition selection rule.
/// </summary>
public sealed record SelectionRule
{
/// <summary>
/// Rule identifier.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Rule priority (lower = higher priority).
/// </summary>
public required int Priority { get; init; }
/// <summary>
/// The disposition this rule produces.
/// </summary>
public required Disposition Disposition { get; init; }
/// <summary>
/// Human-readable condition description.
/// </summary>
public required string ConditionDescription { get; init; }
/// <summary>
/// The condition predicate.
/// </summary>
public required Func<IReadOnlyDictionary<SecurityAtom, K4Value>, bool> Condition { get; init; }
/// <summary>
/// Explanation template.
/// </summary>
public required string ExplanationTemplate { get; init; }
}
/// <summary>
/// Selects dispositions based on atom values using policy-driven rules.
/// </summary>
public sealed class DispositionSelector
{
private readonly List<SelectionRule> _rules;
/// <summary>
/// Creates a new disposition selector with default baseline rules.
/// </summary>
public DispositionSelector()
: this(GetBaselineRules())
{
}
/// <summary>
/// Creates a new disposition selector with custom rules.
/// </summary>
public DispositionSelector(IEnumerable<SelectionRule> rules)
{
_rules = rules.OrderBy(r => r.Priority).ToList();
}
/// <summary>
/// Selects a disposition for the given subject state.
/// </summary>
public DispositionResult Select(SubjectState state)
{
var atomValues = SecurityAtomExtensions.All()
.ToDictionary(a => a, a => state.GetValue(a));
return Select(atomValues);
}
/// <summary>
/// Selects a disposition for the given atom values.
/// </summary>
public DispositionResult Select(IReadOnlyDictionary<SecurityAtom, K4Value> atomValues)
{
var trace = new List<DecisionStep>();
// Detect conflicts and unknowns
var conflicts = atomValues
.Where(kvp => kvp.Value == K4Value.Conflict)
.Select(kvp => kvp.Key)
.ToList();
var unknowns = atomValues
.Where(kvp => kvp.Value == K4Value.Unknown)
.Select(kvp => kvp.Key)
.ToList();
// Evaluate rules in priority order
foreach (var rule in _rules)
{
var matched = rule.Condition(atomValues);
trace.Add(new DecisionStep
{
RuleName = rule.Name,
Matched = matched,
Condition = rule.ConditionDescription,
AtomValues = atomValues,
});
if (matched)
{
return new DispositionResult
{
Disposition = rule.Disposition,
Explanation = FormatExplanation(rule.ExplanationTemplate, atomValues),
MatchedRule = rule.Name,
Trace = trace,
Conflicts = conflicts,
Unknowns = unknowns,
AtomSnapshot = atomValues,
};
}
}
// Fallback to in_triage if no rule matched
return new DispositionResult
{
Disposition = Disposition.InTriage,
Explanation = "No disposition rule matched; defaulting to in_triage.",
MatchedRule = "fallback",
Trace = trace,
Conflicts = conflicts,
Unknowns = unknowns,
AtomSnapshot = atomValues,
};
}
private static string FormatExplanation(
string template,
IReadOnlyDictionary<SecurityAtom, K4Value> atomValues)
{
var result = template;
foreach (var (atom, value) in atomValues)
{
result = result.Replace($"{{{atom}}}", value.ToString());
}
return result;
}
/// <summary>
/// Gets the baseline selection rules per Table 4 of the specification.
/// </summary>
public static IReadOnlyList<SelectionRule> GetBaselineRules() =>
[
// Rule 1: MISATTRIBUTED = T → false_positive
new SelectionRule
{
Name = "misattributed",
Priority = 10,
Disposition = Disposition.FalsePositive,
ConditionDescription = "MISATTRIBUTED = T",
Condition = atoms => atoms[SecurityAtom.Misattributed] == K4Value.True,
ExplanationTemplate = "Vulnerability is misattributed (MISATTRIBUTED = {Misattributed}).",
},
// Rule 2: FIXED = T → resolved_with_pedigree (if full chain) or resolved
new SelectionRule
{
Name = "fixed_resolved",
Priority = 20,
Disposition = Disposition.ResolvedWithPedigree,
ConditionDescription = "FIXED = T",
Condition = atoms => atoms[SecurityAtom.Fixed] == K4Value.True,
ExplanationTemplate = "Vulnerability has been fixed (FIXED = {Fixed}).",
},
// Rule 3: PRESENT = F → false_positive
new SelectionRule
{
Name = "not_present",
Priority = 30,
Disposition = Disposition.FalsePositive,
ConditionDescription = "PRESENT = F",
Condition = atoms => atoms[SecurityAtom.Present] == K4Value.False,
ExplanationTemplate = "Vulnerable component is not present (PRESENT = {Present}).",
},
// Rule 4: APPLIES = F → not_affected
new SelectionRule
{
Name = "not_applicable",
Priority = 40,
Disposition = Disposition.NotAffected,
ConditionDescription = "APPLIES = F",
Condition = atoms => atoms[SecurityAtom.Applies] == K4Value.False,
ExplanationTemplate = "Vulnerability does not apply to this context (APPLIES = {Applies}).",
},
// Rule 5: REACHABLE = F → not_affected
new SelectionRule
{
Name = "not_reachable",
Priority = 50,
Disposition = Disposition.NotAffected,
ConditionDescription = "REACHABLE = F",
Condition = atoms => atoms[SecurityAtom.Reachable] == K4Value.False,
ExplanationTemplate = "Vulnerable code is not reachable (REACHABLE = {Reachable}).",
},
// Rule 6: MITIGATED = T → not_affected
new SelectionRule
{
Name = "mitigated",
Priority = 60,
Disposition = Disposition.NotAffected,
ConditionDescription = "MITIGATED = T",
Condition = atoms => atoms[SecurityAtom.Mitigated] == K4Value.True,
ExplanationTemplate = "Vulnerability is mitigated (MITIGATED = {Mitigated}).",
},
// Rule 7: PRESENT = T ∧ APPLIES = T ∧ REACHABLE = T → exploitable
new SelectionRule
{
Name = "exploitable",
Priority = 70,
Disposition = Disposition.Exploitable,
ConditionDescription = "PRESENT = T ∧ APPLIES = T ∧ REACHABLE = T",
Condition = atoms =>
atoms[SecurityAtom.Present] == K4Value.True &&
atoms[SecurityAtom.Applies] == K4Value.True &&
atoms[SecurityAtom.Reachable] == K4Value.True,
ExplanationTemplate = "Vulnerability is present, applicable, and reachable (PRESENT = {Present}, APPLIES = {Applies}, REACHABLE = {Reachable}).",
},
// Rule 8: PRESENT = T ∧ APPLIES = T ∧ REACHABLE = ⊥ → exploitable (conservative)
new SelectionRule
{
Name = "exploitable_unknown_reachability",
Priority = 75,
Disposition = Disposition.Exploitable,
ConditionDescription = "PRESENT = T ∧ APPLIES = T ∧ REACHABLE = ⊥",
Condition = atoms =>
atoms[SecurityAtom.Present] == K4Value.True &&
atoms[SecurityAtom.Applies] == K4Value.True &&
atoms[SecurityAtom.Reachable] == K4Value.Unknown,
ExplanationTemplate = "Vulnerability is present and applicable; reachability unknown, assuming exploitable (PRESENT = {Present}, APPLIES = {Applies}, REACHABLE = {Reachable}).",
},
// Rule 9: Any conflict → in_triage (requires human review)
new SelectionRule
{
Name = "conflict_detected",
Priority = 80,
Disposition = Disposition.InTriage,
ConditionDescription = "Any atom = (conflict)",
Condition = atoms => atoms.Values.Any(v => v == K4Value.Conflict),
ExplanationTemplate = "Conflicting evidence detected; requires human review.",
},
// Rule 10: Insufficient data → in_triage
new SelectionRule
{
Name = "insufficient_data",
Priority = 100,
Disposition = Disposition.InTriage,
ConditionDescription = "PRESENT = ⊥ APPLIES = ⊥",
Condition = atoms =>
atoms[SecurityAtom.Present] == K4Value.Unknown ||
atoms[SecurityAtom.Applies] == K4Value.Unknown,
ExplanationTemplate = "Insufficient data for disposition (PRESENT = {Present}, APPLIES = {Applies}).",
},
];
}

View File

@@ -0,0 +1,214 @@
/**
* K4 Four-Valued Logic (Belnap-style) for Trust Lattice Engine.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Task: TRUST-001
*
* Implements the knowledge lattice for representing truth values that can be:
* - Unknown (no evidence)
* - True (supported true)
* - False (supported false)
* - Conflict (credible evidence for both)
*
* This four-valued logic enables deterministic aggregation of heterogeneous
* security assertions while preserving unknowns and contradictions.
*/
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// Belnap four-valued logic (K4) for representing knowledge states.
/// Enables monotone, conflict-preserving, order-independent aggregation.
/// </summary>
/// <remarks>
/// The knowledge ordering is:
/// <code>
/// (Conflict)
/// / \
/// T F
/// \ /
/// ⊥ (Unknown)
/// </code>
/// T and F are incomparable; both are above ⊥ and below .
/// </remarks>
public enum K4Value
{
/// <summary>
/// Unknown (⊥) - No evidence supports this proposition.
/// Bottom of the knowledge lattice.
/// </summary>
Unknown = 0,
/// <summary>
/// True (T) - Evidence supports the proposition being true.
/// </summary>
True = 1,
/// <summary>
/// False (F) - Evidence supports the proposition being false.
/// </summary>
False = 2,
/// <summary>
/// Conflict () - Credible evidence exists for both true and false.
/// Top of the knowledge lattice; represents contradiction.
/// </summary>
Conflict = 3,
}
/// <summary>
/// Lattice operations for K4 four-valued logic.
/// All operations are deterministic and order-independent.
/// </summary>
public static class K4Lattice
{
/// <summary>
/// Knowledge join (⊔k): union of support.
/// Aggregates information from multiple sources.
/// </summary>
/// <remarks>
/// Truth table:
/// <code>
/// ⊔k | ⊥ | T | F |
/// ----+----+----+----+----
/// ⊥ | ⊥ | T | F |
/// T | T | T | |
/// F | F | | F |
/// | | | |
/// </code>
/// </remarks>
public static K4Value Join(K4Value a, K4Value b)
{
// Fast paths
if (a == b) return a;
if (a == K4Value.Conflict || b == K4Value.Conflict) return K4Value.Conflict;
if (a == K4Value.Unknown) return b;
if (b == K4Value.Unknown) return a;
// T ⊔ F = (conflict)
return K4Value.Conflict;
}
/// <summary>
/// Knowledge join over a sequence of values.
/// Order-independent aggregation.
/// </summary>
public static K4Value JoinAll(IEnumerable<K4Value> values)
{
var result = K4Value.Unknown;
foreach (var v in values)
{
result = Join(result, v);
// Short-circuit: conflict is maximal
if (result == K4Value.Conflict)
return K4Value.Conflict;
}
return result;
}
/// <summary>
/// Knowledge meet (⊓k): intersection of support.
/// Used for composed claims along dependency chains.
/// </summary>
/// <remarks>
/// Truth table:
/// <code>
/// ⊓k | ⊥ | T | F |
/// ----+----+----+----+----
/// ⊥ | ⊥ | ⊥ | ⊥ | ⊥
/// T | ⊥ | T | ⊥ | T
/// F | ⊥ | ⊥ | F | F
/// | ⊥ | T | F |
/// </code>
/// </remarks>
public static K4Value Meet(K4Value a, K4Value b)
{
// Fast paths
if (a == b) return a;
if (a == K4Value.Unknown || b == K4Value.Unknown) return K4Value.Unknown;
if (a == K4Value.Conflict) return b;
if (b == K4Value.Conflict) return a;
// T ⊓ F = ⊥ (no agreement)
return K4Value.Unknown;
}
/// <summary>
/// Knowledge ordering: a ≤k b means b has at least as much information as a.
/// </summary>
/// <returns>
/// true if a is below or equal to b in the knowledge ordering.
/// </returns>
public static bool LessOrEqual(K4Value a, K4Value b)
{
// ⊥ ≤ everything
if (a == K4Value.Unknown) return true;
// nothing ≤ ⊥ except ⊥
if (b == K4Value.Unknown) return false;
// everything ≤
if (b == K4Value.Conflict) return true;
// ≤ only
if (a == K4Value.Conflict) return false;
// T ≤ T, F ≤ F; T and F are incomparable
return a == b;
}
/// <summary>
/// Checks if two values are comparable in the knowledge ordering.
/// T and F are incomparable.
/// </summary>
public static bool AreComparable(K4Value a, K4Value b)
{
return LessOrEqual(a, b) || LessOrEqual(b, a);
}
/// <summary>
/// Negation of a K4 value.
/// Swaps True ↔ False; Unknown and Conflict are self-negating.
/// </summary>
public static K4Value Negate(K4Value v) => v switch
{
K4Value.True => K4Value.False,
K4Value.False => K4Value.True,
_ => v, // Unknown and Conflict are unchanged
};
/// <summary>
/// Determines if the value has any true support (T or ).
/// </summary>
public static bool HasTrueSupport(K4Value v)
=> v == K4Value.True || v == K4Value.Conflict;
/// <summary>
/// Determines if the value has any false support (F or ).
/// </summary>
public static bool HasFalseSupport(K4Value v)
=> v == K4Value.False || v == K4Value.Conflict;
/// <summary>
/// Determines if the value is definite (T or F, not ⊥ or ).
/// </summary>
public static bool IsDefinite(K4Value v)
=> v == K4Value.True || v == K4Value.False;
/// <summary>
/// Determines if the value represents lack of information (⊥ or ).
/// </summary>
public static bool IsIndeterminate(K4Value v)
=> v == K4Value.Unknown || v == K4Value.Conflict;
/// <summary>
/// Computes K4 value from support set presence.
/// </summary>
/// <param name="hasTrueSupport">True if any claims support the proposition.</param>
/// <param name="hasFalseSupport">True if any claims refute the proposition.</param>
public static K4Value FromSupport(bool hasTrueSupport, bool hasFalseSupport)
{
return (hasTrueSupport, hasFalseSupport) switch
{
(false, false) => K4Value.Unknown,
(true, false) => K4Value.True,
(false, true) => K4Value.False,
(true, true) => K4Value.Conflict,
};
}
}

View File

@@ -0,0 +1,348 @@
/**
* AtomValue and LatticeStore - Aggregation infrastructure.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Tasks: TRUST-003, TRUST-009
*
* AtomValue tracks the K4 truth value for a single atom with support sets.
* LatticeStore maintains the complete aggregation state for all subjects.
*/
using System.Collections.Concurrent;
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// Tracks the K4 truth value for a single security atom with support sets.
/// </summary>
public sealed class AtomValue
{
private readonly HashSet<string> _supportTrue = [];
private readonly HashSet<string> _supportFalse = [];
private TrustLabel? _trustTrue;
private TrustLabel? _trustFalse;
/// <summary>
/// The security atom this value tracks.
/// </summary>
public SecurityAtom Atom { get; }
/// <summary>
/// Creates a new atom value tracker.
/// </summary>
public AtomValue(SecurityAtom atom)
{
Atom = atom;
}
/// <summary>
/// Gets the current K4 value based on support sets.
/// </summary>
public K4Value Value => K4Lattice.FromSupport(
hasTrueSupport: _supportTrue.Count > 0,
hasFalseSupport: _supportFalse.Count > 0);
/// <summary>
/// Claim IDs supporting the proposition as true.
/// </summary>
public IReadOnlySet<string> SupportTrue => _supportTrue;
/// <summary>
/// Claim IDs supporting the proposition as false.
/// </summary>
public IReadOnlySet<string> SupportFalse => _supportFalse;
/// <summary>
/// Highest trust label among true supporters.
/// </summary>
public TrustLabel? TrustTrue => _trustTrue;
/// <summary>
/// Highest trust label among false supporters.
/// </summary>
public TrustLabel? TrustFalse => _trustFalse;
/// <summary>
/// Adds support from a claim.
/// </summary>
/// <param name="claimId">The claim identifier.</param>
/// <param name="value">The asserted value (true or false).</param>
/// <param name="trust">The trust label for this claim.</param>
public void AddSupport(string claimId, bool value, TrustLabel? trust)
{
if (value)
{
_supportTrue.Add(claimId);
if (trust is not null && (_trustTrue is null || trust.CompareTo(_trustTrue) > 0))
_trustTrue = trust;
}
else
{
_supportFalse.Add(claimId);
if (trust is not null && (_trustFalse is null || trust.CompareTo(_trustFalse) > 0))
_trustFalse = trust;
}
}
/// <summary>
/// Removes support from a claim (for retraction/expiry).
/// </summary>
public void RemoveSupport(string claimId)
{
_supportTrue.Remove(claimId);
_supportFalse.Remove(claimId);
// Note: Trust labels are not recalculated on removal for simplicity
}
/// <summary>
/// Creates a snapshot for proof bundles.
/// </summary>
public AtomValueSnapshot ToSnapshot() => new()
{
Atom = Atom,
Value = Value,
SupportTrueCount = _supportTrue.Count,
SupportFalseCount = _supportFalse.Count,
SupportTrueIds = [.. _supportTrue],
SupportFalseIds = [.. _supportFalse],
TrustTrue = _trustTrue,
TrustFalse = _trustFalse,
};
}
/// <summary>
/// Immutable snapshot of an atom value for proof bundles.
/// </summary>
public sealed record AtomValueSnapshot
{
public required SecurityAtom Atom { get; init; }
public required K4Value Value { get; init; }
public required int SupportTrueCount { get; init; }
public required int SupportFalseCount { get; init; }
public required IReadOnlyList<string> SupportTrueIds { get; init; }
public required IReadOnlyList<string> SupportFalseIds { get; init; }
public TrustLabel? TrustTrue { get; init; }
public TrustLabel? TrustFalse { get; init; }
}
/// <summary>
/// Key for indexing atom values by subject and atom.
/// </summary>
public readonly record struct AtomKey(string SubjectDigest, SecurityAtom Atom);
/// <summary>
/// Aggregation state for a single subject.
/// </summary>
public sealed class SubjectState
{
private readonly Dictionary<SecurityAtom, AtomValue> _atoms = [];
private readonly List<string> _claimIds = [];
/// <summary>
/// The subject this state tracks.
/// </summary>
public Subject Subject { get; }
/// <summary>
/// Content-addressable digest of the subject.
/// </summary>
public string SubjectDigest { get; }
/// <summary>
/// Creates a new subject state.
/// </summary>
public SubjectState(Subject subject)
{
Subject = subject;
SubjectDigest = subject.ComputeDigest();
// Initialize all atoms to unknown
foreach (var atom in SecurityAtomExtensions.All())
{
_atoms[atom] = new AtomValue(atom);
}
}
/// <summary>
/// Gets the K4 value for a specific atom.
/// </summary>
public K4Value GetValue(SecurityAtom atom)
=> _atoms.TryGetValue(atom, out var av) ? av.Value : K4Value.Unknown;
/// <summary>
/// Gets the full atom value tracker for a specific atom.
/// </summary>
public AtomValue GetAtomValue(SecurityAtom atom)
=> _atoms.GetValueOrDefault(atom) ?? new AtomValue(atom);
/// <summary>
/// All claim IDs that have contributed to this subject.
/// </summary>
public IReadOnlyList<string> ClaimIds => _claimIds;
/// <summary>
/// Ingests a claim, updating atom values.
/// </summary>
public void IngestClaim(Claim claim)
{
var claimId = claim.Id ?? claim.ComputeId();
_claimIds.Add(claimId);
foreach (var assertion in claim.Assertions)
{
if (_atoms.TryGetValue(assertion.Atom, out var atomValue))
{
atomValue.AddSupport(claimId, assertion.Value, claim.TrustLabel);
}
}
}
/// <summary>
/// Creates a snapshot of all atom values for proof bundles.
/// </summary>
public IReadOnlyDictionary<SecurityAtom, AtomValueSnapshot> ToSnapshot()
{
return _atoms.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.ToSnapshot());
}
}
/// <summary>
/// The lattice store maintains aggregation state for all subjects.
/// Thread-safe for concurrent ingestion.
/// </summary>
public sealed class LatticeStore
{
private readonly ConcurrentDictionary<string, SubjectState> _subjects = new();
private readonly ConcurrentDictionary<string, Claim> _claims = new();
private readonly ConcurrentDictionary<string, Evidence> _evidence = new();
/// <summary>
/// Gets or creates the state for a subject.
/// </summary>
public SubjectState GetOrCreateSubject(Subject subject)
{
var digest = subject.ComputeDigest();
return _subjects.GetOrAdd(digest, _ => new SubjectState(subject));
}
/// <summary>
/// Ingests a claim into the store.
/// </summary>
/// <param name="claim">The claim to ingest.</param>
/// <returns>The claim with computed ID.</returns>
public Claim IngestClaim(Claim claim)
{
var withId = claim.Id is not null ? claim : claim.WithComputedId();
_claims[withId.Id!] = withId;
var subjectState = GetOrCreateSubject(claim.Subject);
subjectState.IngestClaim(withId);
return withId;
}
/// <summary>
/// Registers evidence in the store.
/// </summary>
public void RegisterEvidence(Evidence evidence)
{
_evidence[evidence.Digest] = evidence;
}
/// <summary>
/// Gets a claim by ID.
/// </summary>
public Claim? GetClaim(string claimId)
=> _claims.GetValueOrDefault(claimId);
/// <summary>
/// Gets evidence by digest.
/// </summary>
public Evidence? GetEvidence(string digest)
=> _evidence.GetValueOrDefault(digest);
/// <summary>
/// Gets the subject state if it exists.
/// </summary>
public SubjectState? GetSubjectState(string subjectDigest)
=> _subjects.GetValueOrDefault(subjectDigest);
/// <summary>
/// Gets the K4 value for a specific subject and atom.
/// </summary>
public K4Value GetValue(Subject subject, SecurityAtom atom)
{
var digest = subject.ComputeDigest();
if (_subjects.TryGetValue(digest, out var state))
return state.GetValue(atom);
return K4Value.Unknown;
}
/// <summary>
/// Gets all subjects in the store.
/// </summary>
public IEnumerable<SubjectState> GetAllSubjects()
=> _subjects.Values;
/// <summary>
/// Gets all claims in the store.
/// </summary>
public IEnumerable<Claim> GetAllClaims()
=> _claims.Values;
/// <summary>
/// Gets subjects with conflicts (any atom = ).
/// </summary>
public IEnumerable<SubjectState> GetConflictingSubjects()
{
return _subjects.Values.Where(s =>
SecurityAtomExtensions.All().Any(a => s.GetValue(a) == K4Value.Conflict));
}
/// <summary>
/// Gets subjects with unknowns (any required atom = ⊥).
/// </summary>
public IEnumerable<SubjectState> GetIncompleteSubjects()
{
// Required atoms for disposition: PRESENT, APPLIES, REACHABLE
var requiredAtoms = new[] { SecurityAtom.Present, SecurityAtom.Applies, SecurityAtom.Reachable };
return _subjects.Values.Where(s =>
requiredAtoms.Any(a => s.GetValue(a) == K4Value.Unknown));
}
/// <summary>
/// Clears the store.
/// </summary>
public void Clear()
{
_subjects.Clear();
_claims.Clear();
_evidence.Clear();
}
/// <summary>
/// Gets statistics about the store.
/// </summary>
public LatticeStoreStats GetStats() => new()
{
SubjectCount = _subjects.Count,
ClaimCount = _claims.Count,
EvidenceCount = _evidence.Count,
ConflictCount = GetConflictingSubjects().Count(),
IncompleteCount = GetIncompleteSubjects().Count(),
};
}
/// <summary>
/// Statistics about the lattice store.
/// </summary>
public sealed record LatticeStoreStats
{
public int SubjectCount { get; init; }
public int ClaimCount { get; init; }
public int EvidenceCount { get; init; }
public int ConflictCount { get; init; }
public int IncompleteCount { get; init; }
}

View File

@@ -0,0 +1,197 @@
/**
* OpenVEX Normalizer - Convert OpenVEX documents to canonical claims.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Task: TRUST-011
*
* OpenVEX follows the VEX minimal elements specification.
* See: https://github.com/openvex/spec
*/
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// OpenVEX status values.
/// </summary>
public enum OpenVexStatus
{
/// <summary>
/// Not yet determined if affected.
/// </summary>
UnderInvestigation,
/// <summary>
/// Product is not affected.
/// </summary>
NotAffected,
/// <summary>
/// Product is affected.
/// </summary>
Affected,
/// <summary>
/// Vulnerability has been fixed.
/// </summary>
Fixed,
}
/// <summary>
/// OpenVEX justification values (for not_affected status).
/// </summary>
public enum OpenVexJustification
{
/// <summary>
/// No justification provided.
/// </summary>
None,
/// <summary>
/// Vulnerable component not included.
/// </summary>
ComponentNotPresent,
/// <summary>
/// Vulnerable code not present.
/// </summary>
VulnerableCodeNotPresent,
/// <summary>
/// Vulnerable code not in execute path.
/// </summary>
VulnerableCodeNotInExecutePath,
/// <summary>
/// Vulnerable code cannot be controlled by adversary.
/// </summary>
VulnerableCodeCannotBeControlledByAdversary,
/// <summary>
/// Inline mitigations already exist.
/// </summary>
InlineMitigationsAlreadyExist,
}
/// <summary>
/// Normalizes OpenVEX documents to canonical claims.
/// </summary>
public sealed class OpenVexNormalizer : IVexNormalizer
{
/// <inheritdoc />
public string Format => "OpenVEX";
/// <summary>
/// Mapping from OpenVEX status to atom assertions.
/// Per specification Table 2.
/// </summary>
private static readonly Dictionary<OpenVexStatus, List<AtomAssertion>> StatusToAtoms = new()
{
[OpenVexStatus.UnderInvestigation] =
[
// under_investigation: no definite assertions
],
[OpenVexStatus.NotAffected] =
[
new AtomAssertion { Atom = SecurityAtom.Applies, Value = false, Justification = "not_affected status" },
],
[OpenVexStatus.Affected] =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = true, Justification = "affected status" },
new AtomAssertion { Atom = SecurityAtom.Applies, Value = true, Justification = "affected status" },
],
[OpenVexStatus.Fixed] =
[
new AtomAssertion { Atom = SecurityAtom.Fixed, Value = true, Justification = "fixed status" },
],
};
/// <summary>
/// Mapping from OpenVEX justification to atom assertions.
/// Per specification Table 2.
/// </summary>
private static readonly Dictionary<OpenVexJustification, List<AtomAssertion>> JustificationToAtoms = new()
{
[OpenVexJustification.ComponentNotPresent] =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = false, Justification = "component_not_present" },
],
[OpenVexJustification.VulnerableCodeNotPresent] =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = false, Justification = "vulnerable_code_not_present" },
],
[OpenVexJustification.VulnerableCodeNotInExecutePath] =
[
new AtomAssertion { Atom = SecurityAtom.Reachable, Value = false, Justification = "vulnerable_code_not_in_execute_path" },
],
[OpenVexJustification.VulnerableCodeCannotBeControlledByAdversary] =
[
new AtomAssertion { Atom = SecurityAtom.Mitigated, Value = true, Justification = "vulnerable_code_cannot_be_controlled" },
],
[OpenVexJustification.InlineMitigationsAlreadyExist] =
[
new AtomAssertion { Atom = SecurityAtom.Mitigated, Value = true, Justification = "inline_mitigations_exist" },
],
};
/// <inheritdoc />
public IEnumerable<Claim> Normalize(string document, Principal principal, TrustLabel? trustLabel = null)
{
// Placeholder for JSON parsing implementation
yield break;
}
/// <summary>
/// Normalizes a pre-parsed OpenVEX statement.
/// </summary>
public Claim NormalizeStatement(
Subject subject,
OpenVexStatus status,
OpenVexJustification justification = OpenVexJustification.None,
string? actionStatement = null,
string? impactStatement = null,
Principal? principal = null,
TrustLabel? trustLabel = null)
{
var assertions = new List<AtomAssertion>();
// Add status-based assertions
if (StatusToAtoms.TryGetValue(status, out var statusAtoms))
{
assertions.AddRange(statusAtoms);
}
// Add justification-based assertions
if (justification != OpenVexJustification.None &&
JustificationToAtoms.TryGetValue(justification, out var justAtoms))
{
assertions.AddRange(justAtoms);
}
// Build detail from action/impact statements
var details = new List<string>();
if (!string.IsNullOrWhiteSpace(actionStatement))
details.Add($"action: {actionStatement}");
if (!string.IsNullOrWhiteSpace(impactStatement))
details.Add($"impact: {impactStatement}");
if (details.Count > 0)
{
var detail = string.Join("; ", details);
for (int i = 0; i < assertions.Count; i++)
{
assertions[i] = assertions[i] with
{
Justification = $"{assertions[i].Justification} ({detail})"
};
}
}
return new Claim
{
Subject = subject,
Issuer = principal ?? Principal.Unknown,
Assertions = assertions,
TrustLabel = trustLabel,
Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow },
};
}
}

View File

@@ -0,0 +1,224 @@
/**
* PolicyBundle - Policy configuration for trust evaluation.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Task: TRUST-014
*
* Defines trust roots, trust requirements, and selection rule overrides.
*/
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>
/// 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>
/// 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>
public bool IsTrusted(Principal principal, AuthorityScope? requiredScope = null)
{
var now = DateTimeOffset.UtcNow;
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>
public AssuranceLevel? GetMaxAssurance(Principal principal)
{
var now = DateTimeOffset.UtcNow;
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",
};
}

View File

@@ -0,0 +1,394 @@
/**
* ProofBundle - Content-addressable audit trail for disposition decisions.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Task: TRUST-015
*
* The proof bundle captures all inputs, normalization, atom evaluation,
* and decision trace for deterministic replay and audit.
*/
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// Input evidence that was ingested.
/// </summary>
public sealed record ProofInput
{
/// <summary>
/// The content-addressable digest of the input.
/// </summary>
public required string Digest { get; init; }
/// <summary>
/// The type of input (e.g., "sbom", "vex", "scan", "attestation").
/// </summary>
public required string Type { get; init; }
/// <summary>
/// The format of the input (e.g., "CycloneDX", "SPDX", "OpenVEX").
/// </summary>
public string? Format { get; init; }
/// <summary>
/// URI/path to the original input.
/// </summary>
public string? Source { get; init; }
/// <summary>
/// Timestamp when the input was ingested.
/// </summary>
public DateTimeOffset IngestedAt { get; init; } = DateTimeOffset.UtcNow;
}
/// <summary>
/// Normalization trace for a VEX statement.
/// </summary>
public sealed record NormalizationTrace
{
/// <summary>
/// The original statement ID.
/// </summary>
public string? OriginalId { get; init; }
/// <summary>
/// The VEX format.
/// </summary>
public required string SourceFormat { get; init; }
/// <summary>
/// The original status/state value.
/// </summary>
public string? OriginalStatus { get; init; }
/// <summary>
/// The original justification value.
/// </summary>
public string? OriginalJustification { get; init; }
/// <summary>
/// The generated claim ID.
/// </summary>
public required string ClaimId { get; init; }
/// <summary>
/// The atoms that were asserted.
/// </summary>
public required IReadOnlyList<AtomAssertion> GeneratedAssertions { get; init; }
}
/// <summary>
/// The atom table showing final values for a subject.
/// </summary>
public sealed record AtomTable
{
/// <summary>
/// The subject digest.
/// </summary>
public required string SubjectDigest { get; init; }
/// <summary>
/// The subject details.
/// </summary>
public required Subject Subject { get; init; }
/// <summary>
/// Atom values with support sets.
/// </summary>
public required IReadOnlyDictionary<SecurityAtom, AtomValueSnapshot> Atoms { get; init; }
}
/// <summary>
/// The decision result for a subject.
/// </summary>
public sealed record DecisionRecord
{
/// <summary>
/// The subject digest.
/// </summary>
public required string SubjectDigest { get; init; }
/// <summary>
/// The selected disposition.
/// </summary>
public required Disposition Disposition { get; init; }
/// <summary>
/// The rule that matched.
/// </summary>
public required string MatchedRule { get; init; }
/// <summary>
/// Human-readable explanation.
/// </summary>
public required string Explanation { get; init; }
/// <summary>
/// Full decision trace.
/// </summary>
public required IReadOnlyList<DecisionStep> Trace { get; init; }
/// <summary>
/// Detected conflicts.
/// </summary>
public IReadOnlyList<SecurityAtom> Conflicts { get; init; } = [];
/// <summary>
/// Detected unknowns.
/// </summary>
public IReadOnlyList<SecurityAtom> Unknowns { get; init; } = [];
}
/// <summary>
/// Content-addressable proof bundle for audit and replay.
/// </summary>
public sealed record ProofBundle
{
/// <summary>
/// The proof bundle ID (content-addressable).
/// </summary>
public string? Id { get; init; }
/// <summary>
/// Proof bundle version for schema evolution.
/// </summary>
public string Version { get; init; } = "1.0.0";
/// <summary>
/// Timestamp when the proof bundle was created.
/// </summary>
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
/// <summary>
/// The policy bundle used for evaluation.
/// </summary>
public required string PolicyBundleId { get; init; }
/// <summary>
/// Policy bundle version.
/// </summary>
public string? PolicyBundleVersion { get; init; }
/// <summary>
/// All inputs that were ingested.
/// </summary>
public required IReadOnlyList<ProofInput> Inputs { get; init; }
/// <summary>
/// Normalization traces for VEX statements.
/// </summary>
public IReadOnlyList<NormalizationTrace> Normalization { get; init; } = [];
/// <summary>
/// Claims that were generated/ingested.
/// </summary>
public required IReadOnlyList<Claim> Claims { get; init; }
/// <summary>
/// Atom tables for all subjects.
/// </summary>
public required IReadOnlyList<AtomTable> AtomTables { get; init; }
/// <summary>
/// Decision records for all subjects.
/// </summary>
public required IReadOnlyList<DecisionRecord> Decisions { get; init; }
/// <summary>
/// Summary statistics.
/// </summary>
public ProofBundleStats? Stats { get; init; }
/// <summary>
/// Computes a content-addressable ID for the proof bundle.
/// </summary>
public string ComputeId()
{
// Canonicalize and hash
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false,
};
// Create a canonical form without the Id field
var canonical = new
{
version = Version,
created_at = CreatedAt.ToUnixTimeSeconds(),
policy_bundle_id = PolicyBundleId,
policy_bundle_version = PolicyBundleVersion,
input_digests = Inputs.Select(i => i.Digest).Order().ToList(),
claim_ids = Claims.Select(c => c.Id ?? c.ComputeId()).Order().ToList(),
subject_digests = AtomTables.Select(a => a.SubjectDigest).Order().ToList(),
};
var json = JsonSerializer.Serialize(canonical, options);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
/// <summary>
/// Creates a proof bundle with computed ID.
/// </summary>
public ProofBundle WithComputedId() => this with { Id = ComputeId() };
}
/// <summary>
/// Summary statistics for a proof bundle.
/// </summary>
public sealed record ProofBundleStats
{
/// <summary>
/// Total number of inputs.
/// </summary>
public int InputCount { get; init; }
/// <summary>
/// Total number of claims.
/// </summary>
public int ClaimCount { get; init; }
/// <summary>
/// Total number of subjects.
/// </summary>
public int SubjectCount { get; init; }
/// <summary>
/// Number of subjects with conflicts.
/// </summary>
public int ConflictCount { get; init; }
/// <summary>
/// Number of subjects with incomplete data.
/// </summary>
public int IncompleteCount { get; init; }
/// <summary>
/// Disposition counts.
/// </summary>
public IReadOnlyDictionary<Disposition, int> DispositionCounts { get; init; } =
new Dictionary<Disposition, int>();
}
/// <summary>
/// Builder for creating proof bundles.
/// </summary>
public sealed class ProofBundleBuilder
{
private readonly List<ProofInput> _inputs = [];
private readonly List<NormalizationTrace> _normalization = [];
private readonly List<Claim> _claims = [];
private readonly List<AtomTable> _atomTables = [];
private readonly List<DecisionRecord> _decisions = [];
private string _policyBundleId = "unknown";
private string? _policyBundleVersion;
/// <summary>
/// Sets the policy bundle.
/// </summary>
public ProofBundleBuilder WithPolicyBundle(PolicyBundle policy)
{
_policyBundleId = policy.Id ?? "unknown";
_policyBundleVersion = policy.Version;
return this;
}
/// <summary>
/// Adds an input.
/// </summary>
public ProofBundleBuilder AddInput(ProofInput input)
{
_inputs.Add(input);
return this;
}
/// <summary>
/// Adds a normalization trace.
/// </summary>
public ProofBundleBuilder AddNormalization(NormalizationTrace trace)
{
_normalization.Add(trace);
return this;
}
/// <summary>
/// Adds a claim.
/// </summary>
public ProofBundleBuilder AddClaim(Claim claim)
{
_claims.Add(claim);
return this;
}
/// <summary>
/// Adds an atom table from subject state.
/// </summary>
public ProofBundleBuilder AddAtomTable(SubjectState state)
{
_atomTables.Add(new AtomTable
{
SubjectDigest = state.SubjectDigest,
Subject = state.Subject,
Atoms = state.ToSnapshot(),
});
return this;
}
/// <summary>
/// Adds a decision record.
/// </summary>
public ProofBundleBuilder AddDecision(string subjectDigest, DispositionResult result)
{
_decisions.Add(new DecisionRecord
{
SubjectDigest = subjectDigest,
Disposition = result.Disposition,
MatchedRule = result.MatchedRule,
Explanation = result.Explanation,
Trace = result.Trace,
Conflicts = result.Conflicts,
Unknowns = result.Unknowns,
});
return this;
}
/// <summary>
/// Builds the proof bundle.
/// </summary>
public ProofBundle Build()
{
var dispositionCounts = _decisions
.GroupBy(d => d.Disposition)
.ToDictionary(g => g.Key, g => g.Count());
var stats = new ProofBundleStats
{
InputCount = _inputs.Count,
ClaimCount = _claims.Count,
SubjectCount = _atomTables.Count,
ConflictCount = _decisions.Count(d => d.Conflicts.Count > 0),
IncompleteCount = _decisions.Count(d => d.Unknowns.Count > 0),
DispositionCounts = dispositionCounts,
};
var bundle = new ProofBundle
{
PolicyBundleId = _policyBundleId,
PolicyBundleVersion = _policyBundleVersion,
Inputs = _inputs,
Normalization = _normalization,
Claims = _claims,
AtomTables = _atomTables,
Decisions = _decisions,
Stats = stats,
};
return bundle.WithComputedId();
}
}

View File

@@ -0,0 +1,124 @@
/**
* Security Atoms - Canonical propositions for vulnerability disposition.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Task: TRUST-002
*
* Defines the orthogonal atomic propositions used to represent security
* knowledge about a Subject (artifact + component + vulnerability).
*
* External VEX formats (CycloneDX, OpenVEX, CSAF) are normalized into
* these canonical atoms for uniform aggregation and decision making.
*/
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// Canonical security propositions for vulnerability disposition.
/// Each atom is a boolean proposition that can have a K4 truth value.
/// </summary>
/// <remarks>
/// These atoms are intentionally orthogonal; external VEX formats
/// are normalized into combinations of these atoms.
/// </remarks>
public enum SecurityAtom
{
/// <summary>
/// PRESENT: The component instance exists in the artifact/context.
/// False when component is not actually in the artifact despite declaration.
/// </summary>
Present = 1,
/// <summary>
/// APPLIES: The vulnerability applies to this component (version/range/CPE match).
/// False when version is outside affected range.
/// </summary>
Applies = 2,
/// <summary>
/// REACHABLE: The vulnerable code is reachable in the given execution context.
/// False when code paths to vulnerability are not exercised.
/// </summary>
Reachable = 3,
/// <summary>
/// MITIGATED: Controls exist that prevent exploitation.
/// True when compiler protections, runtime guards, WAF rules, etc. are active.
/// </summary>
Mitigated = 4,
/// <summary>
/// FIXED: Remediation has been applied to the artifact.
/// True when patches, upgrades, or other fixes are in place.
/// </summary>
Fixed = 5,
/// <summary>
/// MISATTRIBUTED: The finding is a false association (false positive).
/// True when the vulnerability was incorrectly linked to this component.
/// </summary>
Misattributed = 6,
}
/// <summary>
/// Extension methods for SecurityAtom.
/// </summary>
public static class SecurityAtomExtensions
{
/// <summary>
/// Returns a human-readable display name for the atom.
/// </summary>
public static string ToDisplayName(this SecurityAtom atom) => atom switch
{
SecurityAtom.Present => "Component Present",
SecurityAtom.Applies => "Vulnerability Applies",
SecurityAtom.Reachable => "Code Reachable",
SecurityAtom.Mitigated => "Mitigations Active",
SecurityAtom.Fixed => "Remediation Applied",
SecurityAtom.Misattributed => "False Association",
_ => atom.ToString(),
};
/// <summary>
/// Returns the canonical string representation for serialization.
/// </summary>
public static string ToCanonicalName(this SecurityAtom atom) => atom switch
{
SecurityAtom.Present => "PRESENT",
SecurityAtom.Applies => "APPLIES",
SecurityAtom.Reachable => "REACHABLE",
SecurityAtom.Mitigated => "MITIGATED",
SecurityAtom.Fixed => "FIXED",
SecurityAtom.Misattributed => "MISATTRIBUTED",
_ => atom.ToString().ToUpperInvariant(),
};
/// <summary>
/// Parses a canonical name to SecurityAtom.
/// </summary>
public static SecurityAtom? FromCanonicalName(string name)
{
return name?.ToUpperInvariant() switch
{
"PRESENT" => SecurityAtom.Present,
"APPLIES" => SecurityAtom.Applies,
"REACHABLE" => SecurityAtom.Reachable,
"MITIGATED" => SecurityAtom.Mitigated,
"FIXED" => SecurityAtom.Fixed,
"MISATTRIBUTED" => SecurityAtom.Misattributed,
_ => null,
};
}
/// <summary>
/// Returns all defined security atoms.
/// </summary>
public static IEnumerable<SecurityAtom> All()
{
yield return SecurityAtom.Present;
yield return SecurityAtom.Applies;
yield return SecurityAtom.Reachable;
yield return SecurityAtom.Mitigated;
yield return SecurityAtom.Fixed;
yield return SecurityAtom.Misattributed;
}
}

View File

@@ -0,0 +1,187 @@
/**
* Subject - The target of security assertions.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Task: TRUST-004
*
* A Subject is the entity we are making a security determination about.
* It uniquely identifies the combination of:
* - Artifact (container image, binary, etc.)
* - Component (library, package)
* - Vulnerability (CVE, OSV, etc.)
* - Optional context (environment, config)
*/
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// Reference to an artifact being analyzed.
/// </summary>
public sealed record ArtifactRef
{
/// <summary>
/// Content-addressable digest (e.g., "sha256:abc123...").
/// </summary>
public required string Digest { get; init; }
/// <summary>
/// Optional name/tag for human readability.
/// </summary>
public string? Name { get; init; }
/// <summary>
/// Artifact type (e.g., "oci-image", "binary", "archive").
/// </summary>
public string? Type { get; init; }
}
/// <summary>
/// Reference to a component within an artifact.
/// </summary>
public sealed record ComponentRef
{
/// <summary>
/// Package URL (PURL) - preferred identifier.
/// Example: "pkg:npm/lodash@4.17.21"
/// </summary>
public string? Purl { get; init; }
/// <summary>
/// CPE (Common Platform Enumeration) - fallback identifier.
/// </summary>
public string? Cpe { get; init; }
/// <summary>
/// BOM reference ID - last resort identifier.
/// </summary>
public string? BomRef { get; init; }
/// <summary>
/// Returns the best available identifier.
/// </summary>
[JsonIgnore]
public string Id => Purl ?? Cpe ?? BomRef ?? "unknown";
}
/// <summary>
/// Reference to a vulnerability.
/// </summary>
public sealed record VulnerabilityRef
{
/// <summary>
/// Vulnerability identifier (e.g., "CVE-2024-12345", "GHSA-xxxx-xxxx-xxxx").
/// </summary>
public required string Id { get; init; }
/// <summary>
/// Source database (e.g., "nvd", "osv", "github").
/// </summary>
public string? Source { get; init; }
}
/// <summary>
/// Optional context for environment-sensitive assertions.
/// </summary>
public sealed record ContextRef
{
/// <summary>
/// Build configuration flags.
/// </summary>
public IReadOnlyList<string>? BuildFlags { get; init; }
/// <summary>
/// Runtime configuration profile.
/// </summary>
public string? ConfigProfile { get; init; }
/// <summary>
/// Deployment mode (e.g., "production", "staging").
/// </summary>
public string? DeploymentMode { get; init; }
/// <summary>
/// Operating system / libc family.
/// </summary>
public string? OsFamily { get; init; }
/// <summary>
/// Whether FIPS mode is enabled.
/// </summary>
public bool? FipsMode { get; init; }
/// <summary>
/// Security posture (e.g., "selinux:enforcing", "apparmor:enabled").
/// </summary>
public string? SecurityPosture { get; init; }
/// <summary>
/// Computes a content-addressable digest for this context.
/// </summary>
public string ComputeDigest()
{
var json = JsonSerializer.SerializeToUtf8Bytes(this, CanonicalJsonOptions.Default);
var hash = SHA256.HashData(json);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}
/// <summary>
/// The subject of a security assertion.
/// Uniquely identifies what we are making a determination about.
/// </summary>
public sealed record Subject
{
/// <summary>
/// Reference to the artifact containing the component.
/// </summary>
public required ArtifactRef Artifact { get; init; }
/// <summary>
/// Reference to the component within the artifact.
/// </summary>
public required ComponentRef Component { get; init; }
/// <summary>
/// Reference to the vulnerability being assessed.
/// </summary>
public required VulnerabilityRef Vulnerability { get; init; }
/// <summary>
/// Optional context for environment-sensitive assertions.
/// </summary>
public ContextRef? Context { get; init; }
/// <summary>
/// Computes a content-addressable digest for this subject.
/// Used as a stable key for aggregation.
/// </summary>
public string ComputeDigest()
{
var json = JsonSerializer.SerializeToUtf8Bytes(this, CanonicalJsonOptions.Default);
var hash = SHA256.HashData(json);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
/// <summary>
/// Returns a human-readable string representation.
/// </summary>
public override string ToString()
=> $"{Vulnerability.Id}@{Component.Id} in {Artifact.Digest[..19]}...";
}
/// <summary>
/// Canonical JSON serialization options for deterministic hashing.
/// </summary>
internal static class CanonicalJsonOptions
{
public static readonly JsonSerializerOptions Default = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
}

View File

@@ -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,
};
}

View File

@@ -0,0 +1,406 @@
/**
* TrustLatticeEngine - Orchestrates the complete trust evaluation pipeline.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Task: TRUST-016
*
* The engine coordinates:
* 1. VEX normalization from multiple formats
* 2. Claim ingestion and aggregation
* 3. K4 lattice evaluation
* 4. Disposition selection
* 5. Proof bundle generation
*/
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// Result of processing a batch of inputs.
/// </summary>
public sealed record EvaluationResult
{
/// <summary>
/// Whether the evaluation completed successfully.
/// </summary>
public required bool Success { get; init; }
/// <summary>
/// Error message if failed.
/// </summary>
public string? Error { get; init; }
/// <summary>
/// The proof bundle containing all evidence.
/// </summary>
public ProofBundle? ProofBundle { get; init; }
/// <summary>
/// Quick access to disposition results by subject.
/// </summary>
public IReadOnlyDictionary<string, DispositionResult> Dispositions { get; init; } =
new Dictionary<string, DispositionResult>();
/// <summary>
/// Warnings generated during evaluation.
/// </summary>
public IReadOnlyList<string> Warnings { get; init; } = [];
}
/// <summary>
/// Options for trust lattice evaluation.
/// </summary>
public sealed record EvaluationOptions
{
/// <summary>
/// Whether to generate a proof bundle.
/// </summary>
public bool GenerateProofBundle { get; init; } = true;
/// <summary>
/// Whether to include full decision traces in the proof bundle.
/// </summary>
public bool IncludeDecisionTraces { get; init; } = true;
/// <summary>
/// Whether to validate claim signatures.
/// </summary>
public bool ValidateSignatures { get; init; } = false;
/// <summary>
/// Timestamp for claim validity evaluation (null = now).
/// </summary>
public DateTimeOffset? EvaluationTime { get; init; }
/// <summary>
/// Filter to specific subjects (null = all).
/// </summary>
public IReadOnlySet<string>? SubjectFilter { get; init; }
}
/// <summary>
/// The trust lattice engine orchestrates the complete evaluation pipeline.
/// </summary>
public sealed class TrustLatticeEngine
{
private readonly PolicyBundle _policy;
private readonly LatticeStore _store;
private readonly DispositionSelector _selector;
private readonly Dictionary<string, IVexNormalizer> _normalizers;
/// <summary>
/// Creates a new trust lattice engine.
/// </summary>
/// <param name="policy">The policy bundle to use.</param>
public TrustLatticeEngine(PolicyBundle? policy = null)
{
_policy = policy ?? PolicyBundle.Default;
_store = new LatticeStore();
_selector = new DispositionSelector(_policy.GetEffectiveRules());
// Register default normalizers
_normalizers = new Dictionary<string, IVexNormalizer>(StringComparer.OrdinalIgnoreCase);
RegisterNormalizer(new CycloneDxVexNormalizer());
RegisterNormalizer(new OpenVexNormalizer());
RegisterNormalizer(new CsafVexNormalizer());
}
/// <summary>
/// Gets the policy bundle.
/// </summary>
public PolicyBundle Policy => _policy;
/// <summary>
/// Gets the lattice store.
/// </summary>
public LatticeStore Store => _store;
/// <summary>
/// Registers a VEX normalizer.
/// </summary>
public void RegisterNormalizer(IVexNormalizer normalizer)
{
_normalizers[normalizer.Format] = normalizer;
}
/// <summary>
/// Ingests a claim directly.
/// </summary>
public Claim IngestClaim(Claim claim)
{
return _store.IngestClaim(claim);
}
/// <summary>
/// Ingests multiple claims.
/// </summary>
public IReadOnlyList<Claim> IngestClaims(IEnumerable<Claim> claims)
{
return claims.Select(c => _store.IngestClaim(c)).ToList();
}
/// <summary>
/// Ingests a VEX document.
/// </summary>
/// <param name="document">The VEX document content.</param>
/// <param name="format">The VEX format (CycloneDX/ECMA-424, OpenVEX, CSAF).</param>
/// <param name="principal">The principal making the assertions.</param>
/// <param name="trustLabel">Default trust label for generated claims.</param>
public IReadOnlyList<Claim> IngestVex(
string document,
string format,
Principal principal,
TrustLabel? trustLabel = null)
{
if (!_normalizers.TryGetValue(format, out var normalizer))
{
throw new ArgumentException($"Unknown VEX format: {format}", nameof(format));
}
var claims = normalizer.Normalize(document, principal, trustLabel).ToList();
return IngestClaims(claims);
}
/// <summary>
/// Gets the disposition for a subject.
/// </summary>
public DispositionResult GetDisposition(Subject subject)
{
var state = _store.GetOrCreateSubject(subject);
return _selector.Select(state);
}
/// <summary>
/// Gets the disposition for a subject by digest.
/// </summary>
public DispositionResult? GetDisposition(string subjectDigest)
{
var state = _store.GetSubjectState(subjectDigest);
if (state is null) return null;
return _selector.Select(state);
}
/// <summary>
/// Evaluates all subjects and produces dispositions.
/// </summary>
public EvaluationResult Evaluate(EvaluationOptions? options = null)
{
options ??= new EvaluationOptions();
var warnings = new List<string>();
var dispositions = new Dictionary<string, DispositionResult>();
try
{
var subjects = _store.GetAllSubjects();
// Apply subject filter if specified
if (options.SubjectFilter is not null)
{
subjects = subjects.Where(s => options.SubjectFilter.Contains(s.SubjectDigest));
}
// Evaluate each subject
foreach (var state in subjects)
{
var result = _selector.Select(state);
dispositions[state.SubjectDigest] = result;
}
// Generate proof bundle if requested
ProofBundle? proofBundle = null;
if (options.GenerateProofBundle)
{
proofBundle = GenerateProofBundle(dispositions, options);
}
return new EvaluationResult
{
Success = true,
ProofBundle = proofBundle,
Dispositions = dispositions,
Warnings = warnings,
};
}
catch (Exception ex)
{
return new EvaluationResult
{
Success = false,
Error = ex.Message,
Dispositions = dispositions,
Warnings = warnings,
};
}
}
/// <summary>
/// Generates a proof bundle for the current evaluation state.
/// </summary>
private ProofBundle GenerateProofBundle(
Dictionary<string, DispositionResult> dispositions,
EvaluationOptions options)
{
var builder = new ProofBundleBuilder()
.WithPolicyBundle(_policy);
// Add all claims
foreach (var claim in _store.GetAllClaims())
{
builder.AddClaim(claim);
}
// Add atom tables and decisions for each subject
foreach (var state in _store.GetAllSubjects())
{
builder.AddAtomTable(state);
if (dispositions.TryGetValue(state.SubjectDigest, out var result))
{
builder.AddDecision(state.SubjectDigest, result);
}
}
return builder.Build();
}
/// <summary>
/// Clears all state from the engine.
/// </summary>
public void Clear()
{
_store.Clear();
}
/// <summary>
/// Gets statistics about the current state.
/// </summary>
public LatticeStoreStats GetStats() => _store.GetStats();
/// <summary>
/// Creates a builder for claims.
/// </summary>
public ClaimBuilder CreateClaim() => new(this);
/// <summary>
/// Fluent builder for creating and ingesting claims.
/// </summary>
public sealed class ClaimBuilder
{
private readonly TrustLatticeEngine _engine;
private Subject? _subject;
private Principal _principal = Principal.Unknown;
private TrustLabel? _trustLabel;
private readonly List<AtomAssertion> _assertions = [];
private readonly List<string> _evidenceRefs = [];
internal ClaimBuilder(TrustLatticeEngine engine)
{
_engine = engine;
}
/// <summary>
/// Sets the subject.
/// </summary>
public ClaimBuilder ForSubject(Subject subject)
{
_subject = subject;
return this;
}
/// <summary>
/// Sets the principal.
/// </summary>
public ClaimBuilder FromPrincipal(Principal principal)
{
_principal = principal;
return this;
}
/// <summary>
/// Sets the trust label.
/// </summary>
public ClaimBuilder WithTrust(TrustLabel label)
{
_trustLabel = label;
return this;
}
/// <summary>
/// Asserts an atom value.
/// </summary>
public ClaimBuilder Assert(SecurityAtom atom, bool value, string? justification = null)
{
_assertions.Add(new AtomAssertion
{
Atom = atom,
Value = value,
Justification = justification,
});
return this;
}
/// <summary>
/// Asserts PRESENT = true.
/// </summary>
public ClaimBuilder Present(bool value = true, string? justification = null)
=> Assert(SecurityAtom.Present, value, justification);
/// <summary>
/// Asserts APPLIES = true.
/// </summary>
public ClaimBuilder Applies(bool value = true, string? justification = null)
=> Assert(SecurityAtom.Applies, value, justification);
/// <summary>
/// Asserts REACHABLE = true.
/// </summary>
public ClaimBuilder Reachable(bool value = true, string? justification = null)
=> Assert(SecurityAtom.Reachable, value, justification);
/// <summary>
/// Asserts MITIGATED = true.
/// </summary>
public ClaimBuilder Mitigated(bool value = true, string? justification = null)
=> Assert(SecurityAtom.Mitigated, value, justification);
/// <summary>
/// Asserts FIXED = true.
/// </summary>
public ClaimBuilder Fixed(bool value = true, string? justification = null)
=> Assert(SecurityAtom.Fixed, value, justification);
/// <summary>
/// Asserts MISATTRIBUTED = true.
/// </summary>
public ClaimBuilder Misattributed(bool value = true, string? justification = null)
=> Assert(SecurityAtom.Misattributed, value, justification);
/// <summary>
/// References evidence by digest.
/// </summary>
public ClaimBuilder WithEvidence(string digest)
{
_evidenceRefs.Add(digest);
return this;
}
/// <summary>
/// Builds and ingests the claim.
/// </summary>
public Claim Build()
{
if (_subject is null)
throw new InvalidOperationException("Subject is required.");
var claim = new Claim
{
Subject = _subject,
Principal = _principal,
TrustLabel = _trustLabel,
Assertions = _assertions,
EvidenceRefs = _evidenceRefs,
TimeInfo = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow },
};
return _engine.IngestClaim(claim);
}
}
}

View File

@@ -0,0 +1,318 @@
/**
* VEX Normalizers - Convert vendor-specific VEX to canonical claims.
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
* Tasks: TRUST-010, TRUST-011, TRUST-012
*
* Normalizers translate CycloneDX/ECMA-424, OpenVEX, and CSAF VEX statements
* into canonical security atom assertions.
*/
namespace StellaOps.Policy.TrustLattice;
/// <summary>
/// Interface for VEX format normalizers.
/// </summary>
public interface IVexNormalizer
{
/// <summary>
/// The VEX format this normalizer handles.
/// </summary>
string Format { get; }
/// <summary>
/// Normalizes a VEX document into canonical claims.
/// </summary>
/// <param name="document">The raw VEX document (JSON or other format).</param>
/// <param name="principal">The principal making the assertions.</param>
/// <param name="trustLabel">Default trust label for generated claims.</param>
/// <returns>A sequence of normalized claims.</returns>
IEnumerable<Claim> Normalize(string document, Principal principal, TrustLabel? trustLabel = null);
}
/// <summary>
/// Result of normalizing a single VEX statement.
/// </summary>
public sealed record NormalizedStatement
{
/// <summary>
/// The generated claim.
/// </summary>
public required Claim Claim { get; init; }
/// <summary>
/// Original statement identifier from the VEX document.
/// </summary>
public string? OriginalId { get; init; }
/// <summary>
/// The VEX format the statement came from.
/// </summary>
public required string SourceFormat { get; init; }
/// <summary>
/// Any warnings generated during normalization.
/// </summary>
public IReadOnlyList<string> Warnings { get; init; } = [];
}
/// <summary>
/// CycloneDX/ECMA-424 VEX status values.
/// Per ECMA-424 section 7.
/// </summary>
public enum CycloneDxVexStatus
{
/// <summary>
/// Status not specified.
/// </summary>
Unknown,
/// <summary>
/// Analysis not yet complete.
/// </summary>
InTriage,
/// <summary>
/// Vulnerability does not affect this component.
/// </summary>
NotAffected,
/// <summary>
/// Vulnerability affects this component.
/// </summary>
Affected,
/// <summary>
/// A fix is available but not yet applied.
/// </summary>
FixAvailable,
/// <summary>
/// Component has been fixed.
/// </summary>
Fixed,
}
/// <summary>
/// CycloneDX/ECMA-424 justification values.
/// Per ECMA-424 section 7.2.
/// </summary>
public enum CycloneDxJustification
{
/// <summary>
/// No justification specified.
/// </summary>
None,
/// <summary>
/// Code not present.
/// </summary>
CodeNotPresent,
/// <summary>
/// Code not reachable.
/// </summary>
CodeNotReachable,
/// <summary>
/// Requires configuration not in default/deployed config.
/// </summary>
RequiresConfiguration,
/// <summary>
/// Requires dependency not in environment.
/// </summary>
RequiresDependency,
/// <summary>
/// Requires specific environment conditions.
/// </summary>
RequiresEnvironment,
/// <summary>
/// Protected by inline mitigation.
/// </summary>
ProtectedByMitigatingControl,
/// <summary>
/// Protected at perimeter.
/// </summary>
ProtectedAtPerimeter,
/// <summary>
/// Protected at runtime.
/// </summary>
ProtectedAtRuntime,
/// <summary>
/// Vulnerability was inaccurate (misattributed).
/// </summary>
VulnerableCodeCannotBeControlledByAdversary,
/// <summary>
/// Inline mitigations exist.
/// </summary>
InlineMitigationsAlreadyExist,
}
/// <summary>
/// Normalizes CycloneDX/ECMA-424 VEX documents to canonical claims.
/// </summary>
public sealed class CycloneDxVexNormalizer : IVexNormalizer
{
/// <inheritdoc />
public string Format => "CycloneDX/ECMA-424";
/// <summary>
/// Mapping from CycloneDX status to atom assertions.
/// Per specification Table 1.
/// </summary>
private static readonly Dictionary<CycloneDxVexStatus, List<AtomAssertion>> StatusToAtoms = new()
{
[CycloneDxVexStatus.InTriage] =
[
// in_triage: no definite assertions, only that analysis is incomplete
],
[CycloneDxVexStatus.NotAffected] =
[
new AtomAssertion { Atom = SecurityAtom.Applies, Value = false, Justification = "not_affected status" },
],
[CycloneDxVexStatus.Affected] =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = true, Justification = "affected status" },
new AtomAssertion { Atom = SecurityAtom.Applies, Value = true, Justification = "affected status" },
],
[CycloneDxVexStatus.FixAvailable] =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = true, Justification = "fix_available status" },
new AtomAssertion { Atom = SecurityAtom.Applies, Value = true, Justification = "fix_available status" },
// Fixed = false (fix is available but not applied)
new AtomAssertion { Atom = SecurityAtom.Fixed, Value = false, Justification = "fix available but not applied" },
],
[CycloneDxVexStatus.Fixed] =
[
new AtomAssertion { Atom = SecurityAtom.Fixed, Value = true, Justification = "fixed status" },
],
};
/// <summary>
/// Mapping from justification to additional atom assertions.
/// Per specification Table 1.
/// </summary>
private static readonly Dictionary<CycloneDxJustification, List<AtomAssertion>> JustificationToAtoms = new()
{
[CycloneDxJustification.CodeNotPresent] =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = false, Justification = "code_not_present" },
],
[CycloneDxJustification.CodeNotReachable] =
[
new AtomAssertion { Atom = SecurityAtom.Reachable, Value = false, Justification = "code_not_reachable" },
],
[CycloneDxJustification.RequiresConfiguration] =
[
new AtomAssertion { Atom = SecurityAtom.Applies, Value = false, Condition = "default_config", Justification = "requires_configuration" },
],
[CycloneDxJustification.RequiresDependency] =
[
new AtomAssertion { Atom = SecurityAtom.Applies, Value = false, Condition = "current_deps", Justification = "requires_dependency" },
],
[CycloneDxJustification.RequiresEnvironment] =
[
new AtomAssertion { Atom = SecurityAtom.Applies, Value = false, Condition = "deployed_env", Justification = "requires_environment" },
],
[CycloneDxJustification.ProtectedByMitigatingControl] =
[
new AtomAssertion { Atom = SecurityAtom.Mitigated, Value = true, Justification = "protected_by_mitigating_control" },
],
[CycloneDxJustification.ProtectedAtPerimeter] =
[
new AtomAssertion { Atom = SecurityAtom.Mitigated, Value = true, Justification = "protected_at_perimeter" },
],
[CycloneDxJustification.ProtectedAtRuntime] =
[
new AtomAssertion { Atom = SecurityAtom.Mitigated, Value = true, Justification = "protected_at_runtime" },
],
[CycloneDxJustification.VulnerableCodeCannotBeControlledByAdversary] =
[
new AtomAssertion { Atom = SecurityAtom.Mitigated, Value = true, Justification = "vulnerable_code_cannot_be_controlled" },
],
[CycloneDxJustification.InlineMitigationsAlreadyExist] =
[
new AtomAssertion { Atom = SecurityAtom.Mitigated, Value = true, Justification = "inline_mitigations_exist" },
],
};
/// <inheritdoc />
public IEnumerable<Claim> Normalize(string document, Principal principal, TrustLabel? trustLabel = null)
{
// For now, this is a simplified implementation.
// Full implementation would parse the CycloneDX JSON and extract VEX data.
// The real implementation should use System.Text.Json to parse the document.
// Placeholder: return empty for now
// Real implementation would:
// 1. Parse JSON document
// 2. Extract vulnerabilities[] array
// 3. For each vulnerability, extract analysis.state, analysis.justification
// 4. Map to atoms using the tables above
// 5. Build Subject from bom-ref, vulnerability ID, etc.
// 6. Create Claim with assertions
yield break;
}
/// <summary>
/// Normalizes a pre-parsed CycloneDX VEX statement.
/// </summary>
/// <param name="subject">The subject of the VEX statement.</param>
/// <param name="status">The CycloneDX status.</param>
/// <param name="justification">Optional justification.</param>
/// <param name="detail">Optional detail text.</param>
/// <param name="principal">The principal making the assertion.</param>
/// <param name="trustLabel">Optional trust label.</param>
/// <returns>A normalized claim.</returns>
public Claim NormalizeStatement(
Subject subject,
CycloneDxVexStatus status,
CycloneDxJustification justification = CycloneDxJustification.None,
string? detail = null,
Principal? principal = null,
TrustLabel? trustLabel = null)
{
var assertions = new List<AtomAssertion>();
// Add status-based assertions
if (StatusToAtoms.TryGetValue(status, out var statusAtoms))
{
assertions.AddRange(statusAtoms);
}
// Add justification-based assertions
if (justification != CycloneDxJustification.None &&
JustificationToAtoms.TryGetValue(justification, out var justAtoms))
{
assertions.AddRange(justAtoms);
}
// Add detail as justification if provided
if (!string.IsNullOrWhiteSpace(detail))
{
for (int i = 0; i < assertions.Count; i++)
{
assertions[i] = assertions[i] with
{
Justification = $"{assertions[i].Justification}: {detail}"
};
}
}
return new Claim
{
Subject = subject,
Principal = principal ?? Principal.Unknown,
Assertions = assertions,
TrustLabel = trustLabel,
TimeInfo = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow },
};
}
}