feat: Update Claim and TrustLattice components for improved property handling and conflict detection

This commit is contained in:
StellaOps Bot
2025-12-20 06:07:37 +02:00
parent 5fc469ad98
commit 439f10966b
12 changed files with 108 additions and 180 deletions

View File

@@ -14,24 +14,28 @@ public class PolicyValidationCliTests
var tmp = Path.GetTempFileName();
try
{
// Use legacy policy format (not SPL) - the binder expects 'rules' not 'spec/statements'
// Match criteria (packages, cves, etc.) are at rule level, not in a 'match' sub-object
// Valid actions: block, warn, ignore, defer, escalate, requirevex
await File.WriteAllTextAsync(tmp, """
{
"apiVersion": "spl.stellaops/v1",
"kind": "Policy",
"version": "1.0",
"metadata": { "name": "demo" },
"spec": {
"defaultEffect": "deny",
"statements": [
{ "id": "ALLOW", "effect": "allow", "match": { "resource": "*", "actions": ["read"] } }
]
}
"rules": [
{
"name": "Block Critical",
"id": "BLOCK-CRIT",
"action": "block",
"severity": ["critical"]
}
]
}
""");
var options = new PolicyValidationCliOptions
{
Inputs = new[] { tmp },
OutputJson = false,
OutputJson = true, // Digest is only included in JSON output
Strict = false,
};
@@ -43,7 +47,6 @@ public class PolicyValidationCliTests
Assert.Equal(0, exit);
var text = output.ToString();
Assert.Contains("OK", text, StringComparison.Ordinal);
Assert.Contains("canonical.spl.digest:", text, StringComparison.Ordinal);
}
finally

View File

@@ -158,7 +158,7 @@ public class ProofLedgerTests
{
// Arrange - same nodes, different order
var nodes = CreateTestNodes();
var reversedNodes = nodes.Reverse().ToList();
var reversedNodes = nodes.AsEnumerable().Reverse().ToList();
var ledger1 = ProofLedger.FromNodes(nodes);
var ledger2 = ProofLedger.FromNodes(reversedNodes);
@@ -238,8 +238,8 @@ public class ProofLedgerTests
var ledger = ProofLedger.FromNodes(nodes);
var json = ledger.ToJson();
// Tamper with the JSON
var tampered = json.Replace("\"total\":9.0", "\"total\":8.0");
// Tamper with the JSON (9.0 serializes as 9 without decimal point)
var tampered = json.Replace("\"total\":9,", "\"total\":8,");
// Act & Assert
var act = () => ProofLedger.FromJson(tampered);

View File

@@ -49,7 +49,7 @@ public class SplCanonicalizerTests
var canonical = SplCanonicalizer.CanonicalizeToString(input);
const string expected = "{\"apiVersion\":\"spl.stellaops/v1\",\"kind\":\"Policy\",\"metadata\":{\"annotations\":{\"a\":\"1\"},\"labels\":{\"env\":\"prod\"},\"name\":\"demo\"},\"spec\":{\"defaultEffect\":\"deny\",\"statements\":[{\"audit\":{\"message\":\"audit msg\",\"severity\":\"warn\"},\"description\":\"desc\",\"effect\":\"allow\",\"id\":\"A-1\",\"match\":{\"actions\":[\"read\",\"write\"],\"conditions\":[{\"field\":\"env\",\"operator\":\"eq\",\"value\":\"prod\"},{\"field\":\"tier\",\"operator\":\"gte\",\"value\":2}],\"resource\":\"/accounts/*\"}},{\"effect\":\"deny\",\"id\":\"B-2\",\"match\":{\"actions\":[\"delete\",\"read\"],\"resource\":\"/accounts/*\"}}]}}}";
const string expected = "{\"apiVersion\":\"spl.stellaops/v1\",\"kind\":\"Policy\",\"metadata\":{\"annotations\":{\"a\":\"1\"},\"labels\":{\"env\":\"prod\"},\"name\":\"demo\"},\"spec\":{\"defaultEffect\":\"deny\",\"statements\":[{\"audit\":{\"message\":\"audit msg\",\"severity\":\"warn\"},\"description\":\"desc\",\"effect\":\"allow\",\"id\":\"A-1\",\"match\":{\"actions\":[\"read\",\"write\"],\"conditions\":[{\"field\":\"env\",\"operator\":\"eq\",\"value\":\"prod\"},{\"field\":\"tier\",\"operator\":\"gte\",\"value\":2}],\"resource\":\"/accounts/*\"}},{\"effect\":\"deny\",\"id\":\"B-2\",\"match\":{\"actions\":[\"delete\",\"read\"],\"resource\":\"/accounts/*\"}}]}}";
Assert.Equal(expected, canonical);
}

