Refactor code structure and optimize performance across multiple modules

This commit is contained in:
StellaOps Bot
2025-12-26 20:03:22 +02:00
parent c786faae84
commit f10d83c444
1385 changed files with 69732 additions and 10280 deletions

View File

@@ -0,0 +1,193 @@
// -----------------------------------------------------------------------------
// BinaryFingerprintEvidenceGenerator.cs
// Sprint: SPRINT_20251226_014_BINIDX
// Task: SCANINT-11 — Implement proof segment generation in Attestor
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json;
using StellaOps.Attestor.ProofChain.Models;
using StellaOps.Attestor.ProofChain.Predicates;
using StellaOps.Canonical.Json;
namespace StellaOps.Attestor.ProofChain.Generators;
/// <summary>
/// Generates binary fingerprint evidence proof segments for scanner findings.
/// Creates attestable evidence of binary vulnerability matches.
/// </summary>
public sealed class BinaryFingerprintEvidenceGenerator
{
private const string ToolId = "stellaops.binaryindex";
private const string ToolVersion = "1.0.0";
/// <summary>
/// Generate a proof segment from binary vulnerability findings.
/// </summary>
public ProofBlob Generate(BinaryFingerprintEvidencePredicate predicate)
{
ArgumentNullException.ThrowIfNull(predicate);
var predicateJson = JsonSerializer.SerializeToDocument(predicate, GetJsonOptions());
var dataHash = CanonJson.Sha256Prefixed(CanonJson.Canonicalize(predicateJson));
// Create subject ID from binary key and scan context
var subjectId = $"binary:{predicate.BinaryIdentity.BinaryKey}";
if (predicate.ScanContext is not null)
{
subjectId = $"{predicate.ScanContext.ScanId}:{subjectId}";
}
// Create evidence entry for each match
var evidences = new List<ProofEvidence>();
foreach (var match in predicate.Matches)
{
var matchData = JsonSerializer.SerializeToDocument(match, GetJsonOptions());
var matchHash = CanonJson.Sha256Prefixed(CanonJson.Canonicalize(matchData));
evidences.Add(new ProofEvidence
{
EvidenceId = $"evidence:binary:{predicate.BinaryIdentity.BinaryKey}:{match.CveId}",
Type = EvidenceType.BinaryFingerprint,
Source = match.Method,
Timestamp = DateTimeOffset.UtcNow,
Data = matchData,
DataHash = matchHash
});
}
// Determine proof type based on matches
var proofType = DetermineProofType(predicate.Matches);
var confidence = ComputeAggregateConfidence(predicate.Matches);
var proof = new ProofBlob
{
ProofId = "", // Will be computed by ProofHashing.WithHash
SubjectId = subjectId,
Type = proofType,
CreatedAt = DateTimeOffset.UtcNow,
Evidences = evidences,
Method = "binary_fingerprint_evidence",
Confidence = confidence,
ToolVersion = ToolVersion,
SnapshotId = GenerateSnapshotId()
};
return ProofHashing.WithHash(proof);
}
/// <summary>
/// Generate proof segments for multiple binary findings in batch.
/// </summary>
public ImmutableArray<ProofBlob> GenerateBatch(
IEnumerable<BinaryFingerprintEvidencePredicate> predicates)
{
var results = new List<ProofBlob>();
foreach (var predicate in predicates)
{
results.Add(Generate(predicate));
}
return results.ToImmutableArray();
}
/// <summary>
/// Create a BinaryFingerprintEvidencePredicate from scan findings.
/// </summary>
public static BinaryFingerprintEvidencePredicate CreatePredicate(
BinaryIdentityInfo identity,
string layerDigest,
IEnumerable<BinaryVulnMatchInfo> matches,
ScanContextInfo? scanContext = null)
{
return new BinaryFingerprintEvidencePredicate
{
BinaryIdentity = identity,
LayerDigest = layerDigest,
Matches = matches.ToImmutableArray(),
ScanContext = scanContext
};
}
private static ProofBlobType DetermineProofType(ImmutableArray<BinaryVulnMatchInfo> matches)
{
if (matches.IsDefaultOrEmpty)
{
return ProofBlobType.Unknown;
}
// Check if all matches have fix status indicating fixed
var allFixed = matches.All(m =>
m.FixStatus?.State?.Equals("fixed", StringComparison.OrdinalIgnoreCase) == true);
if (allFixed)
{
return ProofBlobType.BackportFixed;
}
// Check if any match is vulnerable
var anyVulnerable = matches.Any(m =>
m.FixStatus?.State?.Equals("vulnerable", StringComparison.OrdinalIgnoreCase) == true ||
m.FixStatus is null);
if (anyVulnerable)
{
return ProofBlobType.Vulnerable;
}
// Check for not_affected
var allNotAffected = matches.All(m =>
m.FixStatus?.State?.Equals("not_affected", StringComparison.OrdinalIgnoreCase) == true);
if (allNotAffected)
{
return ProofBlobType.NotAffected;
}
return ProofBlobType.Unknown;
}
private static double ComputeAggregateConfidence(ImmutableArray<BinaryVulnMatchInfo> matches)
{
if (matches.IsDefaultOrEmpty)
{
return 0.0;
}
// Use average confidence, weighted by match method
var weightedSum = 0.0;
var totalWeight = 0.0;
foreach (var match in matches)
{
var methodWeight = match.Method switch
{
"buildid_catalog" => 1.0,
"fingerprint_match" => 0.8,
"range_match" => 0.6,
_ => 0.5
};
weightedSum += (double)match.Confidence * methodWeight;
totalWeight += methodWeight;
}
return totalWeight > 0 ? Math.Min(weightedSum / totalWeight, 0.98) : 0.0;
}
private static string GenerateSnapshotId()
{
return DateTimeOffset.UtcNow.ToString("yyyyMMdd-HHmmss") + "-UTC";
}
private static JsonSerializerOptions GetJsonOptions()
{
return new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = false,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
}
}

