Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user