This commit is contained in:
master
2026-02-04 19:59:20 +02:00
parent 557feefdc3
commit 5548cf83bf
1479 changed files with 53557 additions and 40339 deletions

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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];
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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);

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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). |