View File

@@ -0,0 +1,215 @@
// -----------------------------------------------------------------------------
// BinaryFingerprintEvidencePredicate.cs
// Sprint: SPRINT_20251226_014_BINIDX
// Task: SCANINT-10 — Create binary_fingerprint_evidence proof segment type
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.ProofChain.Predicates;
/// <summary>
/// Predicate for binary fingerprint evidence proof segment.
/// Contains evidence of binary vulnerability matches with fingerprint and fix status.
/// Schema version: 1.0.0
/// </summary>
public sealed record BinaryFingerprintEvidencePredicate
{
/// <summary>
/// Predicate type URI.
/// </summary>
public const string PredicateType = "https://stellaops.dev/predicates/binary-fingerprint-evidence@v1";
/// <summary>
/// Schema version for this predicate format.
/// </summary>
[JsonPropertyName("version")]
public string Version { get; init; } = "1.0.0";
/// <summary>
/// Binary identity information.
/// </summary>
[JsonPropertyName("binary_identity")]
public required BinaryIdentityInfo BinaryIdentity { get; init; }
/// <summary>
/// Layer digest where binary was found.
/// </summary>
[JsonPropertyName("layer_digest")]
public required string LayerDigest { get; init; }
/// <summary>
/// Vulnerability matches for this binary.
/// </summary>
[JsonPropertyName("matches")]
public required ImmutableArray<BinaryVulnMatchInfo> Matches { get; init; }
/// <summary>
/// Scan context metadata.
/// </summary>
[JsonPropertyName("scan_context")]
public ScanContextInfo? ScanContext { get; init; }
}
/// <summary>
/// Binary identity information.
/// </summary>
public sealed record BinaryIdentityInfo
{
/// <summary>
/// Binary format (elf, pe, macho).
/// </summary>
[JsonPropertyName("format")]
public required string Format { get; init; }
/// <summary>
/// GNU Build-ID if available.
/// </summary>
[JsonPropertyName("build_id")]
public string? BuildId { get; init; }
/// <summary>
/// SHA256 hash of the binary file.
/// </summary>
[JsonPropertyName("file_sha256")]
public required string FileSha256 { get; init; }
/// <summary>
/// Target architecture (x86_64, aarch64, etc.).
/// </summary>
[JsonPropertyName("architecture")]
public required string Architecture { get; init; }
/// <summary>
/// Binary key for lookups.
/// </summary>
[JsonPropertyName("binary_key")]
public required string BinaryKey { get; init; }
/// <summary>
/// Path within the container filesystem.
/// </summary>
[JsonPropertyName("path")]
public string? Path { get; init; }
}
/// <summary>
/// Vulnerability match information.
/// </summary>
public sealed record BinaryVulnMatchInfo
{
/// <summary>
/// CVE identifier.
/// </summary>
[JsonPropertyName("cve_id")]
public required string CveId { get; init; }
/// <summary>
/// Match method (buildid_catalog, fingerprint_match, range_match).
/// </summary>
[JsonPropertyName("method")]
public required string Method { get; init; }
/// <summary>
/// Match confidence score (0.0-1.0).
/// </summary>
[JsonPropertyName("confidence")]
public required decimal Confidence { get; init; }
/// <summary>
/// Vulnerable package PURL.
/// </summary>
[JsonPropertyName("vulnerable_purl")]
public required string VulnerablePurl { get; init; }
/// <summary>
/// Fix status if known.
/// </summary>
[JsonPropertyName("fix_status")]
public FixStatusInfo? FixStatus { get; init; }
/// <summary>
/// Similarity score if fingerprint match.
/// </summary>
[JsonPropertyName("similarity")]
public decimal? Similarity { get; init; }
/// <summary>
/// Matched function name if available.
/// </summary>
[JsonPropertyName("matched_function")]
public string? MatchedFunction { get; init; }
}
/// <summary>
/// Fix status information from distro backport detection.
/// </summary>
public sealed record FixStatusInfo
{
/// <summary>
/// Fix state (fixed, vulnerable, not_affected, wontfix, unknown).
/// </summary>
[JsonPropertyName("state")]
public required string State { get; init; }
/// <summary>
/// Version where fix was applied.
/// </summary>
[JsonPropertyName("fixed_version")]
public string? FixedVersion { get; init; }
/// <summary>
/// Detection method (changelog, patch_analysis, advisory).
/// </summary>
[JsonPropertyName("method")]
public required string Method { get; init; }
/// <summary>
/// Confidence in the fix status (0.0-1.0).
/// </summary>
[JsonPropertyName("confidence")]
public required decimal Confidence { get; init; }
}
/// <summary>
/// Scan context metadata.
/// </summary>
public sealed record ScanContextInfo
{
/// <summary>
/// Scan identifier.
/// </summary>
[JsonPropertyName("scan_id")]
public required string ScanId { get; init; }
/// <summary>
/// Container image reference.
/// </summary>
[JsonPropertyName("image_ref")]
public string? ImageRef { get; init; }
/// <summary>
/// Container image digest.
/// </summary>
[JsonPropertyName("image_digest")]
public string? ImageDigest { get; init; }
/// <summary>
/// Detected distribution.
/// </summary>
[JsonPropertyName("distro")]
public string? Distro { get; init; }
/// <summary>
/// Detected distribution release.
/// </summary>
[JsonPropertyName("release")]
public string? Release { get; init; }
/// <summary>
/// Scan timestamp (UTC ISO-8601).
/// </summary>
[JsonPropertyName("scanned_at")]
public required string ScannedAt { get; init; }
}

