part #2
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
using StellaOps.Evidence.Bundle;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class EvidenceBundleAdapter
|
||||
{
|
||||
// Sprint: SPRINT_20260112_008_LB_binary_diff_evidence_models (BINDIFF-LB-003)
|
||||
private static IEvidence ConvertBinaryDiff(
|
||||
BinaryDiffEvidence binaryDiff,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new BinaryDiffPayload
|
||||
{
|
||||
Hash = binaryDiff.Hash,
|
||||
DiffType = binaryDiff.DiffType.ToString(),
|
||||
PreviousBinaryDigest = binaryDiff.PreviousBinaryDigest,
|
||||
CurrentBinaryDigest = binaryDiff.CurrentBinaryDigest,
|
||||
BinaryFormat = binaryDiff.BinaryFormat,
|
||||
ToolVersion = binaryDiff.ToolVersion,
|
||||
SimilarityScore = binaryDiff.SimilarityScore,
|
||||
FunctionChangeCount = binaryDiff.FunctionChanges.Length,
|
||||
SymbolChangeCount = binaryDiff.SymbolChanges.Length,
|
||||
SectionChangeCount = binaryDiff.SectionChanges.Length,
|
||||
SecurityChangeCount = binaryDiff.SecurityChanges.Length,
|
||||
HasSemanticDiff = binaryDiff.SemanticDiff is not null,
|
||||
SemanticSimilarity = binaryDiff.SemanticDiff?.Similarity
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Artifact, payload, provenance, SchemaVersions.BinaryDiff);
|
||||
}
|
||||
|
||||
internal sealed record BinaryDiffPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? DiffType { get; init; }
|
||||
public string? PreviousBinaryDigest { get; init; }
|
||||
public string? CurrentBinaryDigest { get; init; }
|
||||
public string? BinaryFormat { get; init; }
|
||||
public string? ToolVersion { get; init; }
|
||||
public double? SimilarityScore { get; init; }
|
||||
public int FunctionChangeCount { get; init; }
|
||||
public int SymbolChangeCount { get; init; }
|
||||
public int SectionChangeCount { get; init; }
|
||||
public int SecurityChangeCount { get; init; }
|
||||
public bool HasSemanticDiff { get; init; }
|
||||
public double? SemanticSimilarity { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using StellaOps.Evidence.Bundle;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class EvidenceBundleAdapter
|
||||
{
|
||||
private static IEvidence ConvertCallStack(
|
||||
CallStackEvidence callStack,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new CallStackPayload
|
||||
{
|
||||
Hash = callStack.Hash,
|
||||
SinkFrameIndex = callStack.SinkFrameIndex,
|
||||
SourceFrameIndex = callStack.SourceFrameIndex,
|
||||
Frames = callStack.Frames?.Select(f => new StackFramePayload
|
||||
{
|
||||
FunctionName = f.FunctionName,
|
||||
FilePath = f.FilePath,
|
||||
Line = f.Line,
|
||||
Column = f.Column,
|
||||
IsSink = f.IsSink,
|
||||
IsSource = f.IsSource
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Runtime, payload, provenance, SchemaVersions.CallStack);
|
||||
}
|
||||
|
||||
internal sealed record CallStackPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public int? SinkFrameIndex { get; init; }
|
||||
public int? SourceFrameIndex { get; init; }
|
||||
public IReadOnlyList<StackFramePayload>? Frames { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record StackFramePayload
|
||||
{
|
||||
public required string FunctionName { get; init; }
|
||||
public required string FilePath { get; init; }
|
||||
public required int Line { get; init; }
|
||||
public int? Column { get; init; }
|
||||
public bool IsSink { get; init; }
|
||||
public bool IsSource { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using StellaOps.Evidence.Bundle;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class EvidenceBundleAdapter
|
||||
{
|
||||
private static IEvidence ConvertDiff(
|
||||
DiffEvidence diff,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new DiffPayload
|
||||
{
|
||||
Hash = diff.Hash,
|
||||
DiffType = diff.DiffType.ToString(),
|
||||
PreviousScanId = diff.PreviousScanId,
|
||||
PreviousScanTime = diff.PreviousScanTime,
|
||||
Entries = diff.Entries?.Select(e => new DiffEntryPayload
|
||||
{
|
||||
Operation = e.Operation.ToString(),
|
||||
Path = e.Path,
|
||||
OldValue = e.OldValue,
|
||||
NewValue = e.NewValue,
|
||||
ComponentPurl = e.ComponentPurl
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Artifact, payload, provenance, SchemaVersions.Diff);
|
||||
}
|
||||
|
||||
internal sealed record DiffPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? DiffType { get; init; }
|
||||
public string? PreviousScanId { get; init; }
|
||||
public DateTimeOffset? PreviousScanTime { get; init; }
|
||||
public IReadOnlyList<DiffEntryPayload>? Entries { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record DiffEntryPayload
|
||||
{
|
||||
public required string Operation { get; init; }
|
||||
public required string Path { get; init; }
|
||||
public string? OldValue { get; init; }
|
||||
public string? NewValue { get; init; }
|
||||
public string? ComponentPurl { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using StellaOps.Evidence.Bundle;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class EvidenceBundleAdapter
|
||||
{
|
||||
private static IEvidence ConvertGraphRevision(
|
||||
GraphRevisionEvidence graphRevision,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new GraphRevisionPayload
|
||||
{
|
||||
Hash = graphRevision.Hash,
|
||||
RevisionId = graphRevision.GraphRevisionId,
|
||||
VerdictReceipt = graphRevision.VerdictReceipt,
|
||||
GraphComputedAt = graphRevision.GraphComputedAt,
|
||||
NodeCount = graphRevision.TotalNodes,
|
||||
EdgeCount = graphRevision.TotalEdges
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Dependency, payload, provenance, SchemaVersions.GraphRevision);
|
||||
}
|
||||
|
||||
internal sealed record GraphRevisionPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? RevisionId { get; init; }
|
||||
public string? VerdictReceipt { get; init; }
|
||||
public DateTimeOffset? GraphComputedAt { get; init; }
|
||||
public int? NodeCount { get; init; }
|
||||
public int? EdgeCount { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using StellaOps.Evidence.Bundle;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class EvidenceBundleAdapter
|
||||
{
|
||||
private static IEvidence ConvertProvenance(
|
||||
ProvenanceEvidence provenanceEvidence,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new ProvenancePayload
|
||||
{
|
||||
Hash = provenanceEvidence.Hash,
|
||||
BuilderId = provenanceEvidence.Ancestry?.BuildId,
|
||||
BuildTime = provenanceEvidence.Ancestry?.BuildTime,
|
||||
ImageDigest = provenanceEvidence.Ancestry?.ImageDigest,
|
||||
LayerDigest = provenanceEvidence.Ancestry?.LayerDigest,
|
||||
CommitHash = provenanceEvidence.Ancestry?.CommitHash,
|
||||
VerificationStatus = provenanceEvidence.VerificationStatus,
|
||||
RekorLogIndex = provenanceEvidence.RekorEntry?.LogIndex
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Provenance, payload, provenance, SchemaVersions.Provenance);
|
||||
}
|
||||
|
||||
internal sealed record ProvenancePayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? BuilderId { get; init; }
|
||||
public DateTimeOffset? BuildTime { get; init; }
|
||||
public string? ImageDigest { get; init; }
|
||||
public string? LayerDigest { get; init; }
|
||||
public string? CommitHash { get; init; }
|
||||
public string? VerificationStatus { get; init; }
|
||||
public long? RekorLogIndex { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using StellaOps.Evidence.Bundle;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class EvidenceBundleAdapter
|
||||
{
|
||||
private static IEvidence ConvertReachability(
|
||||
ReachabilityEvidence reachability,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new ReachabilityPayload
|
||||
{
|
||||
Hash = reachability.Hash,
|
||||
ProofType = reachability.ProofType.ToString(),
|
||||
FunctionPath = reachability.FunctionPath?.Select(f => new FunctionPathPayload
|
||||
{
|
||||
FunctionName = f.FunctionName,
|
||||
FilePath = f.FilePath,
|
||||
Line = f.Line,
|
||||
Column = f.Column,
|
||||
ModuleName = f.ModuleName
|
||||
}).ToList(),
|
||||
ImportChain = reachability.ImportChain?.Select(i => new ImportChainPayload
|
||||
{
|
||||
PackageName = i.PackageName,
|
||||
Version = i.Version,
|
||||
ImportedBy = i.ImportedBy,
|
||||
ImportPath = i.ImportPath
|
||||
}).ToList(),
|
||||
LatticeState = reachability.LatticeState,
|
||||
ConfidenceTier = reachability.ConfidenceTier
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Reachability, payload, provenance, SchemaVersions.Reachability);
|
||||
}
|
||||
|
||||
internal sealed record ReachabilityPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? ProofType { get; init; }
|
||||
public IReadOnlyList<FunctionPathPayload>? FunctionPath { get; init; }
|
||||
public IReadOnlyList<ImportChainPayload>? ImportChain { get; init; }
|
||||
public string? LatticeState { get; init; }
|
||||
public int? ConfidenceTier { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record FunctionPathPayload
|
||||
{
|
||||
public required string FunctionName { get; init; }
|
||||
public required string FilePath { get; init; }
|
||||
public required int Line { get; init; }
|
||||
public int? Column { get; init; }
|
||||
public string? ModuleName { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record ImportChainPayload
|
||||
{
|
||||
public required string PackageName { get; init; }
|
||||
public string? Version { get; init; }
|
||||
public string? ImportedBy { get; init; }
|
||||
public string? ImportPath { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using StellaOps.Evidence.Bundle;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class EvidenceBundleAdapter
|
||||
{
|
||||
private static IEvidence ConvertVexStatus(
|
||||
VexStatusEvidence vexStatus,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new VexStatusPayload
|
||||
{
|
||||
Hash = vexStatus.Hash,
|
||||
VexStatus = vexStatus.Current?.VexStatus,
|
||||
Justification = vexStatus.Current?.Justification,
|
||||
ImpactStatement = vexStatus.Current?.ImpactStatement,
|
||||
ActionStatement = vexStatus.Current?.ActionStatement,
|
||||
StatementSource = vexStatus.Current?.Source,
|
||||
StatementTimestamp = vexStatus.Current?.Timestamp
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Vex, payload, provenance, SchemaVersions.Vex);
|
||||
}
|
||||
|
||||
internal sealed record VexStatusPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? VexStatus { get; init; }
|
||||
public string? Justification { get; init; }
|
||||
public string? ImpactStatement { get; init; }
|
||||
public string? ActionStatement { get; init; }
|
||||
public string? StatementSource { get; init; }
|
||||
public DateTimeOffset? StatementTimestamp { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace StellaOps.Evidence.Core.Adapters;
|
||||
/// An EvidenceBundle may contain multiple evidence types (reachability, VEX, provenance, etc.),
|
||||
/// each converted to a separate IEvidence record.
|
||||
/// </summary>
|
||||
public sealed class EvidenceBundleAdapter : EvidenceAdapterBase, IEvidenceAdapter<EvidenceBundle>
|
||||
public sealed partial class EvidenceBundleAdapter : EvidenceAdapterBase, IEvidenceAdapter<EvidenceBundle>
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version constants for evidence payloads.
|
||||
@@ -87,284 +87,4 @@ public sealed class EvidenceBundleAdapter : EvidenceAdapterBase, IEvidenceAdapte
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static IEvidence ConvertReachability(
|
||||
ReachabilityEvidence reachability,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new ReachabilityPayload
|
||||
{
|
||||
Hash = reachability.Hash,
|
||||
ProofType = reachability.ProofType.ToString(),
|
||||
FunctionPath = reachability.FunctionPath?.Select(f => new FunctionPathPayload
|
||||
{
|
||||
FunctionName = f.FunctionName,
|
||||
FilePath = f.FilePath,
|
||||
Line = f.Line,
|
||||
Column = f.Column,
|
||||
ModuleName = f.ModuleName
|
||||
}).ToList(),
|
||||
ImportChain = reachability.ImportChain?.Select(i => new ImportChainPayload
|
||||
{
|
||||
PackageName = i.PackageName,
|
||||
Version = i.Version,
|
||||
ImportedBy = i.ImportedBy,
|
||||
ImportPath = i.ImportPath
|
||||
}).ToList(),
|
||||
LatticeState = reachability.LatticeState,
|
||||
ConfidenceTier = reachability.ConfidenceTier
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Reachability, payload, provenance, SchemaVersions.Reachability);
|
||||
}
|
||||
|
||||
private static IEvidence ConvertVexStatus(
|
||||
VexStatusEvidence vexStatus,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new VexStatusPayload
|
||||
{
|
||||
Hash = vexStatus.Hash,
|
||||
VexStatus = vexStatus.Current?.VexStatus,
|
||||
Justification = vexStatus.Current?.Justification,
|
||||
ImpactStatement = vexStatus.Current?.ImpactStatement,
|
||||
ActionStatement = vexStatus.Current?.ActionStatement,
|
||||
StatementSource = vexStatus.Current?.Source,
|
||||
StatementTimestamp = vexStatus.Current?.Timestamp
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Vex, payload, provenance, SchemaVersions.Vex);
|
||||
}
|
||||
|
||||
private static IEvidence ConvertProvenance(
|
||||
ProvenanceEvidence provenanceEvidence,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new ProvenancePayload
|
||||
{
|
||||
Hash = provenanceEvidence.Hash,
|
||||
BuilderId = provenanceEvidence.Ancestry?.BuildId,
|
||||
BuildTime = provenanceEvidence.Ancestry?.BuildTime,
|
||||
ImageDigest = provenanceEvidence.Ancestry?.ImageDigest,
|
||||
LayerDigest = provenanceEvidence.Ancestry?.LayerDigest,
|
||||
CommitHash = provenanceEvidence.Ancestry?.CommitHash,
|
||||
VerificationStatus = provenanceEvidence.VerificationStatus,
|
||||
RekorLogIndex = provenanceEvidence.RekorEntry?.LogIndex
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Provenance, payload, provenance, SchemaVersions.Provenance);
|
||||
}
|
||||
|
||||
private static IEvidence ConvertCallStack(
|
||||
CallStackEvidence callStack,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new CallStackPayload
|
||||
{
|
||||
Hash = callStack.Hash,
|
||||
SinkFrameIndex = callStack.SinkFrameIndex,
|
||||
SourceFrameIndex = callStack.SourceFrameIndex,
|
||||
Frames = callStack.Frames?.Select(f => new StackFramePayload
|
||||
{
|
||||
FunctionName = f.FunctionName,
|
||||
FilePath = f.FilePath,
|
||||
Line = f.Line,
|
||||
Column = f.Column,
|
||||
IsSink = f.IsSink,
|
||||
IsSource = f.IsSource
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Runtime, payload, provenance, SchemaVersions.CallStack);
|
||||
}
|
||||
|
||||
private static IEvidence ConvertDiff(
|
||||
DiffEvidence diff,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new DiffPayload
|
||||
{
|
||||
Hash = diff.Hash,
|
||||
DiffType = diff.DiffType.ToString(),
|
||||
PreviousScanId = diff.PreviousScanId,
|
||||
PreviousScanTime = diff.PreviousScanTime,
|
||||
Entries = diff.Entries?.Select(e => new DiffEntryPayload
|
||||
{
|
||||
Operation = e.Operation.ToString(),
|
||||
Path = e.Path,
|
||||
OldValue = e.OldValue,
|
||||
NewValue = e.NewValue,
|
||||
ComponentPurl = e.ComponentPurl
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Artifact, payload, provenance, SchemaVersions.Diff);
|
||||
}
|
||||
|
||||
private static IEvidence ConvertGraphRevision(
|
||||
GraphRevisionEvidence graphRevision,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new GraphRevisionPayload
|
||||
{
|
||||
Hash = graphRevision.Hash,
|
||||
RevisionId = graphRevision.GraphRevisionId,
|
||||
VerdictReceipt = graphRevision.VerdictReceipt,
|
||||
GraphComputedAt = graphRevision.GraphComputedAt,
|
||||
NodeCount = graphRevision.TotalNodes,
|
||||
EdgeCount = graphRevision.TotalEdges
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Dependency, payload, provenance, SchemaVersions.GraphRevision);
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_008_LB_binary_diff_evidence_models (BINDIFF-LB-003)
|
||||
private static IEvidence ConvertBinaryDiff(
|
||||
BinaryDiffEvidence binaryDiff,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new BinaryDiffPayload
|
||||
{
|
||||
Hash = binaryDiff.Hash,
|
||||
DiffType = binaryDiff.DiffType.ToString(),
|
||||
PreviousBinaryDigest = binaryDiff.PreviousBinaryDigest,
|
||||
CurrentBinaryDigest = binaryDiff.CurrentBinaryDigest,
|
||||
BinaryFormat = binaryDiff.BinaryFormat,
|
||||
ToolVersion = binaryDiff.ToolVersion,
|
||||
SimilarityScore = binaryDiff.SimilarityScore,
|
||||
FunctionChangeCount = binaryDiff.FunctionChanges.Length,
|
||||
SymbolChangeCount = binaryDiff.SymbolChanges.Length,
|
||||
SectionChangeCount = binaryDiff.SectionChanges.Length,
|
||||
SecurityChangeCount = binaryDiff.SecurityChanges.Length,
|
||||
HasSemanticDiff = binaryDiff.SemanticDiff is not null,
|
||||
SemanticSimilarity = binaryDiff.SemanticDiff?.Similarity
|
||||
};
|
||||
|
||||
return CreateEvidence(subjectNodeId, EvidenceType.Artifact, payload, provenance, SchemaVersions.BinaryDiff);
|
||||
}
|
||||
|
||||
#region Payload Records
|
||||
|
||||
internal sealed record ReachabilityPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? ProofType { get; init; }
|
||||
public IReadOnlyList<FunctionPathPayload>? FunctionPath { get; init; }
|
||||
public IReadOnlyList<ImportChainPayload>? ImportChain { get; init; }
|
||||
public string? LatticeState { get; init; }
|
||||
public int? ConfidenceTier { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record FunctionPathPayload
|
||||
{
|
||||
public required string FunctionName { get; init; }
|
||||
public required string FilePath { get; init; }
|
||||
public required int Line { get; init; }
|
||||
public int? Column { get; init; }
|
||||
public string? ModuleName { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record ImportChainPayload
|
||||
{
|
||||
public required string PackageName { get; init; }
|
||||
public string? Version { get; init; }
|
||||
public string? ImportedBy { get; init; }
|
||||
public string? ImportPath { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record VexStatusPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? VexStatus { get; init; }
|
||||
public string? Justification { get; init; }
|
||||
public string? ImpactStatement { get; init; }
|
||||
public string? ActionStatement { get; init; }
|
||||
public string? StatementSource { get; init; }
|
||||
public DateTimeOffset? StatementTimestamp { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record ProvenancePayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? BuilderId { get; init; }
|
||||
public DateTimeOffset? BuildTime { get; init; }
|
||||
public string? ImageDigest { get; init; }
|
||||
public string? LayerDigest { get; init; }
|
||||
public string? CommitHash { get; init; }
|
||||
public string? VerificationStatus { get; init; }
|
||||
public long? RekorLogIndex { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record CallStackPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public int? SinkFrameIndex { get; init; }
|
||||
public int? SourceFrameIndex { get; init; }
|
||||
public IReadOnlyList<StackFramePayload>? Frames { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record StackFramePayload
|
||||
{
|
||||
public required string FunctionName { get; init; }
|
||||
public required string FilePath { get; init; }
|
||||
public required int Line { get; init; }
|
||||
public int? Column { get; init; }
|
||||
public bool IsSink { get; init; }
|
||||
public bool IsSource { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record DiffPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? DiffType { get; init; }
|
||||
public string? PreviousScanId { get; init; }
|
||||
public DateTimeOffset? PreviousScanTime { get; init; }
|
||||
public IReadOnlyList<DiffEntryPayload>? Entries { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record DiffEntryPayload
|
||||
{
|
||||
public required string Operation { get; init; }
|
||||
public required string Path { get; init; }
|
||||
public string? OldValue { get; init; }
|
||||
public string? NewValue { get; init; }
|
||||
public string? ComponentPurl { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record GraphRevisionPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? RevisionId { get; init; }
|
||||
public string? VerdictReceipt { get; init; }
|
||||
public DateTimeOffset? GraphComputedAt { get; init; }
|
||||
public int? NodeCount { get; init; }
|
||||
public int? EdgeCount { get; init; }
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_008_LB_binary_diff_evidence_models (BINDIFF-LB-003)
|
||||
internal sealed record BinaryDiffPayload
|
||||
{
|
||||
public string? Hash { get; init; }
|
||||
public string? DiffType { get; init; }
|
||||
public string? PreviousBinaryDigest { get; init; }
|
||||
public string? CurrentBinaryDigest { get; init; }
|
||||
public string? BinaryFormat { get; init; }
|
||||
public string? ToolVersion { get; init; }
|
||||
public double? SimilarityScore { get; init; }
|
||||
public int FunctionChangeCount { get; init; }
|
||||
public int SymbolChangeCount { get; init; }
|
||||
public int SectionChangeCount { get; init; }
|
||||
public int SecurityChangeCount { get; init; }
|
||||
public bool HasSemanticDiff { get; init; }
|
||||
public double? SemanticSimilarity { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class EvidenceStatementAdapter
|
||||
{
|
||||
internal sealed record EvidenceStatementPayload
|
||||
{
|
||||
public required string Source { get; init; }
|
||||
public required string SourceVersion { get; init; }
|
||||
public required DateTimeOffset CollectionTime { get; init; }
|
||||
public required string SbomEntryId { get; init; }
|
||||
public string? VulnerabilityId { get; init; }
|
||||
public string? RawFindingHash { get; init; }
|
||||
public string? OriginalEvidenceId { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace StellaOps.Evidence.Core.Adapters;
|
||||
/// - Payload from the predicate
|
||||
/// - Provenance from source/sourceVersion/collectionTime
|
||||
/// </remarks>
|
||||
public sealed class EvidenceStatementAdapter : EvidenceAdapterBase, IEvidenceAdapter<EvidenceStatementInput>
|
||||
public sealed partial class EvidenceStatementAdapter : EvidenceAdapterBase, IEvidenceAdapter<EvidenceStatementInput>
|
||||
{
|
||||
private const string SchemaVersion = "evidence-statement/v1";
|
||||
|
||||
@@ -84,65 +84,4 @@ public sealed class EvidenceStatementAdapter : EvidenceAdapterBase, IEvidenceAda
|
||||
};
|
||||
}
|
||||
|
||||
#region Payload Records
|
||||
|
||||
internal sealed record EvidenceStatementPayload
|
||||
{
|
||||
public required string Source { get; init; }
|
||||
public required string SourceVersion { get; init; }
|
||||
public required DateTimeOffset CollectionTime { get; init; }
|
||||
public required string SbomEntryId { get; init; }
|
||||
public string? VulnerabilityId { get; init; }
|
||||
public string? RawFindingHash { get; init; }
|
||||
public string? OriginalEvidenceId { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input DTO for EvidenceStatementAdapter.
|
||||
/// Decouples the adapter from direct dependency on StellaOps.Attestor.ProofChain.
|
||||
/// </summary>
|
||||
public sealed record EvidenceStatementInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Subject artifact digest from the in-toto statement.
|
||||
/// </summary>
|
||||
public required string SubjectDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scanner or feed name that produced this evidence.
|
||||
/// </summary>
|
||||
public required string Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the source tool.
|
||||
/// </summary>
|
||||
public required string SourceVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when evidence was collected.
|
||||
/// </summary>
|
||||
public required DateTimeOffset CollectionTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the SBOM entry this evidence relates to.
|
||||
/// </summary>
|
||||
public required string SbomEntryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVE or vulnerability identifier if applicable.
|
||||
/// </summary>
|
||||
public string? VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the raw finding data (to avoid storing large payloads).
|
||||
/// </summary>
|
||||
public string? RawFindingHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Original content-addressed evidence ID from the statement.
|
||||
/// </summary>
|
||||
public string? EvidenceId { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Input DTO for EvidenceStatementAdapter.
|
||||
/// Decouples the adapter from direct dependency on StellaOps.Attestor.ProofChain.
|
||||
/// </summary>
|
||||
public sealed record EvidenceStatementInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Subject artifact digest from the in-toto statement.
|
||||
/// </summary>
|
||||
public required string SubjectDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scanner or feed name that produced this evidence.
|
||||
/// </summary>
|
||||
public required string Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the source tool.
|
||||
/// </summary>
|
||||
public required string SourceVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when evidence was collected.
|
||||
/// </summary>
|
||||
public required DateTimeOffset CollectionTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the SBOM entry this evidence relates to.
|
||||
/// </summary>
|
||||
public required string SbomEntryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVE or vulnerability identifier if applicable.
|
||||
/// </summary>
|
||||
public string? VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the raw finding data (to avoid storing large payloads).
|
||||
/// </summary>
|
||||
public string? RawFindingHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Original content-addressed evidence ID from the statement.
|
||||
/// </summary>
|
||||
public string? EvidenceId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class ProofSegmentAdapter
|
||||
{
|
||||
internal sealed record ProofSegmentPayload
|
||||
{
|
||||
public required string SegmentId { get; init; }
|
||||
public required string SegmentType { get; init; }
|
||||
public required int Index { get; init; }
|
||||
public required string InputHash { get; init; }
|
||||
public required string ResultHash { get; init; }
|
||||
public string? PrevSegmentHash { get; init; }
|
||||
public required string ToolId { get; init; }
|
||||
public required string ToolVersion { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public string? SpineId { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ namespace StellaOps.Evidence.Core.Adapters;
|
||||
/// Converts Scanner's ProofSegment to unified <see cref="IEvidence"/> records.
|
||||
/// Each segment represents a step in the proof chain from SBOM to VEX verdict.
|
||||
/// </summary>
|
||||
public sealed class ProofSegmentAdapter : EvidenceAdapterBase, IEvidenceAdapter<ProofSegmentInput>
|
||||
public sealed partial class ProofSegmentAdapter : EvidenceAdapterBase, IEvidenceAdapter<ProofSegmentInput>
|
||||
{
|
||||
private const string SchemaVersion = "proof-segment/v1";
|
||||
|
||||
@@ -67,78 +67,4 @@ public sealed class ProofSegmentAdapter : EvidenceAdapterBase, IEvidenceAdapter<
|
||||
_ => EvidenceType.Custom
|
||||
};
|
||||
|
||||
#region Payload Records
|
||||
|
||||
internal sealed record ProofSegmentPayload
|
||||
{
|
||||
public required string SegmentId { get; init; }
|
||||
public required string SegmentType { get; init; }
|
||||
public required int Index { get; init; }
|
||||
public required string InputHash { get; init; }
|
||||
public required string ResultHash { get; init; }
|
||||
public string? PrevSegmentHash { get; init; }
|
||||
public required string ToolId { get; init; }
|
||||
public required string ToolVersion { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public string? SpineId { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input DTO for ProofSegmentAdapter.
|
||||
/// Decouples the adapter from direct dependency on StellaOps.Scanner.ProofSpine.
|
||||
/// </summary>
|
||||
public sealed record ProofSegmentInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique segment identifier.
|
||||
/// </summary>
|
||||
public required string SegmentId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Segment type (e.g., "SbomSlice", "Match", "Reachability", "GuardAnalysis", "RuntimeObservation", "PolicyEval").
|
||||
/// </summary>
|
||||
public required string SegmentType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Position in the proof chain (0-based).
|
||||
/// </summary>
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of input data to this segment.
|
||||
/// </summary>
|
||||
public required string InputHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of output/result from this segment.
|
||||
/// </summary>
|
||||
public required string ResultHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the previous segment (for chaining verification).
|
||||
/// </summary>
|
||||
public string? PrevSegmentHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tool that produced this segment.
|
||||
/// </summary>
|
||||
public required string ToolId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the tool.
|
||||
/// </summary>
|
||||
public required string ToolVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verification status (e.g., "Pending", "Verified", "Invalid", "Untrusted").
|
||||
/// </summary>
|
||||
public required string Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Parent spine ID for correlation.
|
||||
/// </summary>
|
||||
public string? SpineId { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Input DTO for ProofSegmentAdapter.
|
||||
/// Decouples the adapter from direct dependency on StellaOps.Scanner.ProofSpine.
|
||||
/// </summary>
|
||||
public sealed record ProofSegmentInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique segment identifier.
|
||||
/// </summary>
|
||||
public required string SegmentId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Segment type (e.g., "SbomSlice", "Match", "Reachability", "GuardAnalysis", "RuntimeObservation", "PolicyEval").
|
||||
/// </summary>
|
||||
public required string SegmentType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Position in the proof chain (0-based).
|
||||
/// </summary>
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of input data to this segment.
|
||||
/// </summary>
|
||||
public required string InputHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of output/result from this segment.
|
||||
/// </summary>
|
||||
public required string ResultHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the previous segment (for chaining verification).
|
||||
/// </summary>
|
||||
public string? PrevSegmentHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tool that produced this segment.
|
||||
/// </summary>
|
||||
public required string ToolId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the tool.
|
||||
/// </summary>
|
||||
public required string ToolVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verification status (e.g., "Pending", "Verified", "Invalid", "Untrusted").
|
||||
/// </summary>
|
||||
public required string Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Parent spine ID for correlation.
|
||||
/// </summary>
|
||||
public string? SpineId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class VexObservationAdapter
|
||||
{
|
||||
/// <summary>
|
||||
/// Payload for observation-level (provenance) evidence record.
|
||||
/// </summary>
|
||||
private sealed record VexObservationPayload(
|
||||
string ObservationId,
|
||||
string Tenant,
|
||||
string ProviderId,
|
||||
string StreamId,
|
||||
string UpstreamId,
|
||||
string? DocumentVersion,
|
||||
string ContentHash,
|
||||
string Format,
|
||||
string? SpecVersion,
|
||||
int StatementCount,
|
||||
ImmutableArray<string> Supersedes,
|
||||
DateTimeOffset FetchedAt,
|
||||
DateTimeOffset ReceivedAt,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Payload for statement-level VEX evidence record.
|
||||
/// </summary>
|
||||
private sealed record VexStatementPayload(
|
||||
string ObservationId,
|
||||
int StatementIndex,
|
||||
string VulnerabilityId,
|
||||
string ProductKey,
|
||||
string Status,
|
||||
string? Justification,
|
||||
DateTimeOffset? LastObserved,
|
||||
string? Locator,
|
||||
string? IntroducedVersion,
|
||||
string? FixedVersion,
|
||||
string? Purl,
|
||||
string? Cpe,
|
||||
int EvidenceCount,
|
||||
string ProviderId,
|
||||
string StreamId);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class VexObservationAdapter
|
||||
{
|
||||
private EvidenceRecord CreateObservationRecord(
|
||||
VexObservationInput observation,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new VexObservationPayload(
|
||||
ObservationId: observation.ObservationId,
|
||||
Tenant: observation.Tenant,
|
||||
ProviderId: observation.ProviderId,
|
||||
StreamId: observation.StreamId,
|
||||
UpstreamId: observation.Upstream.UpstreamId,
|
||||
DocumentVersion: observation.Upstream.DocumentVersion,
|
||||
ContentHash: observation.Upstream.ContentHash,
|
||||
Format: observation.Content.Format,
|
||||
SpecVersion: observation.Content.SpecVersion,
|
||||
StatementCount: observation.Statements.Length,
|
||||
Supersedes: observation.Supersedes,
|
||||
FetchedAt: observation.Upstream.FetchedAt,
|
||||
ReceivedAt: observation.Upstream.ReceivedAt,
|
||||
CreatedAt: observation.CreatedAt);
|
||||
|
||||
var signatures = BuildObservationSignatures(observation.Upstream.Signature);
|
||||
|
||||
return CreateEvidence(
|
||||
subjectNodeId: subjectNodeId,
|
||||
evidenceType: EvidenceType.Provenance,
|
||||
payload: payload,
|
||||
provenance: provenance,
|
||||
payloadSchemaVersion: PayloadSchemaVersion,
|
||||
signatures: signatures);
|
||||
}
|
||||
|
||||
private EvidenceRecord CreateStatementRecord(
|
||||
VexObservationInput observation,
|
||||
VexObservationStatementInput statement,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance,
|
||||
int statementIndex)
|
||||
{
|
||||
var payload = new VexStatementPayload(
|
||||
ObservationId: observation.ObservationId,
|
||||
StatementIndex: statementIndex,
|
||||
VulnerabilityId: statement.VulnerabilityId,
|
||||
ProductKey: statement.ProductKey,
|
||||
Status: statement.Status,
|
||||
Justification: statement.Justification,
|
||||
LastObserved: statement.LastObserved,
|
||||
Locator: statement.Locator,
|
||||
IntroducedVersion: statement.IntroducedVersion,
|
||||
FixedVersion: statement.FixedVersion,
|
||||
Purl: statement.Purl,
|
||||
Cpe: statement.Cpe,
|
||||
EvidenceCount: statement.Evidence.Length,
|
||||
ProviderId: observation.ProviderId,
|
||||
StreamId: observation.StreamId);
|
||||
|
||||
var signatures = BuildObservationSignatures(observation.Upstream.Signature);
|
||||
|
||||
return CreateEvidence(
|
||||
subjectNodeId: subjectNodeId,
|
||||
evidenceType: EvidenceType.Vex,
|
||||
payload: payload,
|
||||
provenance: provenance,
|
||||
payloadSchemaVersion: PayloadSchemaVersion,
|
||||
signatures: signatures);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed partial class VexObservationAdapter
|
||||
{
|
||||
private static ImmutableArray<EvidenceSignature> BuildObservationSignatures(
|
||||
VexObservationSignatureInput signature)
|
||||
{
|
||||
if (!signature.Present || string.IsNullOrWhiteSpace(signature.Signature))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var sig = new EvidenceSignature
|
||||
{
|
||||
SignerId = signature.KeyId ?? "unknown",
|
||||
Algorithm = signature.Format ?? "unknown",
|
||||
SignatureBase64 = signature.Signature,
|
||||
SignedAt = DateTimeOffset.UtcNow,
|
||||
SignerType = SignerType.Vendor
|
||||
};
|
||||
|
||||
return [sig];
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,8 @@
|
||||
// <copyright file="VexObservationAdapter.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Input DTO for VexObservation data, decoupling from Excititor.Core dependency.
|
||||
/// </summary>
|
||||
public sealed record VexObservationInput
|
||||
{
|
||||
public required string ObservationId { get; init; }
|
||||
public required string Tenant { get; init; }
|
||||
public required string ProviderId { get; init; }
|
||||
public required string StreamId { get; init; }
|
||||
public required VexObservationUpstreamInput Upstream { get; init; }
|
||||
public required ImmutableArray<VexObservationStatementInput> Statements { get; init; }
|
||||
public required VexObservationContentInput Content { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public ImmutableArray<string> Supersedes { get; init; } = [];
|
||||
public ImmutableDictionary<string, string> Attributes { get; init; } = ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
|
||||
public sealed record VexObservationUpstreamInput
|
||||
{
|
||||
public required string UpstreamId { get; init; }
|
||||
public string? DocumentVersion { get; init; }
|
||||
public required DateTimeOffset FetchedAt { get; init; }
|
||||
public required DateTimeOffset ReceivedAt { get; init; }
|
||||
public required string ContentHash { get; init; }
|
||||
public required VexObservationSignatureInput Signature { get; init; }
|
||||
public ImmutableDictionary<string, string> Metadata { get; init; } = ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
|
||||
public sealed record VexObservationSignatureInput
|
||||
{
|
||||
public bool Present { get; init; }
|
||||
public string? Format { get; init; }
|
||||
public string? KeyId { get; init; }
|
||||
public string? Signature { get; init; }
|
||||
}
|
||||
|
||||
public sealed record VexObservationContentInput
|
||||
{
|
||||
public required string Format { get; init; }
|
||||
public string? SpecVersion { get; init; }
|
||||
public JsonNode? Raw { get; init; }
|
||||
public ImmutableDictionary<string, string> Metadata { get; init; } = ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
|
||||
public sealed record VexObservationStatementInput
|
||||
{
|
||||
public required string VulnerabilityId { get; init; }
|
||||
public required string ProductKey { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public DateTimeOffset? LastObserved { get; init; }
|
||||
public string? Locator { get; init; }
|
||||
public string? Justification { get; init; }
|
||||
public string? IntroducedVersion { get; init; }
|
||||
public string? FixedVersion { get; init; }
|
||||
public string? Purl { get; init; }
|
||||
public string? Cpe { get; init; }
|
||||
public ImmutableArray<JsonNode> Evidence { get; init; } = [];
|
||||
public ImmutableDictionary<string, string> Metadata { get; init; } = ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adapter that converts Excititor's VexObservation into unified IEvidence records.
|
||||
/// Uses <see cref="VexObservationInput"/> DTO to avoid circular dependencies.
|
||||
@@ -75,7 +11,7 @@ public sealed record VexObservationStatementInput
|
||||
/// VexObservations contain multiple statements; each statement becomes a separate evidence record.
|
||||
/// An additional observation-level evidence record captures the overall document provenance.
|
||||
/// </remarks>
|
||||
public sealed class VexObservationAdapter : EvidenceAdapterBase, IEvidenceAdapter<VexObservationInput>
|
||||
public sealed partial class VexObservationAdapter : EvidenceAdapterBase, IEvidenceAdapter<VexObservationInput>
|
||||
{
|
||||
private const string PayloadSchemaVersion = "1.0.0";
|
||||
private const string AdapterSource = "VexObservationAdapter";
|
||||
@@ -119,130 +55,4 @@ public sealed class VexObservationAdapter : EvidenceAdapterBase, IEvidenceAdapte
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
private EvidenceRecord CreateObservationRecord(
|
||||
VexObservationInput observation,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance)
|
||||
{
|
||||
var payload = new VexObservationPayload(
|
||||
ObservationId: observation.ObservationId,
|
||||
Tenant: observation.Tenant,
|
||||
ProviderId: observation.ProviderId,
|
||||
StreamId: observation.StreamId,
|
||||
UpstreamId: observation.Upstream.UpstreamId,
|
||||
DocumentVersion: observation.Upstream.DocumentVersion,
|
||||
ContentHash: observation.Upstream.ContentHash,
|
||||
Format: observation.Content.Format,
|
||||
SpecVersion: observation.Content.SpecVersion,
|
||||
StatementCount: observation.Statements.Length,
|
||||
Supersedes: observation.Supersedes,
|
||||
FetchedAt: observation.Upstream.FetchedAt,
|
||||
ReceivedAt: observation.Upstream.ReceivedAt,
|
||||
CreatedAt: observation.CreatedAt);
|
||||
|
||||
var signatures = BuildObservationSignatures(observation.Upstream.Signature);
|
||||
|
||||
return CreateEvidence(
|
||||
subjectNodeId: subjectNodeId,
|
||||
evidenceType: EvidenceType.Provenance,
|
||||
payload: payload,
|
||||
provenance: provenance,
|
||||
payloadSchemaVersion: PayloadSchemaVersion,
|
||||
signatures: signatures);
|
||||
}
|
||||
|
||||
private EvidenceRecord CreateStatementRecord(
|
||||
VexObservationInput observation,
|
||||
VexObservationStatementInput statement,
|
||||
string subjectNodeId,
|
||||
EvidenceProvenance provenance,
|
||||
int statementIndex)
|
||||
{
|
||||
var payload = new VexStatementPayload(
|
||||
ObservationId: observation.ObservationId,
|
||||
StatementIndex: statementIndex,
|
||||
VulnerabilityId: statement.VulnerabilityId,
|
||||
ProductKey: statement.ProductKey,
|
||||
Status: statement.Status,
|
||||
Justification: statement.Justification,
|
||||
LastObserved: statement.LastObserved,
|
||||
Locator: statement.Locator,
|
||||
IntroducedVersion: statement.IntroducedVersion,
|
||||
FixedVersion: statement.FixedVersion,
|
||||
Purl: statement.Purl,
|
||||
Cpe: statement.Cpe,
|
||||
EvidenceCount: statement.Evidence.Length,
|
||||
ProviderId: observation.ProviderId,
|
||||
StreamId: observation.StreamId);
|
||||
|
||||
var signatures = BuildObservationSignatures(observation.Upstream.Signature);
|
||||
|
||||
return CreateEvidence(
|
||||
subjectNodeId: subjectNodeId,
|
||||
evidenceType: EvidenceType.Vex,
|
||||
payload: payload,
|
||||
provenance: provenance,
|
||||
payloadSchemaVersion: PayloadSchemaVersion,
|
||||
signatures: signatures);
|
||||
}
|
||||
|
||||
private static ImmutableArray<EvidenceSignature> BuildObservationSignatures(
|
||||
VexObservationSignatureInput signature)
|
||||
{
|
||||
if (!signature.Present || string.IsNullOrWhiteSpace(signature.Signature))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var sig = new EvidenceSignature
|
||||
{
|
||||
SignerId = signature.KeyId ?? "unknown",
|
||||
Algorithm = signature.Format ?? "unknown",
|
||||
SignatureBase64 = signature.Signature,
|
||||
SignedAt = DateTimeOffset.UtcNow,
|
||||
SignerType = SignerType.Vendor
|
||||
};
|
||||
|
||||
return [sig];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Payload for observation-level (provenance) evidence record.
|
||||
/// </summary>
|
||||
private sealed record VexObservationPayload(
|
||||
string ObservationId,
|
||||
string Tenant,
|
||||
string ProviderId,
|
||||
string StreamId,
|
||||
string UpstreamId,
|
||||
string? DocumentVersion,
|
||||
string ContentHash,
|
||||
string Format,
|
||||
string? SpecVersion,
|
||||
int StatementCount,
|
||||
ImmutableArray<string> Supersedes,
|
||||
DateTimeOffset FetchedAt,
|
||||
DateTimeOffset ReceivedAt,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Payload for statement-level VEX evidence record.
|
||||
/// </summary>
|
||||
private sealed record VexStatementPayload(
|
||||
string ObservationId,
|
||||
int StatementIndex,
|
||||
string VulnerabilityId,
|
||||
string ProductKey,
|
||||
string Status,
|
||||
string? Justification,
|
||||
DateTimeOffset? LastObserved,
|
||||
string? Locator,
|
||||
string? IntroducedVersion,
|
||||
string? FixedVersion,
|
||||
string? Purl,
|
||||
string? Cpe,
|
||||
int EvidenceCount,
|
||||
string ProviderId,
|
||||
string StreamId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed record VexObservationContentInput
|
||||
{
|
||||
public required string Format { get; init; }
|
||||
public string? SpecVersion { get; init; }
|
||||
public JsonNode? Raw { get; init; }
|
||||
public ImmutableDictionary<string, string> Metadata { get; init; } =
|
||||
ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Input DTO for VexObservation data, decoupling from Excititor.Core dependency.
|
||||
/// </summary>
|
||||
public sealed record VexObservationInput
|
||||
{
|
||||
public required string ObservationId { get; init; }
|
||||
public required string Tenant { get; init; }
|
||||
public required string ProviderId { get; init; }
|
||||
public required string StreamId { get; init; }
|
||||
public required VexObservationUpstreamInput Upstream { get; init; }
|
||||
public required ImmutableArray<VexObservationStatementInput> Statements { get; init; }
|
||||
public required VexObservationContentInput Content { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public ImmutableArray<string> Supersedes { get; init; } = [];
|
||||
public ImmutableDictionary<string, string> Attributes { get; init; } =
|
||||
ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed record VexObservationSignatureInput
|
||||
{
|
||||
public bool Present { get; init; }
|
||||
public string? Format { get; init; }
|
||||
public string? KeyId { get; init; }
|
||||
public string? Signature { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed record VexObservationStatementInput
|
||||
{
|
||||
public required string VulnerabilityId { get; init; }
|
||||
public required string ProductKey { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public DateTimeOffset? LastObserved { get; init; }
|
||||
public string? Locator { get; init; }
|
||||
public string? Justification { get; init; }
|
||||
public string? IntroducedVersion { get; init; }
|
||||
public string? FixedVersion { get; init; }
|
||||
public string? Purl { get; init; }
|
||||
public string? Cpe { get; init; }
|
||||
public ImmutableArray<JsonNode> Evidence { get; init; } = [];
|
||||
public ImmutableDictionary<string, string> Metadata { get; init; } =
|
||||
ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
public sealed record VexObservationUpstreamInput
|
||||
{
|
||||
public required string UpstreamId { get; init; }
|
||||
public string? DocumentVersion { get; init; }
|
||||
public required DateTimeOffset FetchedAt { get; init; }
|
||||
public required DateTimeOffset ReceivedAt { get; init; }
|
||||
public required string ContentHash { get; init; }
|
||||
public required VexObservationSignatureInput Signature { get; init; }
|
||||
public ImmutableDictionary<string, string> Metadata { get; init; } =
|
||||
ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
13
src/__Libraries/StellaOps.Evidence.Core/EvidenceHashInput.cs
Normal file
13
src/__Libraries/StellaOps.Evidence.Core/EvidenceHashInput.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace StellaOps.Evidence.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Internal record for evidence ID hash computation.
|
||||
/// Fields are sorted alphabetically for deterministic canonicalization.
|
||||
/// </summary>
|
||||
internal sealed record EvidenceHashInput(
|
||||
string GeneratedAt,
|
||||
string GeneratorId,
|
||||
string GeneratorVersion,
|
||||
string EvidenceType,
|
||||
string PayloadBase64,
|
||||
string SubjectNodeId);
|
||||
@@ -111,14 +111,3 @@ public sealed record EvidenceRecord : IEvidence
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal record for evidence ID hash computation.
|
||||
/// Fields are sorted alphabetically for deterministic canonicalization.
|
||||
/// </summary>
|
||||
internal sealed record EvidenceHashInput(
|
||||
string GeneratedAt,
|
||||
string GeneratorId,
|
||||
string GeneratorVersion,
|
||||
string EvidenceType,
|
||||
string PayloadBase64,
|
||||
string SubjectNodeId);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace StellaOps.Evidence.Core;
|
||||
|
||||
public sealed partial class InMemoryEvidenceStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<bool> DeleteAsync(string evidenceId, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(evidenceId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (!_byId.TryRemove(evidenceId, out var evidence))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
// Note: We don't remove from _bySubject index (ConcurrentBag doesn't support removal).
|
||||
// The GetBySubject method filters out null entries.
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
namespace StellaOps.Evidence.Core;
|
||||
|
||||
public sealed partial class InMemoryEvidenceStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<IEvidence?> GetByIdAsync(string evidenceId, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(evidenceId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
_byId.TryGetValue(evidenceId, out var evidence);
|
||||
return Task.FromResult(evidence);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<IEvidence>> GetByTypeAsync(
|
||||
EvidenceType evidenceType,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var results = _byId.Values
|
||||
.Where(e => e.EvidenceType == evidenceType)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<IEvidence>>(results);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> ExistsAsync(string subjectNodeId, EvidenceType type, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectNodeId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (!_bySubject.TryGetValue(subjectNodeId, out var evidenceIds))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var exists = evidenceIds
|
||||
.Distinct()
|
||||
.Any(id => _byId.TryGetValue(id, out var e) && e.EvidenceType == type);
|
||||
|
||||
return Task.FromResult(exists);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> CountBySubjectAsync(string subjectNodeId, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectNodeId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (!_bySubject.TryGetValue(subjectNodeId, out var evidenceIds))
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
var count = evidenceIds
|
||||
.Distinct()
|
||||
.Count(id => _byId.ContainsKey(id));
|
||||
|
||||
return Task.FromResult(count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace StellaOps.Evidence.Core;
|
||||
|
||||
public sealed partial class InMemoryEvidenceStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<string> StoreAsync(IEvidence evidence, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evidence);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
_byId.TryAdd(evidence.EvidenceId, evidence);
|
||||
var subjectBag = _bySubject.GetOrAdd(evidence.SubjectNodeId, _ => []);
|
||||
if (!subjectBag.Contains(evidence.EvidenceId))
|
||||
{
|
||||
subjectBag.Add(evidence.EvidenceId);
|
||||
}
|
||||
|
||||
return Task.FromResult(evidence.EvidenceId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> StoreBatchAsync(IEnumerable<IEvidence> evidenceRecords, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evidenceRecords);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var count = 0;
|
||||
foreach (var evidence in evidenceRecords)
|
||||
{
|
||||
if (_byId.TryAdd(evidence.EvidenceId, evidence))
|
||||
{
|
||||
var subjectBag = _bySubject.GetOrAdd(evidence.SubjectNodeId, _ => []);
|
||||
subjectBag.Add(evidence.EvidenceId);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace StellaOps.Evidence.Core;
|
||||
|
||||
public sealed partial class InMemoryEvidenceStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<IEvidence>> GetBySubjectAsync(
|
||||
string subjectNodeId,
|
||||
EvidenceType? typeFilter = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectNodeId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (!_bySubject.TryGetValue(subjectNodeId, out var evidenceIds))
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<IEvidence>>([]);
|
||||
}
|
||||
|
||||
var results = evidenceIds
|
||||
.Distinct()
|
||||
.Select(id => _byId.TryGetValue(id, out var e) ? e : null)
|
||||
.Where(e => e is not null)
|
||||
.Where(e => typeFilter is null || e!.EvidenceType == typeFilter)
|
||||
.Cast<IEvidence>()
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<IEvidence>>(results);
|
||||
}
|
||||
}
|
||||
@@ -6,151 +6,11 @@ namespace StellaOps.Evidence.Core;
|
||||
/// Thread-safe in-memory implementation of <see cref="IEvidenceStore"/>.
|
||||
/// Intended for testing, development, and ephemeral processing.
|
||||
/// </summary>
|
||||
public sealed class InMemoryEvidenceStore : IEvidenceStore
|
||||
public sealed partial class InMemoryEvidenceStore : IEvidenceStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, IEvidence> _byId = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ConcurrentBag<string>> _bySubject = new(StringComparer.Ordinal);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> StoreAsync(IEvidence evidence, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evidence);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
_byId.TryAdd(evidence.EvidenceId, evidence);
|
||||
|
||||
var subjectBag = _bySubject.GetOrAdd(evidence.SubjectNodeId, _ => []);
|
||||
if (!subjectBag.Contains(evidence.EvidenceId))
|
||||
{
|
||||
subjectBag.Add(evidence.EvidenceId);
|
||||
}
|
||||
|
||||
return Task.FromResult(evidence.EvidenceId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> StoreBatchAsync(IEnumerable<IEvidence> evidenceRecords, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evidenceRecords);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var count = 0;
|
||||
foreach (var evidence in evidenceRecords)
|
||||
{
|
||||
if (_byId.TryAdd(evidence.EvidenceId, evidence))
|
||||
{
|
||||
var subjectBag = _bySubject.GetOrAdd(evidence.SubjectNodeId, _ => []);
|
||||
subjectBag.Add(evidence.EvidenceId);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(count);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IEvidence?> GetByIdAsync(string evidenceId, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(evidenceId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
_byId.TryGetValue(evidenceId, out var evidence);
|
||||
return Task.FromResult(evidence);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<IEvidence>> GetBySubjectAsync(
|
||||
string subjectNodeId,
|
||||
EvidenceType? typeFilter = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectNodeId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (!_bySubject.TryGetValue(subjectNodeId, out var evidenceIds))
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<IEvidence>>([]);
|
||||
}
|
||||
|
||||
var results = evidenceIds
|
||||
.Distinct()
|
||||
.Select(id => _byId.TryGetValue(id, out var e) ? e : null)
|
||||
.Where(e => e is not null)
|
||||
.Where(e => typeFilter is null || e!.EvidenceType == typeFilter)
|
||||
.Cast<IEvidence>()
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<IEvidence>>(results);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<IEvidence>> GetByTypeAsync(
|
||||
EvidenceType evidenceType,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var results = _byId.Values
|
||||
.Where(e => e.EvidenceType == evidenceType)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<IEvidence>>(results);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> ExistsAsync(string subjectNodeId, EvidenceType type, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectNodeId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (!_bySubject.TryGetValue(subjectNodeId, out var evidenceIds))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var exists = evidenceIds
|
||||
.Distinct()
|
||||
.Any(id => _byId.TryGetValue(id, out var e) && e.EvidenceType == type);
|
||||
|
||||
return Task.FromResult(exists);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> DeleteAsync(string evidenceId, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(evidenceId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (!_byId.TryRemove(evidenceId, out var evidence))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
// Note: We don't remove from _bySubject index (ConcurrentBag doesn't support removal).
|
||||
// The GetBySubject method filters out null entries.
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> CountBySubjectAsync(string subjectNodeId, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectNodeId);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (!_bySubject.TryGetValue(subjectNodeId, out var evidenceIds))
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
var count = evidenceIds
|
||||
.Distinct()
|
||||
.Count(id => _byId.ContainsKey(id));
|
||||
|
||||
return Task.FromResult(count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all stored evidence. For testing only.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0080-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0080-A | TODO | Revalidated 2026-01-08 (open findings). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| REMED-07 | DONE | Split adapters/store into <=100-line partials; added EvidenceBundleAdapter test; dotnet test 2026-02-04 (113 tests). |
|
||||
|
||||
Reference in New Issue
Block a user