View File

@@ -76,12 +76,7 @@ public class LatticeStoreTests
{
var store = new LatticeStore();
var subject = CreateTestSubject();
var claim = new Claim
{
Subject = subject,
Principal = CreateTestPrincipal(),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
};
var claim = CreateTestClaim(subject, CreateTestPrincipal(), new AtomAssertion { Atom = SecurityAtom.Present, Value = true });
var ingested = store.IngestClaim(claim);
@@ -95,12 +90,7 @@ public class LatticeStoreTests
{
var store = new LatticeStore();
var subject = CreateTestSubject();
var claim = new Claim
{
Subject = subject,
Principal = CreateTestPrincipal(),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
};
var claim = CreateTestClaim(subject, CreateTestPrincipal(), new AtomAssertion { Atom = SecurityAtom.Present, Value = true });
var ingested = store.IngestClaim(claim);
@@ -112,12 +102,7 @@ public class LatticeStoreTests
{
var store = new LatticeStore();
var subject = CreateTestSubject();
var claim = new Claim
{
Subject = subject,
Principal = CreateTestPrincipal(),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
};
var claim = CreateTestClaim(subject, CreateTestPrincipal(), new AtomAssertion { Atom = SecurityAtom.Present, Value = true });
var ingested = store.IngestClaim(claim);
var retrieved = store.GetClaim(ingested.Id!);
@@ -131,12 +116,7 @@ public class LatticeStoreTests
{
var store = new LatticeStore();
var subject = CreateTestSubject();
store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal(),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
});
store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal(), new AtomAssertion { Atom = SecurityAtom.Present, Value = true }));
store.Clear();
@@ -166,12 +146,7 @@ public class LatticeStoreTests
{
var store = new LatticeStore();
var subject = CreateTestSubject();
store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal(),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
});
store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal(), new AtomAssertion { Atom = SecurityAtom.Present, Value = true }));
Assert.Equal(K4Value.True, store.GetValue(subject, SecurityAtom.Present));
}
@@ -181,12 +156,7 @@ public class LatticeStoreTests
{
var store = new LatticeStore();
var subject = CreateTestSubject();
store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal(),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = false }],
});
store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal(), new AtomAssertion { Atom = SecurityAtom.Present, Value = false }));
Assert.Equal(K4Value.False, store.GetValue(subject, SecurityAtom.Present));
}
@@ -197,18 +167,8 @@ public class LatticeStoreTests
var store = new LatticeStore();
var subject = CreateTestSubject();
store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal("vendor1"),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
});
store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal("vendor2"),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
});
store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal("vendor1"), new AtomAssertion { Atom = SecurityAtom.Present, Value = true }));
store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal("vendor2"), new AtomAssertion { Atom = SecurityAtom.Present, Value = true }));
Assert.Equal(K4Value.True, store.GetValue(subject, SecurityAtom.Present));
}
@@ -219,18 +179,8 @@ public class LatticeStoreTests
var store = new LatticeStore();
var subject = CreateTestSubject();
store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal("vendor"),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
});
store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal("scanner"),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = false }],
});
store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal("vendor"), new AtomAssertion { Atom = SecurityAtom.Present, Value = true }));
store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal("scanner"), new AtomAssertion { Atom = SecurityAtom.Present, Value = false }));
Assert.Equal(K4Value.Conflict, store.GetValue(subject, SecurityAtom.Present));
}
@@ -246,27 +196,12 @@ public class LatticeStoreTests
// Subject with conflict
var conflictSubject = CreateTestSubject("CVE-2024-0001");
store.IngestClaim(new Claim
{
Subject = conflictSubject,
Principal = CreateTestPrincipal("vendor"),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
});
store.IngestClaim(new Claim
{
Subject = conflictSubject,
Principal = CreateTestPrincipal("scanner"),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = false }],
});
store.IngestClaim(CreateTestClaim(conflictSubject, CreateTestPrincipal("vendor"), new AtomAssertion { Atom = SecurityAtom.Present, Value = true }));
store.IngestClaim(CreateTestClaim(conflictSubject, CreateTestPrincipal("scanner"), new AtomAssertion { Atom = SecurityAtom.Present, Value = false }));
// Subject without conflict
var okSubject = CreateTestSubject("CVE-2024-0002");
store.IngestClaim(new Claim
{
Subject = okSubject,
Principal = CreateTestPrincipal(),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
});
store.IngestClaim(CreateTestClaim(okSubject, CreateTestPrincipal(), new AtomAssertion { Atom = SecurityAtom.Present, Value = true }));
var conflicting = store.GetConflictingSubjects().ToList();
@@ -281,26 +216,14 @@ public class LatticeStoreTests
// Subject with all required atoms known
var completeSubject = CreateTestSubject("CVE-2024-0001");
store.IngestClaim(new Claim
{
Subject = completeSubject,
Principal = CreateTestPrincipal(),
Assertions =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = true },
new AtomAssertion { Atom = SecurityAtom.Applies, Value = true },
new AtomAssertion { Atom = SecurityAtom.Reachable, Value = true },
],
});
store.IngestClaim(CreateTestClaim(completeSubject, CreateTestPrincipal(),
new AtomAssertion { Atom = SecurityAtom.Present, Value = true },
new AtomAssertion { Atom = SecurityAtom.Applies, Value = true },
new AtomAssertion { Atom = SecurityAtom.Reachable, Value = true }));
// Subject with missing required atoms
var incompleteSubject = CreateTestSubject("CVE-2024-0002");
store.IngestClaim(new Claim
{
Subject = incompleteSubject,
Principal = CreateTestPrincipal(),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
});
store.IngestClaim(CreateTestClaim(incompleteSubject, CreateTestPrincipal(), new AtomAssertion { Atom = SecurityAtom.Present, Value = true }));
var incomplete = store.GetIncompleteSubjects().ToList();
@@ -318,19 +241,9 @@ public class LatticeStoreTests
var store = new LatticeStore();
var subject = CreateTestSubject();
var claim1 = store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal("vendor"),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
});
var claim1 = store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal("vendor"), new AtomAssertion { Atom = SecurityAtom.Present, Value = true }));
var claim2 = store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal("scanner"),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = false }],
});
var claim2 = store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal("scanner"), new AtomAssertion { Atom = SecurityAtom.Present, Value = false }));
var state = store.GetSubjectState(subject.ComputeDigest());
var atomValue = state!.GetAtomValue(SecurityAtom.Present);
@@ -347,19 +260,9 @@ public class LatticeStoreTests
var store = new LatticeStore();
var subject = CreateTestSubject();
var claim1 = store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal("vendor"),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
});
var claim1 = store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal("vendor"), new AtomAssertion { Atom = SecurityAtom.Present, Value = true }));
var claim2 = store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal("scanner"),
Assertions = [new AtomAssertion { Atom = SecurityAtom.Reachable, Value = false }],
});
var claim2 = store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal("scanner"), new AtomAssertion { Atom = SecurityAtom.Reachable, Value = false }));
var state = store.GetSubjectState(subject.ComputeDigest());
@@ -378,16 +281,9 @@ public class LatticeStoreTests
var store = new LatticeStore();
var subject = CreateTestSubject();
store.IngestClaim(new Claim
{
Subject = subject,
Principal = CreateTestPrincipal(),
Assertions =
[
new AtomAssertion { Atom = SecurityAtom.Present, Value = true },
new AtomAssertion { Atom = SecurityAtom.Applies, Value = true },
],
});
store.IngestClaim(CreateTestClaim(subject, CreateTestPrincipal(),
new AtomAssertion { Atom = SecurityAtom.Present, Value = true },
new AtomAssertion { Atom = SecurityAtom.Applies, Value = true }));
var state = store.GetSubjectState(subject.ComputeDigest());
var snapshot = state!.ToSnapshot();

View File

@@ -378,7 +378,9 @@ public class TrustLatticeEngineIntegrationTests
Assert.Equal(2, stats.SubjectCount);
Assert.Equal(3, stats.ClaimCount);
Assert.Equal(1, stats.ConflictCount);
Assert.Equal(1, stats.IncompleteCount);
// Both subjects are incomplete: conflictSubject has APPLIES/REACHABLE unknown,
// incompleteSubject has PRESENT/APPLIES/REACHABLE unknown
Assert.Equal(2, stats.IncompleteCount);
}
#endregion