feat: Update Claim and TrustLattice components for improved property handling and conflict detection
This commit is contained in:
@@ -549,13 +549,21 @@ public static class SplLayeringEngine
|
||||
CopyAllProperties(o, meta);
|
||||
}
|
||||
|
||||
meta["labels"] = MergeStringMap(
|
||||
baseMeta.GetPropertyOrNull("labels"),
|
||||
overlayMeta.GetPropertyOrNull("labels"));
|
||||
// Only add labels if at least one input has them
|
||||
var baseLabels = baseMeta.GetPropertyOrNull("labels");
|
||||
var overlayLabels = overlayMeta.GetPropertyOrNull("labels");
|
||||
if (baseLabels.HasValue || overlayLabels.HasValue)
|
||||
{
|
||||
meta["labels"] = MergeStringMap(baseLabels, overlayLabels);
|
||||
}
|
||||
|
||||
meta["annotations"] = MergeStringMap(
|
||||
baseMeta.GetPropertyOrNull("annotations"),
|
||||
overlayMeta.GetPropertyOrNull("annotations"));
|
||||
// Only add annotations if at least one input has them
|
||||
var baseAnnotations = baseMeta.GetPropertyOrNull("annotations");
|
||||
var overlayAnnotations = overlayMeta.GetPropertyOrNull("annotations");
|
||||
if (baseAnnotations.HasValue || overlayAnnotations.HasValue)
|
||||
{
|
||||
meta["annotations"] = MergeStringMap(baseAnnotations, overlayAnnotations);
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
@@ -54,9 +54,12 @@ public static class SplMigrationTool
|
||||
labels[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
// Extract name value before creating result - can't reuse JsonNode across parents
|
||||
var nameValue = metadata.TryGetValue("name", out var name) ? name : null;
|
||||
|
||||
return new JsonObject
|
||||
{
|
||||
["name"] = labels.TryGetPropertyValue("name", out var nameNode) && nameNode is JsonValue ? nameNode : null,
|
||||
["name"] = nameValue,
|
||||
["labels"] = labels
|
||||
};
|
||||
}
|
||||
@@ -109,32 +112,39 @@ public static class SplMigrationTool
|
||||
|
||||
private static JsonObject BuildMatch(PolicyRuleMatchCriteria match)
|
||||
{
|
||||
var actions = new JsonArray();
|
||||
var resources = new JsonArray();
|
||||
var actionsList = new List<string>();
|
||||
var resourcesList = new List<string>();
|
||||
|
||||
foreach (var pkg in match.Packages)
|
||||
{
|
||||
resources.Add(pkg);
|
||||
actions.Add("use");
|
||||
resourcesList.Add(pkg);
|
||||
actionsList.Add("use");
|
||||
}
|
||||
|
||||
foreach (var path in match.Paths)
|
||||
{
|
||||
resources.Add(path);
|
||||
actions.Add("access");
|
||||
resourcesList.Add(path);
|
||||
actionsList.Add("access");
|
||||
}
|
||||
|
||||
// Ensure at least one action + resource to satisfy SPL schema.
|
||||
if (resources.Count == 0)
|
||||
if (resourcesList.Count == 0)
|
||||
{
|
||||
resources.Add("*");
|
||||
actions.Add("read");
|
||||
resourcesList.Add("*");
|
||||
actionsList.Add("read");
|
||||
}
|
||||
|
||||
// Build fresh JsonArray for actions - can't reuse JsonNode across parents
|
||||
var actionsArray = new JsonArray();
|
||||
foreach (var action in actionsList)
|
||||
{
|
||||
actionsArray.Add(action);
|
||||
}
|
||||
|
||||
return new JsonObject
|
||||
{
|
||||
["resource"] = resources[0],
|
||||
["actions"] = actions
|
||||
["resource"] = resourcesList[0],
|
||||
["actions"] = actionsArray
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -77,14 +77,14 @@ public sealed record Claim
|
||||
public required Subject Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The principal making this claim.
|
||||
/// The issuer (principal) making this claim.
|
||||
/// </summary>
|
||||
public Principal Principal { get; init; } = Principal.Unknown;
|
||||
public required Principal Issuer { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time information for the claim.
|
||||
/// </summary>
|
||||
public ClaimTimeInfo? TimeInfo { get; init; }
|
||||
public required ClaimTimeInfo Time { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// List of atomic assertions in this claim.
|
||||
@@ -120,8 +120,8 @@ public sealed record Claim
|
||||
var forHashing = new
|
||||
{
|
||||
subject = Subject,
|
||||
principal = new { id = Principal.Id },
|
||||
time = TimeInfo,
|
||||
issuer = new { id = Issuer.Id },
|
||||
time = Time,
|
||||
assertions = Assertions,
|
||||
evidence_refs = EvidenceRefs,
|
||||
};
|
||||
@@ -141,9 +141,9 @@ public sealed record Claim
|
||||
/// </summary>
|
||||
public bool IsValidAt(DateTimeOffset asOf)
|
||||
{
|
||||
if (TimeInfo?.ValidFrom.HasValue == true && asOf < TimeInfo.ValidFrom.Value)
|
||||
if (Time.ValidFrom.HasValue && asOf < Time.ValidFrom.Value)
|
||||
return false;
|
||||
if (TimeInfo?.ValidUntil.HasValue == true && asOf > TimeInfo.ValidUntil.Value)
|
||||
if (Time.ValidUntil.HasValue && asOf > Time.ValidUntil.Value)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -194,14 +194,17 @@ public sealed class DispositionSelector
|
||||
{
|
||||
var trace = new List<DecisionStep>();
|
||||
|
||||
// Detect conflicts and unknowns
|
||||
// Detect conflicts and unknowns on CRITICAL atoms (PRESENT, APPLIES, REACHABLE)
|
||||
// These are the atoms that determine whether a vulnerability is exploitable
|
||||
var criticalAtoms = new[] { SecurityAtom.Present, SecurityAtom.Applies, SecurityAtom.Reachable };
|
||||
|
||||
var conflicts = atomValues
|
||||
.Where(kvp => kvp.Value == K4Value.Conflict)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
var unknowns = atomValues
|
||||
.Where(kvp => kvp.Value == K4Value.Unknown)
|
||||
.Where(kvp => criticalAtoms.Contains(kvp.Key) && kvp.Value == K4Value.Unknown)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
@@ -356,15 +359,19 @@ public sealed class DispositionSelector
|
||||
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)
|
||||
// Rule 9: Any conflict on critical atoms → in_triage (requires human review)
|
||||
// This must fire BEFORE definitive dismissal rules to prevent auto-dismissal with conflicts
|
||||
new SelectionRule
|
||||
{
|
||||
Name = "conflict_detected",
|
||||
Priority = 80,
|
||||
Priority = 25, // After fixed (20), before not_present (30)
|
||||
Disposition = Disposition.InTriage,
|
||||
ConditionDescription = "Any atom = ⊤ (conflict)",
|
||||
Condition = atoms => atoms.Values.Any(v => v == K4Value.Conflict),
|
||||
ExplanationTemplate = "Conflicting evidence detected; requires human review.",
|
||||
ConditionDescription = "Any critical atom = ⊤ (conflict)",
|
||||
Condition = atoms =>
|
||||
atoms[SecurityAtom.Present] == K4Value.Conflict ||
|
||||
atoms[SecurityAtom.Applies] == K4Value.Conflict ||
|
||||
atoms[SecurityAtom.Reachable] == K4Value.Conflict,
|
||||
ExplanationTemplate = "Conflicting evidence detected on critical atoms; requires human review.",
|
||||
},
|
||||
|
||||
// Rule 10: Insufficient data → in_triage
|
||||
|
||||
@@ -393,11 +393,11 @@ public sealed class TrustLatticeEngine
|
||||
var claim = new Claim
|
||||
{
|
||||
Subject = _subject,
|
||||
Principal = _principal,
|
||||
Issuer = _principal,
|
||||
TrustLabel = _trustLabel,
|
||||
Assertions = _assertions,
|
||||
EvidenceRefs = _evidenceRefs,
|
||||
TimeInfo = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow },
|
||||
Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow },
|
||||
};
|
||||
|
||||
return _engine.IngestClaim(claim);
|
||||
|
||||
@@ -309,10 +309,10 @@ public sealed class CycloneDxVexNormalizer : IVexNormalizer
|
||||
return new Claim
|
||||
{
|
||||
Subject = subject,
|
||||
Principal = principal ?? Principal.Unknown,
|
||||
Issuer = principal ?? Principal.Unknown,
|
||||
Assertions = assertions,
|
||||
TrustLabel = trustLabel,
|
||||
TimeInfo = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow },
|
||||
Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user