Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Services;
|
||||
|
||||
public interface IEvidenceGraphBuilder
|
||||
{
|
||||
Task<EvidenceGraphResponse?> BuildAsync(Guid findingId, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed class EvidenceGraphBuilder : IEvidenceGraphBuilder
|
||||
{
|
||||
private readonly IEvidenceRepository _evidenceRepo;
|
||||
private readonly IAttestationVerifier _attestationVerifier;
|
||||
|
||||
public EvidenceGraphBuilder(
|
||||
IEvidenceRepository evidenceRepo,
|
||||
IAttestationVerifier attestationVerifier)
|
||||
{
|
||||
_evidenceRepo = evidenceRepo;
|
||||
_attestationVerifier = attestationVerifier;
|
||||
}
|
||||
|
||||
public async Task<EvidenceGraphResponse?> BuildAsync(
|
||||
Guid findingId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var evidence = await _evidenceRepo.GetFullEvidenceAsync(findingId, ct);
|
||||
if (evidence is null)
|
||||
return null;
|
||||
|
||||
var nodes = new List<EvidenceNode>();
|
||||
var edges = new List<EvidenceEdge>();
|
||||
|
||||
// Build verdict node (root)
|
||||
var verdictNode = BuildVerdictNode(evidence.Verdict);
|
||||
nodes.Add(verdictNode);
|
||||
|
||||
// Build policy trace node
|
||||
if (evidence.PolicyTrace is not null)
|
||||
{
|
||||
var policyNode = await BuildPolicyNodeAsync(evidence.PolicyTrace, ct);
|
||||
nodes.Add(policyNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = policyNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.DerivedFrom,
|
||||
Label = "policy evaluation"
|
||||
});
|
||||
}
|
||||
|
||||
// Build VEX nodes
|
||||
foreach (var vex in evidence.VexStatements)
|
||||
{
|
||||
var vexNode = await BuildVexNodeAsync(vex, ct);
|
||||
nodes.Add(vexNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = vexNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.DerivedFrom,
|
||||
Label = vex.Status.ToLowerInvariant()
|
||||
});
|
||||
}
|
||||
|
||||
// Build reachability node
|
||||
if (evidence.Reachability is not null)
|
||||
{
|
||||
var reachNode = await BuildReachabilityNodeAsync(evidence.Reachability, ct);
|
||||
nodes.Add(reachNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = reachNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.Corroborates,
|
||||
Label = "reachability analysis"
|
||||
});
|
||||
}
|
||||
|
||||
// Build runtime nodes
|
||||
foreach (var runtime in evidence.RuntimeObservations)
|
||||
{
|
||||
var runtimeNode = await BuildRuntimeNodeAsync(runtime, ct);
|
||||
nodes.Add(runtimeNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = runtimeNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.Corroborates,
|
||||
Label = "runtime observation"
|
||||
});
|
||||
}
|
||||
|
||||
// Build SBOM node
|
||||
if (evidence.SbomComponent is not null)
|
||||
{
|
||||
var sbomNode = BuildSbomNode(evidence.SbomComponent);
|
||||
nodes.Add(sbomNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = sbomNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.References,
|
||||
Label = "component"
|
||||
});
|
||||
}
|
||||
|
||||
// Build provenance node
|
||||
if (evidence.Provenance is not null)
|
||||
{
|
||||
var provNode = await BuildProvenanceNodeAsync(evidence.Provenance, ct);
|
||||
nodes.Add(provNode);
|
||||
edges.Add(new EvidenceEdge
|
||||
{
|
||||
From = provNode.Id,
|
||||
To = verdictNode.Id,
|
||||
Relation = EvidenceRelation.VerifiedBy,
|
||||
Label = "provenance"
|
||||
});
|
||||
}
|
||||
|
||||
return new EvidenceGraphResponse
|
||||
{
|
||||
FindingId = findingId,
|
||||
VulnerabilityId = evidence.VulnerabilityId,
|
||||
Nodes = nodes,
|
||||
Edges = edges,
|
||||
RootNodeId = verdictNode.Id,
|
||||
GeneratedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private EvidenceNode BuildVerdictNode(VerdictEvidence verdict)
|
||||
{
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"verdict:{verdict.Digest}",
|
||||
Type = EvidenceNodeType.Verdict,
|
||||
Label = $"Verdict: {verdict.Status}",
|
||||
Digest = verdict.Digest,
|
||||
Issuer = verdict.Issuer,
|
||||
Timestamp = verdict.Timestamp,
|
||||
Signature = new SignatureStatus { IsSigned = false },
|
||||
ContentUrl = $"/api/v1/evidence/{verdict.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<EvidenceNode> BuildPolicyNodeAsync(
|
||||
PolicyTraceEvidence policy,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signature = await VerifySignatureAsync(policy.AttestationDigest, ct);
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"policy:{policy.Digest}",
|
||||
Type = EvidenceNodeType.PolicyTrace,
|
||||
Label = $"Policy: {policy.PolicyName}",
|
||||
Digest = policy.Digest,
|
||||
Issuer = policy.Issuer,
|
||||
Timestamp = policy.Timestamp,
|
||||
Signature = signature,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["policyName"] = policy.PolicyName,
|
||||
["policyVersion"] = policy.PolicyVersion
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{policy.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<EvidenceNode> BuildVexNodeAsync(
|
||||
VexEvidence vex,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signature = await VerifySignatureAsync(vex.AttestationDigest, ct);
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"vex:{vex.Digest}",
|
||||
Type = EvidenceNodeType.VexStatement,
|
||||
Label = $"VEX: {vex.Status}",
|
||||
Digest = vex.Digest,
|
||||
Issuer = vex.Issuer,
|
||||
Timestamp = vex.Timestamp,
|
||||
Signature = signature,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["status"] = vex.Status,
|
||||
["justification"] = vex.Justification ?? string.Empty
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{vex.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<EvidenceNode> BuildReachabilityNodeAsync(
|
||||
ReachabilityEvidence reach,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signature = await VerifySignatureAsync(reach.AttestationDigest, ct);
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"reach:{reach.Digest}",
|
||||
Type = EvidenceNodeType.Reachability,
|
||||
Label = $"Reachability: {reach.State}",
|
||||
Digest = reach.Digest,
|
||||
Issuer = reach.Issuer,
|
||||
Timestamp = reach.Timestamp,
|
||||
Signature = signature,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["state"] = reach.State,
|
||||
["confidence"] = reach.Confidence.ToString("F2")
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{reach.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<EvidenceNode> BuildRuntimeNodeAsync(
|
||||
RuntimeEvidence runtime,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signature = await VerifySignatureAsync(runtime.AttestationDigest, ct);
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"runtime:{runtime.Digest}",
|
||||
Type = EvidenceNodeType.RuntimeObservation,
|
||||
Label = $"Runtime: {runtime.ObservationType}",
|
||||
Digest = runtime.Digest,
|
||||
Issuer = runtime.Issuer,
|
||||
Timestamp = runtime.Timestamp,
|
||||
Signature = signature,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["observationType"] = runtime.ObservationType,
|
||||
["durationMinutes"] = runtime.DurationMinutes.ToString()
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{runtime.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private EvidenceNode BuildSbomNode(SbomComponentEvidence sbom)
|
||||
{
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"sbom:{sbom.Digest}",
|
||||
Type = EvidenceNodeType.SbomComponent,
|
||||
Label = $"Component: {sbom.ComponentName}",
|
||||
Digest = sbom.Digest,
|
||||
Issuer = sbom.Issuer,
|
||||
Timestamp = sbom.Timestamp,
|
||||
Signature = new SignatureStatus { IsSigned = false },
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["purl"] = sbom.Purl,
|
||||
["version"] = sbom.Version
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{sbom.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<EvidenceNode> BuildProvenanceNodeAsync(
|
||||
ProvenanceEvidence prov,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signature = await VerifySignatureAsync(prov.AttestationDigest, ct);
|
||||
return new EvidenceNode
|
||||
{
|
||||
Id = $"prov:{prov.Digest}",
|
||||
Type = EvidenceNodeType.Provenance,
|
||||
Label = $"Provenance: {prov.BuilderType}",
|
||||
Digest = prov.Digest,
|
||||
Issuer = prov.Issuer,
|
||||
Timestamp = prov.Timestamp,
|
||||
Signature = signature,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["builderType"] = prov.BuilderType,
|
||||
["repoUrl"] = prov.RepoUrl ?? string.Empty
|
||||
},
|
||||
ContentUrl = $"/api/v1/evidence/{prov.Digest}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<SignatureStatus> VerifySignatureAsync(
|
||||
string? attestationDigest,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (attestationDigest is null)
|
||||
{
|
||||
return new SignatureStatus { IsSigned = false };
|
||||
}
|
||||
|
||||
var result = await _attestationVerifier.VerifyAsync(attestationDigest, ct);
|
||||
return new SignatureStatus
|
||||
{
|
||||
IsSigned = true,
|
||||
IsValid = result.IsValid,
|
||||
SignerIdentity = result.SignerIdentity,
|
||||
SignedAt = result.SignedAt,
|
||||
KeyId = result.KeyId,
|
||||
RekorLogIndex = result.RekorLogIndex
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repository for evidence retrieval.
|
||||
/// </summary>
|
||||
public interface IEvidenceRepository
|
||||
{
|
||||
Task<FullEvidence?> GetFullEvidenceAsync(Guid findingId, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for attestation verification.
|
||||
/// </summary>
|
||||
public interface IAttestationVerifier
|
||||
{
|
||||
Task<AttestationVerificationResult> VerifyAsync(string digest, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full evidence bundle for a finding.
|
||||
/// </summary>
|
||||
public sealed record FullEvidence
|
||||
{
|
||||
public required string VulnerabilityId { get; init; }
|
||||
public required VerdictEvidence Verdict { get; init; }
|
||||
public PolicyTraceEvidence? PolicyTrace { get; init; }
|
||||
public IReadOnlyList<VexEvidence> VexStatements { get; init; } = Array.Empty<VexEvidence>();
|
||||
public ReachabilityEvidence? Reachability { get; init; }
|
||||
public IReadOnlyList<RuntimeEvidence> RuntimeObservations { get; init; } = Array.Empty<RuntimeEvidence>();
|
||||
public SbomComponentEvidence? SbomComponent { get; init; }
|
||||
public ProvenanceEvidence? Provenance { get; init; }
|
||||
}
|
||||
|
||||
public sealed record VerdictEvidence
|
||||
{
|
||||
public required string Status { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PolicyTraceEvidence
|
||||
{
|
||||
public required string PolicyName { get; init; }
|
||||
public required string PolicyVersion { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public sealed record VexEvidence
|
||||
{
|
||||
public required string Status { get; init; }
|
||||
public string? Justification { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ReachabilityEvidence
|
||||
{
|
||||
public required string State { get; init; }
|
||||
public required decimal Confidence { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RuntimeEvidence
|
||||
{
|
||||
public required string ObservationType { get; init; }
|
||||
public required int DurationMinutes { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SbomComponentEvidence
|
||||
{
|
||||
public required string ComponentName { get; init; }
|
||||
public required string Purl { get; init; }
|
||||
public required string Version { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ProvenanceEvidence
|
||||
{
|
||||
public required string BuilderType { get; init; }
|
||||
public string? RepoUrl { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public sealed record AttestationVerificationResult
|
||||
{
|
||||
public required bool IsValid { get; init; }
|
||||
public string? SignerIdentity { get; init; }
|
||||
public DateTimeOffset? SignedAt { get; init; }
|
||||
public string? KeyId { get; init; }
|
||||
public long? RekorLogIndex { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user