Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
using StellaOps.Findings.Ledger.WebService.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests.Services;
|
||||
|
||||
public class EvidenceGraphBuilderTests
|
||||
{
|
||||
private readonly Mock<IEvidenceRepository> _evidenceRepo = new();
|
||||
private readonly Mock<IAttestationVerifier> _attestationVerifier = new();
|
||||
private readonly EvidenceGraphBuilder _builder;
|
||||
|
||||
public EvidenceGraphBuilderTests()
|
||||
{
|
||||
_builder = new EvidenceGraphBuilder(_evidenceRepo.Object, _attestationVerifier.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_FindingNotFound_ReturnsNull()
|
||||
{
|
||||
_evidenceRepo.Setup(r => r.GetFullEvidenceAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((FullEvidence?)null);
|
||||
|
||||
var result = await _builder.BuildAsync(Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithAllEvidence_ReturnsCompleteGraph()
|
||||
{
|
||||
var evidence = CreateFullEvidence();
|
||||
_evidenceRepo.Setup(r => r.GetFullEvidenceAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(evidence);
|
||||
_attestationVerifier.Setup(v => v.VerifyAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AttestationVerificationResult
|
||||
{
|
||||
IsValid = true,
|
||||
SignerIdentity = "test-signer",
|
||||
SignedAt = DateTimeOffset.UtcNow,
|
||||
KeyId = "key-123",
|
||||
RekorLogIndex = 12345
|
||||
});
|
||||
|
||||
var findingId = Guid.NewGuid();
|
||||
var result = await _builder.BuildAsync(findingId, CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.FindingId.Should().Be(findingId);
|
||||
result.VulnerabilityId.Should().Be("CVE-2024-1234");
|
||||
result.Nodes.Should().HaveCountGreaterThan(1);
|
||||
result.Edges.Should().NotBeEmpty();
|
||||
result.RootNodeId.Should().NotBeNullOrEmpty();
|
||||
result.RootNodeId.Should().StartWith("verdict:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SignedAttestation_IncludesSignatureStatus()
|
||||
{
|
||||
var evidence = CreateEvidenceWithSignedAttestation();
|
||||
_evidenceRepo.Setup(r => r.GetFullEvidenceAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(evidence);
|
||||
_attestationVerifier.Setup(v => v.VerifyAsync("attestation-digest-123", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AttestationVerificationResult
|
||||
{
|
||||
IsValid = true,
|
||||
SignerIdentity = "trusted-signer@example.com",
|
||||
SignedAt = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
KeyId = "key-abc",
|
||||
RekorLogIndex = 54321
|
||||
});
|
||||
|
||||
var result = await _builder.BuildAsync(Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
var signedNode = result!.Nodes.FirstOrDefault(n => n.Signature.IsSigned);
|
||||
signedNode.Should().NotBeNull();
|
||||
signedNode!.Signature.IsValid.Should().BeTrue();
|
||||
signedNode.Signature.SignerIdentity.Should().Be("trusted-signer@example.com");
|
||||
signedNode.Signature.KeyId.Should().Be("key-abc");
|
||||
signedNode.Signature.RekorLogIndex.Should().Be(54321);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_EdgeRelationships_CorrectlyLinked()
|
||||
{
|
||||
var evidence = CreateFullEvidence();
|
||||
_evidenceRepo.Setup(r => r.GetFullEvidenceAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(evidence);
|
||||
_attestationVerifier.Setup(v => v.VerifyAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AttestationVerificationResult { IsValid = true });
|
||||
|
||||
var result = await _builder.BuildAsync(Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
|
||||
// Verify policy trace edge
|
||||
var policyEdge = result!.Edges.FirstOrDefault(e => e.Label == "policy evaluation");
|
||||
policyEdge.Should().NotBeNull();
|
||||
policyEdge!.Relation.Should().Be(EvidenceRelation.DerivedFrom);
|
||||
policyEdge.To.Should().Be(result.RootNodeId);
|
||||
|
||||
// Verify VEX edge
|
||||
var vexEdge = result.Edges.FirstOrDefault(e => e.Label == "affected");
|
||||
vexEdge.Should().NotBeNull();
|
||||
vexEdge!.Relation.Should().Be(EvidenceRelation.DerivedFrom);
|
||||
|
||||
// Verify reachability edge
|
||||
var reachEdge = result.Edges.FirstOrDefault(e => e.Label == "reachability analysis");
|
||||
reachEdge.Should().NotBeNull();
|
||||
reachEdge!.Relation.Should().Be(EvidenceRelation.Corroborates);
|
||||
|
||||
// Verify runtime edge
|
||||
var runtimeEdge = result.Edges.FirstOrDefault(e => e.Label == "runtime observation");
|
||||
runtimeEdge.Should().NotBeNull();
|
||||
runtimeEdge!.Relation.Should().Be(EvidenceRelation.Corroborates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_NodeTypes_CorrectlyAssigned()
|
||||
{
|
||||
var evidence = CreateFullEvidence();
|
||||
_evidenceRepo.Setup(r => r.GetFullEvidenceAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(evidence);
|
||||
_attestationVerifier.Setup(v => v.VerifyAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AttestationVerificationResult { IsValid = true });
|
||||
|
||||
var result = await _builder.BuildAsync(Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Nodes.Should().Contain(n => n.Type == EvidenceNodeType.Verdict);
|
||||
result.Nodes.Should().Contain(n => n.Type == EvidenceNodeType.PolicyTrace);
|
||||
result.Nodes.Should().Contain(n => n.Type == EvidenceNodeType.VexStatement);
|
||||
result.Nodes.Should().Contain(n => n.Type == EvidenceNodeType.Reachability);
|
||||
result.Nodes.Should().Contain(n => n.Type == EvidenceNodeType.RuntimeObservation);
|
||||
result.Nodes.Should().Contain(n => n.Type == EvidenceNodeType.SbomComponent);
|
||||
result.Nodes.Should().Contain(n => n.Type == EvidenceNodeType.Provenance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_MinimalEvidence_CreatesVerdictOnly()
|
||||
{
|
||||
var evidence = CreateMinimalEvidence();
|
||||
_evidenceRepo.Setup(r => r.GetFullEvidenceAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(evidence);
|
||||
|
||||
var result = await _builder.BuildAsync(Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Nodes.Should().HaveCount(1);
|
||||
result.Nodes[0].Type.Should().Be(EvidenceNodeType.Verdict);
|
||||
result.Edges.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_UnsignedEvidence_HasUnsignedStatus()
|
||||
{
|
||||
var evidence = CreateMinimalEvidence();
|
||||
_evidenceRepo.Setup(r => r.GetFullEvidenceAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(evidence);
|
||||
|
||||
var result = await _builder.BuildAsync(Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Nodes.Should().AllSatisfy(n =>
|
||||
{
|
||||
if (n.Type == EvidenceNodeType.Verdict || n.Type == EvidenceNodeType.SbomComponent)
|
||||
{
|
||||
n.Signature.IsSigned.Should().BeFalse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_NodeMetadata_PopulatedCorrectly()
|
||||
{
|
||||
var evidence = CreateFullEvidence();
|
||||
_evidenceRepo.Setup(r => r.GetFullEvidenceAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(evidence);
|
||||
_attestationVerifier.Setup(v => v.VerifyAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AttestationVerificationResult { IsValid = true });
|
||||
|
||||
var result = await _builder.BuildAsync(Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
|
||||
var policyNode = result!.Nodes.First(n => n.Type == EvidenceNodeType.PolicyTrace);
|
||||
policyNode.Metadata.Should().ContainKey("policyName");
|
||||
policyNode.Metadata["policyName"].Should().Be("security-baseline");
|
||||
|
||||
var vexNode = result.Nodes.First(n => n.Type == EvidenceNodeType.VexStatement);
|
||||
vexNode.Metadata.Should().ContainKey("status");
|
||||
vexNode.Metadata["status"].Should().Be("affected");
|
||||
|
||||
var sbomNode = result.Nodes.First(n => n.Type == EvidenceNodeType.SbomComponent);
|
||||
sbomNode.Metadata.Should().ContainKey("purl");
|
||||
sbomNode.Metadata["purl"].Should().Be("pkg:npm/lodash@4.17.20");
|
||||
}
|
||||
|
||||
private static FullEvidence CreateFullEvidence()
|
||||
{
|
||||
return new FullEvidence
|
||||
{
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
Verdict = new VerdictEvidence
|
||||
{
|
||||
Status = "Affected",
|
||||
Digest = "sha256:verdict123",
|
||||
Issuer = "stellaops",
|
||||
Timestamp = DateTimeOffset.UtcNow.AddDays(-1)
|
||||
},
|
||||
PolicyTrace = new PolicyTraceEvidence
|
||||
{
|
||||
PolicyName = "security-baseline",
|
||||
PolicyVersion = "v1.0.0",
|
||||
Digest = "sha256:policy123",
|
||||
Issuer = "stellaops",
|
||||
Timestamp = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
AttestationDigest = "attestation-policy-123"
|
||||
},
|
||||
VexStatements = new[]
|
||||
{
|
||||
new VexEvidence
|
||||
{
|
||||
Status = "affected",
|
||||
Justification = "vulnerable_code_path_reachable",
|
||||
Digest = "sha256:vex123",
|
||||
Issuer = "vendor",
|
||||
Timestamp = DateTimeOffset.UtcNow.AddDays(-2),
|
||||
AttestationDigest = "attestation-vex-123"
|
||||
}
|
||||
},
|
||||
Reachability = new ReachabilityEvidence
|
||||
{
|
||||
State = "StaticReachable",
|
||||
Confidence = 0.92m,
|
||||
Digest = "sha256:reach123",
|
||||
Issuer = "stellaops",
|
||||
Timestamp = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
AttestationDigest = "attestation-reach-123"
|
||||
},
|
||||
RuntimeObservations = new[]
|
||||
{
|
||||
new RuntimeEvidence
|
||||
{
|
||||
ObservationType = "ComponentLoaded",
|
||||
DurationMinutes = 120,
|
||||
Digest = "sha256:runtime123",
|
||||
Issuer = "stellaops",
|
||||
Timestamp = DateTimeOffset.UtcNow.AddHours(-2),
|
||||
AttestationDigest = "attestation-runtime-123"
|
||||
}
|
||||
},
|
||||
SbomComponent = new SbomComponentEvidence
|
||||
{
|
||||
ComponentName = "lodash",
|
||||
Purl = "pkg:npm/lodash@4.17.20",
|
||||
Version = "4.17.20",
|
||||
Digest = "sha256:sbom123",
|
||||
Issuer = "stellaops",
|
||||
Timestamp = DateTimeOffset.UtcNow.AddDays(-3)
|
||||
},
|
||||
Provenance = new ProvenanceEvidence
|
||||
{
|
||||
BuilderType = "github-actions",
|
||||
RepoUrl = "https://github.com/lodash/lodash",
|
||||
Digest = "sha256:prov123",
|
||||
Issuer = "github",
|
||||
Timestamp = DateTimeOffset.UtcNow.AddDays(-30),
|
||||
AttestationDigest = "attestation-prov-123"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static FullEvidence CreateEvidenceWithSignedAttestation()
|
||||
{
|
||||
return new FullEvidence
|
||||
{
|
||||
VulnerabilityId = "CVE-2024-5678",
|
||||
Verdict = new VerdictEvidence
|
||||
{
|
||||
Status = "Affected",
|
||||
Digest = "sha256:verdict456",
|
||||
Issuer = "stellaops",
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
},
|
||||
VexStatements = new[]
|
||||
{
|
||||
new VexEvidence
|
||||
{
|
||||
Status = "affected",
|
||||
Digest = "sha256:vex456",
|
||||
Issuer = "vendor",
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
AttestationDigest = "attestation-digest-123"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static FullEvidence CreateMinimalEvidence()
|
||||
{
|
||||
return new FullEvidence
|
||||
{
|
||||
VulnerabilityId = "CVE-2024-9999",
|
||||
Verdict = new VerdictEvidence
|
||||
{
|
||||
Status = "UnderReview",
|
||||
Digest = "sha256:verdict999",
|
||||
Issuer = "stellaops",
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
using StellaOps.Findings.Ledger.WebService.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests.Services;
|
||||
|
||||
public class FindingSummaryBuilderTests
|
||||
{
|
||||
private readonly FindingSummaryBuilder _builder = new();
|
||||
|
||||
[Fact]
|
||||
public void Build_AffectedFinding_ReturnsRedChip()
|
||||
{
|
||||
var finding = CreateFinding(isAffected: true);
|
||||
|
||||
var result = _builder.Build(finding);
|
||||
|
||||
result.Status.Should().Be(VerdictStatus.Affected);
|
||||
result.Chip.Color.Should().Be(ChipColor.Red);
|
||||
result.Chip.Label.Should().Be("AFFECTED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NotAffectedFinding_ReturnsGreenChip()
|
||||
{
|
||||
var finding = CreateFinding(isAffected: false);
|
||||
|
||||
var result = _builder.Build(finding);
|
||||
|
||||
result.Status.Should().Be(VerdictStatus.NotAffected);
|
||||
result.Chip.Color.Should().Be(ChipColor.Green);
|
||||
result.Chip.Label.Should().Be("NOT AFFECTED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_MitigatedFinding_ReturnsBlueChip()
|
||||
{
|
||||
var finding = CreateFinding(isAffected: true, isMitigated: true);
|
||||
|
||||
var result = _builder.Build(finding);
|
||||
|
||||
result.Status.Should().Be(VerdictStatus.Mitigated);
|
||||
result.Chip.Color.Should().Be(ChipColor.Blue);
|
||||
result.Chip.Label.Should().Be("MITIGATED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_UnknownStatus_ReturnsYellowChip()
|
||||
{
|
||||
var finding = CreateFinding(isAffected: null);
|
||||
|
||||
var result = _builder.Build(finding);
|
||||
|
||||
result.Status.Should().Be(VerdictStatus.UnderReview);
|
||||
result.Chip.Color.Should().Be(ChipColor.Yellow);
|
||||
result.Chip.Label.Should().Be("REVIEW NEEDED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_ReachableVulnerability_GeneratesAppropriateOneLiner()
|
||||
{
|
||||
var finding = CreateFinding(isAffected: true, isReachable: true);
|
||||
|
||||
var result = _builder.Build(finding);
|
||||
|
||||
result.OneLiner.Should().Contain("reachable");
|
||||
result.OneLiner.Should().Contain("actively used");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithCallGraph_StrongReachabilityBadge()
|
||||
{
|
||||
var finding = CreateFinding(isAffected: true, isReachable: true, hasCallGraph: true);
|
||||
|
||||
var result = _builder.Build(finding);
|
||||
|
||||
result.Badges.Reachability.Should().Be(BadgeStatus.Strong);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithRuntimeEvidence_StrongRuntimeBadge()
|
||||
{
|
||||
var finding = CreateFinding(isAffected: true, hasRuntimeEvidence: true);
|
||||
|
||||
var result = _builder.Build(finding);
|
||||
|
||||
result.Badges.Runtime.Should().Be(BadgeStatus.Strong);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithVerifiedRuntime_VerifiedBadge()
|
||||
{
|
||||
var finding = CreateFinding(
|
||||
isAffected: true,
|
||||
hasRuntimeEvidence: true,
|
||||
runtimeConfirmed: true);
|
||||
|
||||
var result = _builder.Build(finding);
|
||||
|
||||
result.Badges.Runtime.Should().Be(BadgeStatus.Verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithAttestation_StrongProvenanceBadge()
|
||||
{
|
||||
var finding = CreateFinding(isAffected: true, hasAttestation: true);
|
||||
|
||||
var result = _builder.Build(finding);
|
||||
|
||||
result.Badges.Provenance.Should().Be(BadgeStatus.Strong);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithVerifiedAttestation_VerifiedProvenanceBadge()
|
||||
{
|
||||
var finding = CreateFinding(
|
||||
isAffected: true,
|
||||
hasAttestation: true,
|
||||
attestationVerified: true);
|
||||
|
||||
var result = _builder.Build(finding);
|
||||
|
||||
result.Badges.Provenance.Should().Be(BadgeStatus.Verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_CopiesAllBasicFields()
|
||||
{
|
||||
var finding = CreateFinding(isAffected: true);
|
||||
|
||||
var result = _builder.Build(finding);
|
||||
|
||||
result.FindingId.Should().Be(finding.Id);
|
||||
result.VulnerabilityId.Should().Be(finding.VulnerabilityId);
|
||||
result.Component.Should().Be(finding.ComponentPurl);
|
||||
result.Confidence.Should().Be(finding.Confidence);
|
||||
result.CvssScore.Should().Be(finding.CvssScore);
|
||||
result.Severity.Should().Be(finding.Severity);
|
||||
}
|
||||
|
||||
private static FindingData CreateFinding(
|
||||
bool? isAffected = true,
|
||||
bool isMitigated = false,
|
||||
bool? isReachable = null,
|
||||
bool hasCallGraph = false,
|
||||
bool hasRuntimeEvidence = false,
|
||||
bool runtimeConfirmed = false,
|
||||
bool hasAttestation = false,
|
||||
bool attestationVerified = false)
|
||||
{
|
||||
return new FindingData
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
Title = "Test Vulnerability",
|
||||
ComponentPurl = "pkg:npm/test-package@1.0.0",
|
||||
IsAffected = isAffected,
|
||||
IsMitigated = isMitigated,
|
||||
MitigationReason = isMitigated ? "Test mitigation" : null,
|
||||
Confidence = 0.85m,
|
||||
IsReachable = isReachable,
|
||||
HasCallGraph = hasCallGraph,
|
||||
HasRuntimeEvidence = hasRuntimeEvidence,
|
||||
RuntimeConfirmed = runtimeConfirmed,
|
||||
HasPolicyEvaluation = false,
|
||||
PolicyPassed = false,
|
||||
HasAttestation = hasAttestation,
|
||||
AttestationVerified = attestationVerified,
|
||||
CvssScore = 7.5m,
|
||||
Severity = "High",
|
||||
FirstSeen = DateTimeOffset.UtcNow.AddDays(-7),
|
||||
LastUpdated = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Findings.Ledger\StellaOps.Findings.Ledger.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Findings.Ledger.WebService\StellaOps.Findings.Ledger.WebService.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
@@ -17,5 +18,6 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user