View File

@@ -0,0 +1,442 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Attestor.ProofChain.Predicates.AI;
using StellaOps.Attestor.ProofChain.MediaTypes;
namespace StellaOps.Attestor.ProofChain.Verification;
/// <summary>
/// Verification step for AI-generated artifacts within proof bundles.
/// Verifies authority classification, model identifiers, determinism, and evidence backing.
/// Sprint: SPRINT_20251226_018_AI_attestations
/// Task: AIATTEST-21
/// </summary>
public sealed class AIArtifactVerificationStep : IVerificationStep
{
private readonly IProofBundleStore _proofStore;
private readonly IAIEvidenceResolver? _evidenceResolver;
private readonly AIAuthorityThresholds _thresholds;
private readonly ILogger _logger;
public string Name => "ai_artifact";
public AIArtifactVerificationStep(
IProofBundleStore proofStore,
ILogger logger,
IAIEvidenceResolver? evidenceResolver = null,
AIAuthorityThresholds? thresholds = null)
{
_proofStore = proofStore ?? throw new ArgumentNullException(nameof(proofStore));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_evidenceResolver = evidenceResolver;
_thresholds = thresholds ?? new AIAuthorityThresholds();
}
public async Task<VerificationStepResult> ExecuteAsync(
VerificationContext context,
CancellationToken ct = default)
{
var stopwatch = Stopwatch.StartNew();
try
{
// Get the proof bundle
var bundle = await _proofStore.GetBundleAsync(context.ProofBundleId, ct);
if (bundle is null)
{
return CreatePassedResult(stopwatch.Elapsed, "No proof bundle found, skipping AI verification");
}
// Find AI artifact statements
var aiStatements = bundle.Statements
.Where(s => IsAIPredicateType(s.PredicateType))
.ToList();
if (aiStatements.Count == 0)
{
// No AI artifacts to verify - pass
return CreatePassedResult(stopwatch.Elapsed, "No AI artifacts in bundle");
}
// Verify each AI artifact
var verificationResults = new List<AIArtifactVerificationResult>();
foreach (var statement in aiStatements)
{
var result = await VerifyAIArtifactAsync(statement, ct);
verificationResults.Add(result);
if (!result.IsValid)
{
return new VerificationStepResult
{
StepName = Name,
Passed = false,
Duration = stopwatch.Elapsed,
ErrorMessage = result.ErrorMessage,
Details = $"AI artifact verification failed for {statement.PredicateType}"
};
}
}
// Store verification results for downstream use
context.SetData("aiArtifactResults", verificationResults);
var summary = BuildVerificationSummary(verificationResults);
return new VerificationStepResult
{
StepName = Name,
Passed = true,
Duration = stopwatch.Elapsed,
Details = summary
};
}
catch (Exception ex)
{
_logger.LogError(ex, "AI artifact verification failed with exception");
return new VerificationStepResult
{
StepName = Name,
Passed = false,
Duration = stopwatch.Elapsed,
ErrorMessage = $"Exception: {ex.Message}"
};
}
}
private async Task<AIArtifactVerificationResult> VerifyAIArtifactAsync(
ProofStatement statement,
CancellationToken ct)
{
var predicateJson = JsonSerializer.Serialize(statement.Predicate);
// Parse base predicate fields
AIArtifactBasePredicate? basePredicate = null;
try
{
basePredicate = statement.PredicateType switch
{
var t when t.Contains("explanation", StringComparison.OrdinalIgnoreCase) =>
JsonSerializer.Deserialize<AIExplanationPredicate>(predicateJson),
var t when t.Contains("remediation", StringComparison.OrdinalIgnoreCase) =>
JsonSerializer.Deserialize<AIRemediationPlanPredicate>(predicateJson),
var t when t.Contains("vexdraft", StringComparison.OrdinalIgnoreCase) =>
JsonSerializer.Deserialize<AIVexDraftPredicate>(predicateJson),
var t when t.Contains("policydraft", StringComparison.OrdinalIgnoreCase) =>
JsonSerializer.Deserialize<AIPolicyDraftPredicate>(predicateJson),
_ => null
};
}
catch (JsonException ex)
{
return new AIArtifactVerificationResult
{
IsValid = false,
ArtifactId = "unknown",
PredicateType = statement.PredicateType,
ErrorMessage = $"Failed to parse AI predicate: {ex.Message}"
};
}
if (basePredicate is null)
{
return new AIArtifactVerificationResult
{
IsValid = false,
ArtifactId = "unknown",
PredicateType = statement.PredicateType,
ErrorMessage = "Unrecognized AI predicate type"
};
}
// Verify artifact ID format
if (!IsValidArtifactId(basePredicate.ArtifactId))
{
return new AIArtifactVerificationResult
{
IsValid = false,
ArtifactId = basePredicate.ArtifactId,
PredicateType = statement.PredicateType,
ErrorMessage = "Invalid artifact ID format (expected sha256:<64-hex-chars>)"
};
}
// Verify model identifier
if (!IsValidModelId(basePredicate.ModelId))
{
return new AIArtifactVerificationResult
{
IsValid = false,
ArtifactId = basePredicate.ArtifactId,
PredicateType = statement.PredicateType,
ErrorMessage = $"Invalid model identifier: {basePredicate.ModelId}"
};
}
// Verify determinism for replay capability
var determinismResult = VerifyDeterminism(basePredicate.DecodingParams);
if (!determinismResult.IsDeterministic)
{
_logger.LogWarning(
"AI artifact {ArtifactId} is not deterministic: {Reason}",
basePredicate.ArtifactId, determinismResult.Reason);
}
// Verify output hash format
if (!IsValidHash(basePredicate.OutputHash))
{
return new AIArtifactVerificationResult
{
IsValid = false,
ArtifactId = basePredicate.ArtifactId,
PredicateType = statement.PredicateType,
ErrorMessage = "Invalid output hash format"
};
}
// Verify input hashes
foreach (var inputHash in basePredicate.InputHashes)
{
if (!IsValidHash(inputHash))
{
return new AIArtifactVerificationResult
{
IsValid = false,
ArtifactId = basePredicate.ArtifactId,
PredicateType = statement.PredicateType,
ErrorMessage = $"Invalid input hash format: {inputHash}"
};
}
}
// Re-classify authority to verify claimed classification
var classifier = new AIAuthorityClassifier(_thresholds, ResolveEvidence);
AIAuthorityClassificationResult? classificationResult = null;
try
{
classificationResult = statement.PredicateType switch
{
var t when t.Contains("explanation", StringComparison.OrdinalIgnoreCase) =>
classifier.ClassifyExplanation(JsonSerializer.Deserialize<AIExplanationPredicate>(predicateJson)!),
var t when t.Contains("remediation", StringComparison.OrdinalIgnoreCase) =>
classifier.ClassifyRemediationPlan(JsonSerializer.Deserialize<AIRemediationPlanPredicate>(predicateJson)!),
var t when t.Contains("vexdraft", StringComparison.OrdinalIgnoreCase) =>
classifier.ClassifyVexDraft(JsonSerializer.Deserialize<AIVexDraftPredicate>(predicateJson)!),
var t when t.Contains("policydraft", StringComparison.OrdinalIgnoreCase) =>
classifier.ClassifyPolicyDraft(JsonSerializer.Deserialize<AIPolicyDraftPredicate>(predicateJson)!),
_ => null
};
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to re-classify AI artifact {ArtifactId}", basePredicate.ArtifactId);
}
// Warn if claimed authority is higher than verified
if (classificationResult is not null &&
basePredicate.Authority > classificationResult.Authority)
{
_logger.LogWarning(
"AI artifact {ArtifactId} claims {Claimed} authority but verification shows {Actual}",
basePredicate.ArtifactId, basePredicate.Authority, classificationResult.Authority);
}
return new AIArtifactVerificationResult
{
IsValid = true,
ArtifactId = basePredicate.ArtifactId,
PredicateType = statement.PredicateType,
ModelId = basePredicate.ModelId.ToString(),
ClaimedAuthority = basePredicate.Authority,
VerifiedAuthority = classificationResult?.Authority,
QualityScore = classificationResult?.QualityScore,
IsDeterministic = determinismResult.IsDeterministic,
CanAutoProcess = classificationResult?.CanAutoProcess ?? false
};
}
private bool ResolveEvidence(string evidenceRef)
{
if (_evidenceResolver is null)
{
// Assume resolvable if no resolver configured
return true;
}
try
{
return _evidenceResolver.CanResolve(evidenceRef);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to resolve evidence ref {Ref}", evidenceRef);
return false;
}
}
private static bool IsAIPredicateType(string predicateType)
{
return predicateType.Contains("ai.", StringComparison.OrdinalIgnoreCase) ||
predicateType.Contains("explanation", StringComparison.OrdinalIgnoreCase) ||
predicateType.Contains("remediation", StringComparison.OrdinalIgnoreCase) ||
predicateType.Contains("vexdraft", StringComparison.OrdinalIgnoreCase) ||
predicateType.Contains("policydraft", StringComparison.OrdinalIgnoreCase) ||
AIArtifactMediaTypes.IsAIArtifactMediaType(predicateType);
}
private static bool IsValidArtifactId(string artifactId)
{
if (string.IsNullOrEmpty(artifactId)) return false;
if (!artifactId.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)) return false;
var hexPart = artifactId[7..];
return hexPart.Length == 64 && hexPart.All(c => Uri.IsHexDigit(c));
}
private static bool IsValidModelId(AIModelIdentifier modelId)
{
return !string.IsNullOrEmpty(modelId.Provider) &&
!string.IsNullOrEmpty(modelId.Model) &&
!string.IsNullOrEmpty(modelId.Version);
}
private static bool IsValidHash(string hash)
{
if (string.IsNullOrEmpty(hash)) return false;
// Support sha256: and sha384: and sha512: prefixes
var parts = hash.Split(':');
if (parts.Length != 2) return false;
var algo = parts[0].ToLowerInvariant();
var hexPart = parts[1];
var expectedLength = algo switch
{
"sha256" => 64,
"sha384" => 96,
"sha512" => 128,
_ => -1
};
if (expectedLength < 0) return false;
return hexPart.Length == expectedLength && hexPart.All(c => Uri.IsHexDigit(c));
}
private static (bool IsDeterministic, string? Reason) VerifyDeterminism(AIDecodingParameters decodingParams)
{
if (decodingParams.Temperature > 0)
{
return (false, $"Temperature {decodingParams.Temperature} > 0");
}
if (!decodingParams.Seed.HasValue)
{
return (false, "No seed specified");
}
return (true, null);
}
private static string BuildVerificationSummary(List<AIArtifactVerificationResult> results)
{
var suggestions = results.Count(r => r.ClaimedAuthority == AIArtifactAuthority.Suggestion);
var evidenceBacked = results.Count(r => r.ClaimedAuthority == AIArtifactAuthority.EvidenceBacked);
var authorityThreshold = results.Count(r => r.ClaimedAuthority == AIArtifactAuthority.AuthorityThreshold);
var deterministic = results.Count(r => r.IsDeterministic);
var autoProcessable = results.Count(r => r.CanAutoProcess);
return $"Verified {results.Count} AI artifact(s): " +
$"{suggestions} suggestion(s), {evidenceBacked} evidence-backed, {authorityThreshold} authority-threshold; " +
$"{deterministic} deterministic, {autoProcessable} auto-processable";
}
private static VerificationStepResult CreatePassedResult(TimeSpan duration, string details) => new()
{
StepName = "ai_artifact",
Passed = true,
Duration = duration,
Details = details
};
}
/// <summary>
/// Result of verifying a single AI artifact.
/// </summary>
public sealed record AIArtifactVerificationResult
{
/// <summary>
/// Whether verification passed.
/// </summary>
public required bool IsValid { get; init; }
/// <summary>
/// Artifact ID that was verified.
/// </summary>
public required string ArtifactId { get; init; }
/// <summary>
/// Predicate type.
/// </summary>
public required string PredicateType { get; init; }
/// <summary>
/// Model identifier string.
/// </summary>
public string? ModelId { get; init; }
/// <summary>
/// Authority claimed by the artifact.
/// </summary>
public AIArtifactAuthority? ClaimedAuthority { get; init; }
/// <summary>
/// Authority determined by verification.
/// </summary>
public AIArtifactAuthority? VerifiedAuthority { get; init; }
/// <summary>
/// Quality score from classification.
/// </summary>
public double? QualityScore { get; init; }
/// <summary>
/// Whether the artifact is deterministic (replayable).
/// </summary>
public bool IsDeterministic { get; init; }
/// <summary>
/// Whether the artifact can be auto-processed without human review.
/// </summary>
public bool CanAutoProcess { get; init; }
/// <summary>
/// Error message if verification failed.
/// </summary>
public string? ErrorMessage { get; init; }
}
/// <summary>
/// Interface for resolving evidence references.
/// </summary>
public interface IAIEvidenceResolver
{
/// <summary>
/// Check if an evidence reference can be resolved.
/// </summary>
bool CanResolve(string evidenceRef);
/// <summary>
/// Resolve an evidence reference and return its content hash.
/// </summary>
Task<string?> ResolveAsync(string evidenceRef, CancellationToken ct = default);
}

