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:
@@ -6,7 +6,19 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="8.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy/StellaOps.Policy.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* K4 Lattice Unit Tests
|
||||
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
|
||||
* Task: TRUST-017
|
||||
*
|
||||
* Tests for Belnap four-valued logic operations:
|
||||
* - Join (knowledge union)
|
||||
* - Meet (knowledge intersection)
|
||||
* - Order (knowledge ordering)
|
||||
* - Negation
|
||||
*/
|
||||
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
|
||||
namespace StellaOps.Policy.Tests.TrustLattice;
|
||||
|
||||
public class K4LatticeTests
|
||||
{
|
||||
#region Join Tests
|
||||
|
||||
[Fact]
|
||||
public void Join_UnknownWithUnknown_ReturnsUnknown()
|
||||
{
|
||||
Assert.Equal(K4Value.Unknown, K4Lattice.Join(K4Value.Unknown, K4Value.Unknown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(K4Value.True)]
|
||||
[InlineData(K4Value.False)]
|
||||
[InlineData(K4Value.Conflict)]
|
||||
public void Join_UnknownWithAny_ReturnsOther(K4Value other)
|
||||
{
|
||||
Assert.Equal(other, K4Lattice.Join(K4Value.Unknown, other));
|
||||
Assert.Equal(other, K4Lattice.Join(other, K4Value.Unknown));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Join_TrueWithTrue_ReturnsTrue()
|
||||
{
|
||||
Assert.Equal(K4Value.True, K4Lattice.Join(K4Value.True, K4Value.True));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Join_FalseWithFalse_ReturnsFalse()
|
||||
{
|
||||
Assert.Equal(K4Value.False, K4Lattice.Join(K4Value.False, K4Value.False));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Join_TrueWithFalse_ReturnsConflict()
|
||||
{
|
||||
Assert.Equal(K4Value.Conflict, K4Lattice.Join(K4Value.True, K4Value.False));
|
||||
Assert.Equal(K4Value.Conflict, K4Lattice.Join(K4Value.False, K4Value.True));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(K4Value.Unknown)]
|
||||
[InlineData(K4Value.True)]
|
||||
[InlineData(K4Value.False)]
|
||||
[InlineData(K4Value.Conflict)]
|
||||
public void Join_ConflictWithAny_ReturnsConflict(K4Value other)
|
||||
{
|
||||
Assert.Equal(K4Value.Conflict, K4Lattice.Join(K4Value.Conflict, other));
|
||||
Assert.Equal(K4Value.Conflict, K4Lattice.Join(other, K4Value.Conflict));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Join_IsCommutative()
|
||||
{
|
||||
var values = new[] { K4Value.Unknown, K4Value.True, K4Value.False, K4Value.Conflict };
|
||||
foreach (var a in values)
|
||||
foreach (var b in values)
|
||||
{
|
||||
Assert.Equal(K4Lattice.Join(a, b), K4Lattice.Join(b, a));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Join_IsAssociative()
|
||||
{
|
||||
var values = new[] { K4Value.Unknown, K4Value.True, K4Value.False, K4Value.Conflict };
|
||||
foreach (var a in values)
|
||||
foreach (var b in values)
|
||||
foreach (var c in values)
|
||||
{
|
||||
Assert.Equal(
|
||||
K4Lattice.Join(K4Lattice.Join(a, b), c),
|
||||
K4Lattice.Join(a, K4Lattice.Join(b, c)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JoinAll_EmptySequence_ReturnsUnknown()
|
||||
{
|
||||
Assert.Equal(K4Value.Unknown, K4Lattice.JoinAll([]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JoinAll_SingleValue_ReturnsSame()
|
||||
{
|
||||
Assert.Equal(K4Value.True, K4Lattice.JoinAll([K4Value.True]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JoinAll_MultipleValues_ReturnsJoin()
|
||||
{
|
||||
Assert.Equal(K4Value.Conflict, K4Lattice.JoinAll([K4Value.Unknown, K4Value.True, K4Value.False]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Meet Tests
|
||||
|
||||
[Fact]
|
||||
public void Meet_ConflictWithConflict_ReturnsConflict()
|
||||
{
|
||||
Assert.Equal(K4Value.Conflict, K4Lattice.Meet(K4Value.Conflict, K4Value.Conflict));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(K4Value.Unknown)]
|
||||
[InlineData(K4Value.True)]
|
||||
[InlineData(K4Value.False)]
|
||||
public void Meet_ConflictWithAny_ReturnsOther(K4Value other)
|
||||
{
|
||||
Assert.Equal(other, K4Lattice.Meet(K4Value.Conflict, other));
|
||||
Assert.Equal(other, K4Lattice.Meet(other, K4Value.Conflict));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Meet_TrueWithFalse_ReturnsUnknown()
|
||||
{
|
||||
Assert.Equal(K4Value.Unknown, K4Lattice.Meet(K4Value.True, K4Value.False));
|
||||
Assert.Equal(K4Value.Unknown, K4Lattice.Meet(K4Value.False, K4Value.True));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(K4Value.Unknown)]
|
||||
[InlineData(K4Value.True)]
|
||||
[InlineData(K4Value.False)]
|
||||
[InlineData(K4Value.Conflict)]
|
||||
public void Meet_UnknownWithAny_ReturnsUnknown(K4Value other)
|
||||
{
|
||||
Assert.Equal(K4Value.Unknown, K4Lattice.Meet(K4Value.Unknown, other));
|
||||
Assert.Equal(K4Value.Unknown, K4Lattice.Meet(other, K4Value.Unknown));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Meet_IsCommutative()
|
||||
{
|
||||
var values = new[] { K4Value.Unknown, K4Value.True, K4Value.False, K4Value.Conflict };
|
||||
foreach (var a in values)
|
||||
foreach (var b in values)
|
||||
{
|
||||
Assert.Equal(K4Lattice.Meet(a, b), K4Lattice.Meet(b, a));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Order Tests
|
||||
|
||||
[Fact]
|
||||
public void LessOrEqual_UnknownLessOrEqualToAll()
|
||||
{
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.Unknown, K4Value.Unknown));
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.Unknown, K4Value.True));
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.Unknown, K4Value.False));
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.Unknown, K4Value.Conflict));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LessOrEqual_ConflictGreaterOrEqualToAll()
|
||||
{
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.Unknown, K4Value.Conflict));
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.True, K4Value.Conflict));
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.False, K4Value.Conflict));
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.Conflict, K4Value.Conflict));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LessOrEqual_TrueAndFalseIncomparable()
|
||||
{
|
||||
Assert.False(K4Lattice.LessOrEqual(K4Value.True, K4Value.False));
|
||||
Assert.False(K4Lattice.LessOrEqual(K4Value.False, K4Value.True));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LessOrEqual_IsReflexive()
|
||||
{
|
||||
var values = new[] { K4Value.Unknown, K4Value.True, K4Value.False, K4Value.Conflict };
|
||||
foreach (var v in values)
|
||||
{
|
||||
Assert.True(K4Lattice.LessOrEqual(v, v));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LessOrEqual_IsTransitive()
|
||||
{
|
||||
// ⊥ ≤ T ≤ ⊤ and ⊥ ≤ F ≤ ⊤
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.Unknown, K4Value.True));
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.True, K4Value.Conflict));
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.Unknown, K4Value.Conflict));
|
||||
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.Unknown, K4Value.False));
|
||||
Assert.True(K4Lattice.LessOrEqual(K4Value.False, K4Value.Conflict));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FromSupport Tests
|
||||
|
||||
[Fact]
|
||||
public void FromSupport_NoSupport_ReturnsUnknown()
|
||||
{
|
||||
Assert.Equal(K4Value.Unknown, K4Lattice.FromSupport(false, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromSupport_TrueSupportOnly_ReturnsTrue()
|
||||
{
|
||||
Assert.Equal(K4Value.True, K4Lattice.FromSupport(true, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromSupport_FalseSupportOnly_ReturnsFalse()
|
||||
{
|
||||
Assert.Equal(K4Value.False, K4Lattice.FromSupport(false, true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromSupport_BothSupports_ReturnsConflict()
|
||||
{
|
||||
Assert.Equal(K4Value.Conflict, K4Lattice.FromSupport(true, true));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Negation Tests
|
||||
|
||||
[Fact]
|
||||
public void Negate_True_ReturnsFalse()
|
||||
{
|
||||
Assert.Equal(K4Value.False, K4Lattice.Negate(K4Value.True));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Negate_False_ReturnsTrue()
|
||||
{
|
||||
Assert.Equal(K4Value.True, K4Lattice.Negate(K4Value.False));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Negate_Unknown_ReturnsUnknown()
|
||||
{
|
||||
Assert.Equal(K4Value.Unknown, K4Lattice.Negate(K4Value.Unknown));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Negate_Conflict_ReturnsConflict()
|
||||
{
|
||||
Assert.Equal(K4Value.Conflict, K4Lattice.Negate(K4Value.Conflict));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Negate_IsInvolutive()
|
||||
{
|
||||
var values = new[] { K4Value.Unknown, K4Value.True, K4Value.False, K4Value.Conflict };
|
||||
foreach (var v in values)
|
||||
{
|
||||
Assert.Equal(v, K4Lattice.Negate(K4Lattice.Negate(v)));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Support Predicates Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(K4Value.True, true)]
|
||||
[InlineData(K4Value.False, false)]
|
||||
[InlineData(K4Value.Unknown, false)]
|
||||
[InlineData(K4Value.Conflict, true)]
|
||||
public void HasTrueSupport_ReturnsCorrectValue(K4Value value, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, K4Lattice.HasTrueSupport(value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(K4Value.True, false)]
|
||||
[InlineData(K4Value.False, true)]
|
||||
[InlineData(K4Value.Unknown, false)]
|
||||
[InlineData(K4Value.Conflict, true)]
|
||||
public void HasFalseSupport_ReturnsCorrectValue(K4Value value, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, K4Lattice.HasFalseSupport(value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(K4Value.True, true)]
|
||||
[InlineData(K4Value.False, true)]
|
||||
[InlineData(K4Value.Unknown, false)]
|
||||
[InlineData(K4Value.Conflict, false)]
|
||||
public void IsDefinite_ReturnsCorrectValue(K4Value value, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, K4Lattice.IsDefinite(value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(K4Value.True, false)]
|
||||
[InlineData(K4Value.False, false)]
|
||||
[InlineData(K4Value.Unknown, true)]
|
||||
[InlineData(K4Value.Conflict, true)]
|
||||
public void IsIndeterminate_ReturnsCorrectValue(K4Value value, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, K4Lattice.IsIndeterminate(value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* LatticeStore Aggregation Unit Tests
|
||||
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
|
||||
* Task: TRUST-019
|
||||
*
|
||||
* Tests for claim aggregation and K4 value computation:
|
||||
* - Support set tracking
|
||||
* - K4 value computation from support sets
|
||||
* - Conflict detection
|
||||
* - Trust label tracking
|
||||
*/
|
||||
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
|
||||
namespace StellaOps.Policy.Tests.TrustLattice;
|
||||
|
||||
public class LatticeStoreTests
|
||||
{
|
||||
private static Subject CreateTestSubject(string vulnId = "CVE-2024-1234")
|
||||
{
|
||||
return new Subject
|
||||
{
|
||||
Artifact = new ArtifactRef
|
||||
{
|
||||
Digest = "sha256:abc123",
|
||||
Name = "test-image:latest",
|
||||
Type = "oci",
|
||||
},
|
||||
Component = new ComponentRef
|
||||
{
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
},
|
||||
Vulnerability = new VulnerabilityRef
|
||||
{
|
||||
Id = vulnId,
|
||||
Source = "NVD",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static Principal CreateTestPrincipal(string id = "vendor")
|
||||
{
|
||||
return new Principal
|
||||
{
|
||||
Id = id,
|
||||
Roles = PrincipalRole.Vendor,
|
||||
};
|
||||
}
|
||||
|
||||
private static Claim CreateTestClaim(Subject subject, Principal issuer, params AtomAssertion[] assertions)
|
||||
{
|
||||
return new Claim
|
||||
{
|
||||
Subject = subject,
|
||||
Issuer = issuer,
|
||||
Time = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow },
|
||||
Assertions = assertions,
|
||||
};
|
||||
}
|
||||
|
||||
#region Basic Store Operations
|
||||
|
||||
[Fact]
|
||||
public void NewStore_IsEmpty()
|
||||
{
|
||||
var store = new LatticeStore();
|
||||
var stats = store.GetStats();
|
||||
|
||||
Assert.Equal(0, stats.SubjectCount);
|
||||
Assert.Equal(0, stats.ClaimCount);
|
||||
Assert.Equal(0, stats.EvidenceCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IngestClaim_AddsToStore()
|
||||
{
|
||||
var store = new LatticeStore();
|
||||
var subject = CreateTestSubject();
|
||||
var claim = new Claim
|
||||
{
|
||||
Subject = subject,
|
||||
Principal = CreateTestPrincipal(),
|
||||
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
|
||||
};
|
||||
|
||||
var ingested = store.IngestClaim(claim);
|
||||
|
||||
Assert.NotNull(ingested.Id);
|
||||
Assert.Equal(1, store.GetStats().SubjectCount);
|
||||
Assert.Equal(1, store.GetStats().ClaimCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IngestClaim_ComputesContentAddressableId()
|
||||
{
|
||||
var store = new LatticeStore();
|
||||
var subject = CreateTestSubject();
|
||||
var claim = new Claim
|
||||
{
|
||||
Subject = subject,
|
||||
Principal = CreateTestPrincipal(),
|
||||
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
|
||||
};
|
||||
|
||||
var ingested = store.IngestClaim(claim);
|
||||
|
||||
Assert.StartsWith("sha256:", ingested.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetClaim_ReturnsIngestedClaim()
|
||||
{
|
||||
var store = new LatticeStore();
|
||||
var subject = CreateTestSubject();
|
||||
var claim = new Claim
|
||||
{
|
||||
Subject = subject,
|
||||
Principal = CreateTestPrincipal(),
|
||||
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
|
||||
};
|
||||
|
||||
var ingested = store.IngestClaim(claim);
|
||||
var retrieved = store.GetClaim(ingested.Id!);
|
||||
|
||||
Assert.NotNull(retrieved);
|
||||
Assert.Equal(ingested.Id, retrieved.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_RemovesAllData()
|
||||
{
|
||||
var store = new LatticeStore();
|
||||
var subject = CreateTestSubject();
|
||||
store.IngestClaim(new Claim
|
||||
{
|
||||
Subject = subject,
|
||||
Principal = CreateTestPrincipal(),
|
||||
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
|
||||
});
|
||||
|
||||
store.Clear();
|
||||
|
||||
Assert.Equal(0, store.GetStats().SubjectCount);
|
||||
Assert.Equal(0, store.GetStats().ClaimCount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region K4 Value Computation
|
||||
|
||||
[Fact]
|
||||
public void NoAssertions_ReturnsUnknown()
|
||||
{
|
||||
var store = new LatticeStore();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var state = store.GetOrCreateSubject(subject);
|
||||
|
||||
Assert.Equal(K4Value.Unknown, state.GetValue(SecurityAtom.Present));
|
||||
Assert.Equal(K4Value.Unknown, state.GetValue(SecurityAtom.Applies));
|
||||
Assert.Equal(K4Value.Unknown, state.GetValue(SecurityAtom.Reachable));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrueAssertion_ReturnsTrue()
|
||||
{
|
||||
var store = new LatticeStore();
|
||||
var subject = CreateTestSubject();
|
||||
store.IngestClaim(new Claim
|
||||
{
|
||||
Subject = subject,
|
||||
Principal = CreateTestPrincipal(),
|
||||
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = true }],
|
||||
});
|
||||
|
||||
Assert.Equal(K4Value.True, store.GetValue(subject, SecurityAtom.Present));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FalseAssertion_ReturnsFalse()
|
||||
{
|
||||
var store = new LatticeStore();
|
||||
var subject = CreateTestSubject();
|
||||
store.IngestClaim(new Claim
|
||||
{
|
||||
Subject = subject,
|
||||
Principal = CreateTestPrincipal(),
|
||||
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = false }],
|
||||
});
|
||||
|
||||
Assert.Equal(K4Value.False, store.GetValue(subject, SecurityAtom.Present));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleTrueAssertions_ReturnsTrue()
|
||||
{
|
||||
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 }],
|
||||
});
|
||||
|
||||
Assert.Equal(K4Value.True, store.GetValue(subject, SecurityAtom.Present));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConflictingAssertions_ReturnsConflict()
|
||||
{
|
||||
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 }],
|
||||
});
|
||||
|
||||
Assert.Equal(K4Value.Conflict, store.GetValue(subject, SecurityAtom.Present));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Conflict Detection
|
||||
|
||||
[Fact]
|
||||
public void GetConflictingSubjects_ReturnsConflicts()
|
||||
{
|
||||
var store = new LatticeStore();
|
||||
|
||||
// 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 }],
|
||||
});
|
||||
|
||||
// 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 }],
|
||||
});
|
||||
|
||||
var conflicting = store.GetConflictingSubjects().ToList();
|
||||
|
||||
Assert.Single(conflicting);
|
||||
Assert.Equal(conflictSubject.ComputeDigest(), conflicting[0].SubjectDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIncompleteSubjects_ReturnsUnknowns()
|
||||
{
|
||||
var store = new LatticeStore();
|
||||
|
||||
// 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 },
|
||||
],
|
||||
});
|
||||
|
||||
// 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 }],
|
||||
});
|
||||
|
||||
var incomplete = store.GetIncompleteSubjects().ToList();
|
||||
|
||||
Assert.Single(incomplete);
|
||||
Assert.Equal(incompleteSubject.ComputeDigest(), incomplete[0].SubjectDigest);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Support Set Tracking
|
||||
|
||||
[Fact]
|
||||
public void AtomValue_TracksSupportSets()
|
||||
{
|
||||
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 claim2 = store.IngestClaim(new Claim
|
||||
{
|
||||
Subject = subject,
|
||||
Principal = CreateTestPrincipal("scanner"),
|
||||
Assertions = [new AtomAssertion { Atom = SecurityAtom.Present, Value = false }],
|
||||
});
|
||||
|
||||
var state = store.GetSubjectState(subject.ComputeDigest());
|
||||
var atomValue = state!.GetAtomValue(SecurityAtom.Present);
|
||||
|
||||
Assert.Single(atomValue.SupportTrue);
|
||||
Assert.Single(atomValue.SupportFalse);
|
||||
Assert.Contains(claim1.Id!, atomValue.SupportTrue);
|
||||
Assert.Contains(claim2.Id!, atomValue.SupportFalse);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubjectState_TracksAllClaimIds()
|
||||
{
|
||||
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 claim2 = store.IngestClaim(new Claim
|
||||
{
|
||||
Subject = subject,
|
||||
Principal = CreateTestPrincipal("scanner"),
|
||||
Assertions = [new AtomAssertion { Atom = SecurityAtom.Reachable, Value = false }],
|
||||
});
|
||||
|
||||
var state = store.GetSubjectState(subject.ComputeDigest());
|
||||
|
||||
Assert.Equal(2, state!.ClaimIds.Count);
|
||||
Assert.Contains(claim1.Id!, state.ClaimIds);
|
||||
Assert.Contains(claim2.Id!, state.ClaimIds);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Snapshot Tests
|
||||
|
||||
[Fact]
|
||||
public void SubjectState_ToSnapshot_CapturesAllAtoms()
|
||||
{
|
||||
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 },
|
||||
],
|
||||
});
|
||||
|
||||
var state = store.GetSubjectState(subject.ComputeDigest());
|
||||
var snapshot = state!.ToSnapshot();
|
||||
|
||||
Assert.Equal(6, snapshot.Count); // All 6 atoms
|
||||
Assert.Equal(K4Value.True, snapshot[SecurityAtom.Present].Value);
|
||||
Assert.Equal(K4Value.True, snapshot[SecurityAtom.Applies].Value);
|
||||
Assert.Equal(K4Value.Unknown, snapshot[SecurityAtom.Reachable].Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
/**
|
||||
* Trust Lattice Engine Integration Tests
|
||||
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
|
||||
* Task: TRUST-020
|
||||
*
|
||||
* Integration tests for the complete trust evaluation pipeline:
|
||||
* - Vendor vs scanner conflict scenario
|
||||
* - Multi-source claim aggregation
|
||||
* - Disposition selection with conflicts
|
||||
* - Proof bundle generation
|
||||
*/
|
||||
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
|
||||
namespace StellaOps.Policy.Tests.TrustLattice;
|
||||
|
||||
public class TrustLatticeEngineIntegrationTests
|
||||
{
|
||||
private static Subject CreateTestSubject(
|
||||
string vulnId = "CVE-2024-1234",
|
||||
string component = "pkg:npm/lodash@4.17.21")
|
||||
{
|
||||
return new Subject
|
||||
{
|
||||
Artifact = new ArtifactRef
|
||||
{
|
||||
Digest = "sha256:abc123def456",
|
||||
Name = "myapp:v1.0",
|
||||
Type = "oci",
|
||||
},
|
||||
Component = new ComponentRef
|
||||
{
|
||||
Purl = component,
|
||||
},
|
||||
Vulnerability = new VulnerabilityRef
|
||||
{
|
||||
Id = vulnId,
|
||||
Source = "NVD",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#region Vendor vs Scanner Conflict Scenario
|
||||
|
||||
[Fact]
|
||||
public void VendorVsScannerConflict_DetectsConflict()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var vendor = new Principal
|
||||
{
|
||||
Id = "npm-lodash-maintainer",
|
||||
DisplayName = "Lodash Maintainers",
|
||||
Roles = PrincipalRole.Vendor,
|
||||
};
|
||||
|
||||
var scanner = new Principal
|
||||
{
|
||||
Id = "stellaops-scanner",
|
||||
DisplayName = "StellaOps Scanner",
|
||||
Roles = PrincipalRole.Scanner,
|
||||
};
|
||||
|
||||
// Vendor claims: not affected - code not reachable
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.FromPrincipal(vendor)
|
||||
.Applies(false, "not_affected - test function only")
|
||||
.Reachable(false, "vulnerable code not in main execution path")
|
||||
.Build();
|
||||
|
||||
// Scanner claims: affected - found via static analysis
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.FromPrincipal(scanner)
|
||||
.Present(true, "component detected in SBOM")
|
||||
.Applies(true, "version matches CVE range")
|
||||
.Build();
|
||||
|
||||
// Evaluate
|
||||
var result = engine.GetDisposition(subject);
|
||||
|
||||
// APPLIES has conflict (vendor says false, scanner says true)
|
||||
Assert.Contains(SecurityAtom.Applies, result.Conflicts);
|
||||
Assert.Equal(Disposition.InTriage, result.Disposition);
|
||||
Assert.Contains("conflict", result.Explanation.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VendorVsScannerConflict_ProofBundleCapturesEvidence()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var vendor = new Principal { Id = "vendor", Roles = PrincipalRole.Vendor };
|
||||
var scanner = new Principal { Id = "scanner", Roles = PrincipalRole.Scanner };
|
||||
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.FromPrincipal(vendor)
|
||||
.Reachable(false, "not in execution path")
|
||||
.Build();
|
||||
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.FromPrincipal(scanner)
|
||||
.Reachable(true, "static analysis shows call path")
|
||||
.Build();
|
||||
|
||||
var evalResult = engine.Evaluate();
|
||||
|
||||
Assert.True(evalResult.Success);
|
||||
Assert.NotNull(evalResult.ProofBundle);
|
||||
|
||||
var proof = evalResult.ProofBundle!;
|
||||
Assert.Equal(2, proof.Claims.Count);
|
||||
Assert.Single(proof.AtomTables);
|
||||
Assert.Single(proof.Decisions);
|
||||
|
||||
// Verify conflict is captured in atom table
|
||||
var atomTable = proof.AtomTables[0];
|
||||
Assert.Equal(K4Value.Conflict, atomTable.Atoms[SecurityAtom.Reachable].Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Resolution Scenarios
|
||||
|
||||
[Fact]
|
||||
public void AllSourcesAgree_Exploitable_Disposition()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.Present(true)
|
||||
.Applies(true)
|
||||
.Reachable(true)
|
||||
.Build();
|
||||
|
||||
var result = engine.GetDisposition(subject);
|
||||
|
||||
Assert.Equal(Disposition.Exploitable, result.Disposition);
|
||||
Assert.Empty(result.Conflicts);
|
||||
Assert.Empty(result.Unknowns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fixed_Overrides_Exploitability()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
// Initially exploitable
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.Present(true)
|
||||
.Applies(true)
|
||||
.Reachable(true)
|
||||
.Build();
|
||||
|
||||
// Then fixed
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.Fixed(true, "patched in v4.17.22")
|
||||
.Build();
|
||||
|
||||
var result = engine.GetDisposition(subject);
|
||||
|
||||
Assert.Equal(Disposition.ResolvedWithPedigree, result.Disposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Misattributed_Produces_FalsePositive()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.Present(true)
|
||||
.Applies(true)
|
||||
.Misattributed(true, "CVE assigned to wrong package version")
|
||||
.Build();
|
||||
|
||||
var result = engine.GetDisposition(subject);
|
||||
|
||||
Assert.Equal(Disposition.FalsePositive, result.Disposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotReachable_Produces_NotAffected()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.Present(true)
|
||||
.Applies(true)
|
||||
.Reachable(false, "dead code branch")
|
||||
.Build();
|
||||
|
||||
var result = engine.GetDisposition(subject);
|
||||
|
||||
Assert.Equal(Disposition.NotAffected, result.Disposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mitigated_Produces_NotAffected()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.Present(true)
|
||||
.Applies(true)
|
||||
.Reachable(true)
|
||||
.Mitigated(true, "WAF blocks exploit")
|
||||
.Build();
|
||||
|
||||
var result = engine.GetDisposition(subject);
|
||||
|
||||
Assert.Equal(Disposition.NotAffected, result.Disposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsufficientData_Produces_InTriage()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
// No claims at all
|
||||
var state = engine.Store.GetOrCreateSubject(subject);
|
||||
var result = engine.GetDisposition(subject);
|
||||
|
||||
Assert.Equal(Disposition.InTriage, result.Disposition);
|
||||
Assert.Contains(SecurityAtom.Present, result.Unknowns);
|
||||
Assert.Contains(SecurityAtom.Applies, result.Unknowns);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Decision Trace Tests
|
||||
|
||||
[Fact]
|
||||
public void DecisionTrace_ContainsAllEvaluatedRules()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.Present(true)
|
||||
.Applies(true)
|
||||
.Reachable(true)
|
||||
.Build();
|
||||
|
||||
var result = engine.GetDisposition(subject);
|
||||
|
||||
Assert.NotEmpty(result.Trace);
|
||||
Assert.All(result.Trace, step => Assert.NotNull(step.RuleName));
|
||||
Assert.Contains(result.Trace, step => step.Matched);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecisionTrace_FirstMatchWins()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
// Fixed should match before exploitable
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.Present(true)
|
||||
.Applies(true)
|
||||
.Reachable(true)
|
||||
.Fixed(true)
|
||||
.Build();
|
||||
|
||||
var result = engine.GetDisposition(subject);
|
||||
|
||||
// Verify the fixed rule matched first
|
||||
Assert.Equal("fixed_resolved", result.MatchedRule);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Multi-Subject Evaluation
|
||||
|
||||
[Fact]
|
||||
public void MultipleSubjects_EvaluatesAll()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
|
||||
var subject1 = CreateTestSubject("CVE-2024-0001", "pkg:npm/a@1.0.0");
|
||||
var subject2 = CreateTestSubject("CVE-2024-0002", "pkg:npm/b@1.0.0");
|
||||
var subject3 = CreateTestSubject("CVE-2024-0003", "pkg:npm/c@1.0.0");
|
||||
|
||||
// Subject 1: exploitable
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject1)
|
||||
.Present(true).Applies(true).Reachable(true)
|
||||
.Build();
|
||||
|
||||
// Subject 2: fixed
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject2)
|
||||
.Fixed(true)
|
||||
.Build();
|
||||
|
||||
// Subject 3: not present
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject3)
|
||||
.Present(false)
|
||||
.Build();
|
||||
|
||||
var evalResult = engine.Evaluate();
|
||||
|
||||
Assert.True(evalResult.Success);
|
||||
Assert.Equal(3, evalResult.Dispositions.Count);
|
||||
|
||||
Assert.Equal(Disposition.Exploitable, evalResult.Dispositions[subject1.ComputeDigest()].Disposition);
|
||||
Assert.Equal(Disposition.ResolvedWithPedigree, evalResult.Dispositions[subject2.ComputeDigest()].Disposition);
|
||||
Assert.Equal(Disposition.FalsePositive, evalResult.Dispositions[subject3.ComputeDigest()].Disposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofBundle_ContentAddressable()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.Present(true).Applies(true).Reachable(true)
|
||||
.Build();
|
||||
|
||||
var result1 = engine.Evaluate();
|
||||
var result2 = engine.Evaluate();
|
||||
|
||||
// Same inputs should produce same proof bundle ID
|
||||
Assert.Equal(result1.ProofBundle!.Id, result2.ProofBundle!.Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Statistics Tests
|
||||
|
||||
[Fact]
|
||||
public void Stats_ReflectStoreState()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
|
||||
// Add a conflicting subject
|
||||
var conflictSubject = CreateTestSubject("CVE-2024-0001");
|
||||
engine.CreateClaim()
|
||||
.ForSubject(conflictSubject)
|
||||
.Present(true)
|
||||
.Build();
|
||||
engine.CreateClaim()
|
||||
.ForSubject(conflictSubject)
|
||||
.Present(false)
|
||||
.Build();
|
||||
|
||||
// Add an incomplete subject
|
||||
var incompleteSubject = CreateTestSubject("CVE-2024-0002");
|
||||
engine.CreateClaim()
|
||||
.ForSubject(incompleteSubject)
|
||||
.Mitigated(true) // Only mitigated, no PRESENT/APPLIES/REACHABLE
|
||||
.Build();
|
||||
|
||||
var stats = engine.GetStats();
|
||||
|
||||
Assert.Equal(2, stats.SubjectCount);
|
||||
Assert.Equal(3, stats.ClaimCount);
|
||||
Assert.Equal(1, stats.ConflictCount);
|
||||
Assert.Equal(1, stats.IncompleteCount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Engine Clear Tests
|
||||
|
||||
[Fact]
|
||||
public void Clear_ResetsEngine()
|
||||
{
|
||||
var engine = new TrustLatticeEngine();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
engine.CreateClaim()
|
||||
.ForSubject(subject)
|
||||
.Present(true)
|
||||
.Build();
|
||||
|
||||
Assert.Equal(1, engine.GetStats().SubjectCount);
|
||||
|
||||
engine.Clear();
|
||||
|
||||
Assert.Equal(0, engine.GetStats().SubjectCount);
|
||||
Assert.Equal(0, engine.GetStats().ClaimCount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* VEX Normalizer Unit Tests
|
||||
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
|
||||
* Task: TRUST-018
|
||||
*
|
||||
* Tests for VEX format normalization to canonical atoms:
|
||||
* - CycloneDX/ECMA-424 status and justification mappings
|
||||
* - OpenVEX status and justification mappings
|
||||
* - CSAF product status and flag mappings
|
||||
*/
|
||||
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
|
||||
namespace StellaOps.Policy.Tests.TrustLattice;
|
||||
|
||||
public class VexNormalizerTests
|
||||
{
|
||||
private static Subject CreateTestSubject(string vulnId = "CVE-2024-1234")
|
||||
{
|
||||
return new Subject
|
||||
{
|
||||
Artifact = new ArtifactRef
|
||||
{
|
||||
Digest = "sha256:abc123",
|
||||
Name = "test-image:latest",
|
||||
Type = "oci",
|
||||
},
|
||||
Component = new ComponentRef
|
||||
{
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
},
|
||||
Vulnerability = new VulnerabilityRef
|
||||
{
|
||||
Id = vulnId,
|
||||
Source = "NVD",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#region CycloneDX/ECMA-424 Tests
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_Affected_SetsPresent_And_Applies_True()
|
||||
{
|
||||
var normalizer = new CycloneDxVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, CycloneDxVexStatus.Affected);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Present && a.Value == true);
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Applies && a.Value == true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_NotAffected_SetsApplies_False()
|
||||
{
|
||||
var normalizer = new CycloneDxVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, CycloneDxVexStatus.NotAffected);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Applies && a.Value == false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_Fixed_SetsFixed_True()
|
||||
{
|
||||
var normalizer = new CycloneDxVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, CycloneDxVexStatus.Fixed);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Fixed && a.Value == true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_FixAvailable_SetsFixed_False()
|
||||
{
|
||||
var normalizer = new CycloneDxVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, CycloneDxVexStatus.FixAvailable);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Fixed && a.Value == false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_InTriage_ProducesNoAssertions()
|
||||
{
|
||||
var normalizer = new CycloneDxVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, CycloneDxVexStatus.InTriage);
|
||||
|
||||
Assert.Empty(claim.Assertions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_CodeNotPresent_SetsPresent_False()
|
||||
{
|
||||
var normalizer = new CycloneDxVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(
|
||||
subject,
|
||||
CycloneDxVexStatus.NotAffected,
|
||||
CycloneDxJustification.CodeNotPresent);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Present && a.Value == false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_CodeNotReachable_SetsReachable_False()
|
||||
{
|
||||
var normalizer = new CycloneDxVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(
|
||||
subject,
|
||||
CycloneDxVexStatus.NotAffected,
|
||||
CycloneDxJustification.CodeNotReachable);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Reachable && a.Value == false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_ProtectedByMitigatingControl_SetsMitigated_True()
|
||||
{
|
||||
var normalizer = new CycloneDxVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(
|
||||
subject,
|
||||
CycloneDxVexStatus.NotAffected,
|
||||
CycloneDxJustification.ProtectedByMitigatingControl);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Mitigated && a.Value == true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycloneDx_WithDetail_IncludesDetailInJustification()
|
||||
{
|
||||
var normalizer = new CycloneDxVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
const string detail = "WAF blocks this attack vector";
|
||||
|
||||
var claim = normalizer.NormalizeStatement(
|
||||
subject,
|
||||
CycloneDxVexStatus.NotAffected,
|
||||
CycloneDxJustification.ProtectedAtPerimeter,
|
||||
detail);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Justification != null && a.Justification.Contains(detail));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region OpenVEX Tests
|
||||
|
||||
[Fact]
|
||||
public void OpenVex_Affected_SetsPresent_And_Applies_True()
|
||||
{
|
||||
var normalizer = new OpenVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, OpenVexStatus.Affected);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Present && a.Value == true);
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Applies && a.Value == true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenVex_NotAffected_SetsApplies_False()
|
||||
{
|
||||
var normalizer = new OpenVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, OpenVexStatus.NotAffected);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Applies && a.Value == false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenVex_Fixed_SetsFixed_True()
|
||||
{
|
||||
var normalizer = new OpenVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, OpenVexStatus.Fixed);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Fixed && a.Value == true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenVex_UnderInvestigation_ProducesNoAssertions()
|
||||
{
|
||||
var normalizer = new OpenVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, OpenVexStatus.UnderInvestigation);
|
||||
|
||||
Assert.Empty(claim.Assertions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenVex_VulnerableCodeNotInExecutePath_SetsReachable_False()
|
||||
{
|
||||
var normalizer = new OpenVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(
|
||||
subject,
|
||||
OpenVexStatus.NotAffected,
|
||||
OpenVexJustification.VulnerableCodeNotInExecutePath);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Reachable && a.Value == false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenVex_ComponentNotPresent_SetsPresent_False()
|
||||
{
|
||||
var normalizer = new OpenVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(
|
||||
subject,
|
||||
OpenVexStatus.NotAffected,
|
||||
OpenVexJustification.ComponentNotPresent);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Present && a.Value == false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenVex_WithActionAndImpact_IncludesInJustification()
|
||||
{
|
||||
var normalizer = new OpenVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(
|
||||
subject,
|
||||
OpenVexStatus.Affected,
|
||||
OpenVexJustification.None,
|
||||
actionStatement: "Apply patch CVE-2024-1234-fix",
|
||||
impactStatement: "Remote code execution");
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Justification != null && a.Justification.Contains("action:"));
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Justification != null && a.Justification.Contains("impact:"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CSAF Tests
|
||||
|
||||
[Fact]
|
||||
public void Csaf_KnownAffected_SetsPresent_And_Applies_True()
|
||||
{
|
||||
var normalizer = new CsafVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, CsafProductStatus.KnownAffected);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Present && a.Value == true);
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Applies && a.Value == true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Csaf_KnownNotAffected_SetsApplies_False()
|
||||
{
|
||||
var normalizer = new CsafVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, CsafProductStatus.KnownNotAffected);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Applies && a.Value == false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Csaf_Fixed_SetsFixed_True()
|
||||
{
|
||||
var normalizer = new CsafVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, CsafProductStatus.Fixed);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Fixed && a.Value == true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Csaf_UnderInvestigation_ProducesNoAssertions()
|
||||
{
|
||||
var normalizer = new CsafVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(subject, CsafProductStatus.UnderInvestigation);
|
||||
|
||||
Assert.Empty(claim.Assertions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Csaf_VulnerableCodeNotInExecutePath_SetsReachable_False()
|
||||
{
|
||||
var normalizer = new CsafVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(
|
||||
subject,
|
||||
CsafProductStatus.KnownNotAffected,
|
||||
CsafFlagLabel.VulnerableCodeNotInExecutePath);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Reachable && a.Value == false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Csaf_ComponentNotPresent_SetsPresent_False()
|
||||
{
|
||||
var normalizer = new CsafVexNormalizer();
|
||||
var subject = CreateTestSubject();
|
||||
|
||||
var claim = normalizer.NormalizeStatement(
|
||||
subject,
|
||||
CsafProductStatus.KnownNotAffected,
|
||||
CsafFlagLabel.ComponentNotPresent);
|
||||
|
||||
Assert.Contains(claim.Assertions, a =>
|
||||
a.Atom == SecurityAtom.Present && a.Value == false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Format Property Tests
|
||||
|
||||
[Fact]
|
||||
public void CycloneDxNormalizer_Format_IsCorrect()
|
||||
{
|
||||
var normalizer = new CycloneDxVexNormalizer();
|
||||
Assert.Equal("CycloneDX/ECMA-424", normalizer.Format);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenVexNormalizer_Format_IsCorrect()
|
||||
{
|
||||
var normalizer = new OpenVexNormalizer();
|
||||
Assert.Equal("OpenVEX", normalizer.Format);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CsafNormalizer_Format_IsCorrect()
|
||||
{
|
||||
var normalizer = new CsafVexNormalizer();
|
||||
Assert.Equal("CSAF", normalizer.Format);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user