View File

@@ -10,6 +10,7 @@ using StellaOps.Attestor.Envelope;
using StellaOps.Attestor.GraphRoot.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Attestor.GraphRoot.Tests;
public class GraphRootAttestorTests
@@ -43,7 +44,8 @@ public class GraphRootAttestorTests
NullLogger<GraphRootAttestor>.Instance);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AttestAsync_ValidRequest_ReturnsResult()
{
// Arrange
@@ -60,7 +62,8 @@ public class GraphRootAttestorTests
Assert.Equal(2, result.EdgeCount);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AttestAsync_SortsNodeIds()
{
// Arrange
@@ -96,7 +99,8 @@ public class GraphRootAttestorTests
Assert.Equal("z-node", thirdNodeId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AttestAsync_SortsEdgeIds()
{
// Arrange
@@ -130,7 +134,8 @@ public class GraphRootAttestorTests
Assert.Equal("z-edge", secondEdgeId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AttestAsync_IncludesInputDigestsInLeaves()
{
// Arrange
@@ -165,14 +170,16 @@ public class GraphRootAttestorTests
Assert.Contains("sha256:params", digestStrings);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AttestAsync_NullRequest_ThrowsArgumentNullException()
{
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => _attestor.AttestAsync(null!));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AttestAsync_KeyResolverReturnsNull_ThrowsInvalidOperationException()
{
// Arrange
@@ -189,7 +196,8 @@ public class GraphRootAttestorTests
Assert.Contains("Unable to resolve signing key", ex.Message);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AttestAsync_CancellationRequested_ThrowsOperationCanceledException()
{
// Arrange
@@ -201,7 +209,8 @@ public class GraphRootAttestorTests
await Assert.ThrowsAsync<OperationCanceledException>(() => _attestor.AttestAsync(request, cts.Token));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AttestAsync_ReturnsCorrectGraphType()
{
// Arrange

View File

@@ -3,11 +3,13 @@ using System.Collections.Generic;
using StellaOps.Attestor.GraphRoot.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Attestor.GraphRoot.Tests;
public class GraphRootModelsTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphRootAttestationRequest_RequiredProperties_Set()
{
// Arrange & Act
@@ -33,7 +35,8 @@ public class GraphRootModelsTests
Assert.Empty(request.EvidenceIds);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphRootAttestationRequest_OptionalProperties_HaveDefaults()
{
// Arrange & Act
@@ -55,7 +58,8 @@ public class GraphRootModelsTests
Assert.Empty(request.EvidenceIds);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphRootPredicate_RequiredProperties_Set()
{
// Arrange & Act
@@ -88,7 +92,8 @@ public class GraphRootModelsTests
Assert.Equal(15, predicate.EdgeCount);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphRootAttestation_HasCorrectDefaults()
{
// Arrange & Act
@@ -129,13 +134,15 @@ public class GraphRootModelsTests
Assert.Equal(GraphRootPredicateTypes.GraphRootV1, attestation.PredicateType);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphRootPredicateTypes_HasCorrectValue()
{
Assert.Equal("https://stella-ops.org/attestation/graph-root/v1", GraphRootPredicateTypes.GraphRootV1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphRootVerificationResult_ValidResult()
{
// Arrange & Act
@@ -155,7 +162,8 @@ public class GraphRootModelsTests
Assert.Equal(5, result.NodeCount);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphRootVerificationResult_InvalidResult_HasReason()
{
// Arrange & Act
@@ -173,7 +181,8 @@ public class GraphRootModelsTests
Assert.NotEqual(result.ExpectedRoot, result.ComputedRoot);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphNodeData_RequiredProperty()
{
// Arrange & Act
@@ -188,7 +197,8 @@ public class GraphRootModelsTests
Assert.Equal("optional content", node.Content);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphEdgeData_AllProperties()
{
// Arrange & Act
@@ -205,7 +215,8 @@ public class GraphRootModelsTests
Assert.Equal("target-node", edge.TargetNodeId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphInputDigests_AllDigests()
{
// Arrange & Act

View File

@@ -24,6 +24,7 @@ using StellaOps.Attestor.Envelope;
using StellaOps.Attestor.GraphRoot.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Attestor.GraphRoot.Tests;
/// <summary>
@@ -123,7 +124,8 @@ public class GraphRootPipelineIntegrationTests
#region Full Pipeline Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_CreateAndVerify_Succeeds()
{
// Arrange
@@ -148,7 +150,8 @@ public class GraphRootPipelineIntegrationTests
Assert.Equal(request.EdgeIds.Count, verifyResult.EdgeCount);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_LargeGraph_Succeeds()
{
// Arrange - Large graph with 1000 nodes and 2000 edges
@@ -167,7 +170,8 @@ public class GraphRootPipelineIntegrationTests
Assert.Equal(2000, verifyResult.EdgeCount);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_AllGraphTypes_Succeed()
{
// Arrange
@@ -197,7 +201,8 @@ public class GraphRootPipelineIntegrationTests
#region Rekor Integration Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_WithRekor_IncludesLogIndex()
{
// Arrange
@@ -245,7 +250,8 @@ public class GraphRootPipelineIntegrationTests
Times.Once);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_RekorFailure_ContinuesWithoutLogIndex()
{
// Arrange
@@ -281,7 +287,8 @@ public class GraphRootPipelineIntegrationTests
Assert.Null(result.RekorLogIndex);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_RekorFailure_ThrowsWhenConfigured()
{
// Arrange
@@ -317,7 +324,8 @@ public class GraphRootPipelineIntegrationTests
#region Tamper Detection Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_ModifiedNode_VerificationFails()
{
// Arrange
@@ -344,7 +352,8 @@ public class GraphRootPipelineIntegrationTests
Assert.NotEqual(verifyResult.ExpectedRoot, verifyResult.ComputedRoot);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_ModifiedEdge_VerificationFails()
{
// Arrange
@@ -369,7 +378,8 @@ public class GraphRootPipelineIntegrationTests
Assert.Contains("Root mismatch", verifyResult.FailureReason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_AddedNode_VerificationFails()
{
// Arrange
@@ -394,7 +404,8 @@ public class GraphRootPipelineIntegrationTests
Assert.NotEqual(request.NodeIds.Count, verifyResult.NodeCount);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_RemovedNode_VerificationFails()
{
// Arrange
@@ -420,7 +431,8 @@ public class GraphRootPipelineIntegrationTests
#region Determinism Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_SameInputs_ProducesSameRoot()
{
// Arrange
@@ -463,7 +475,8 @@ public class GraphRootPipelineIntegrationTests
Assert.Equal(result1.RootHash, result2.RootHash);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FullPipeline_DifferentNodeOrder_ProducesSameRoot()
{
// Arrange
@@ -507,7 +520,8 @@ public class GraphRootPipelineIntegrationTests
#region DI Integration Tests
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DependencyInjection_RegistersServices()
{
// Arrange

View File

@@ -2,19 +2,22 @@ using System;
using System.Collections.Generic;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Attestor.GraphRoot.Tests;
public class Sha256MerkleRootComputerTests
{
private readonly Sha256MerkleRootComputer _computer = new();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Algorithm_ReturnsSha256()
{
Assert.Equal("sha256", _computer.Algorithm);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeRoot_SingleLeaf_ReturnsHash()
{
// Arrange
@@ -29,7 +32,8 @@ public class Sha256MerkleRootComputerTests
Assert.Equal(32, root.Length); // SHA-256 produces 32 bytes
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeRoot_TwoLeaves_CombinesCorrectly()
{
// Arrange
@@ -45,7 +49,8 @@ public class Sha256MerkleRootComputerTests
Assert.Equal(32, root.Length);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeRoot_OddLeaves_DuplicatesLast()
{
// Arrange
@@ -64,7 +69,8 @@ public class Sha256MerkleRootComputerTests
Assert.Equal(32, root.Length);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeRoot_Deterministic_SameInputSameOutput()
{
// Arrange
@@ -84,7 +90,8 @@ public class Sha256MerkleRootComputerTests
Assert.Equal(root1, root2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeRoot_DifferentInputs_DifferentOutputs()
{
// Arrange
@@ -99,7 +106,8 @@ public class Sha256MerkleRootComputerTests
Assert.NotEqual(root1, root2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeRoot_OrderMatters()
{
// Arrange
@@ -122,7 +130,8 @@ public class Sha256MerkleRootComputerTests
Assert.NotEqual(rootAB, rootBA);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeRoot_EmptyList_ThrowsArgumentException()
{
// Arrange
@@ -132,14 +141,16 @@ public class Sha256MerkleRootComputerTests
Assert.Throws<ArgumentException>(() => _computer.ComputeRoot(leaves));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeRoot_NullInput_ThrowsArgumentNullException()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => _computer.ComputeRoot(null!));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeRoot_LargeTree_HandlesCorrectly()
{
// Arrange - create 100 leaves
@@ -157,7 +168,8 @@ public class Sha256MerkleRootComputerTests
Assert.Equal(32, root.Length);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeRoot_PowerOfTwo_HandlesCorrectly()
{
// Arrange - 8 leaves (power of 2)