sprints work
This commit is contained in:
256
src/__Libraries/StellaOps.Evidence.Pack/EvidenceResolver.cs
Normal file
256
src/__Libraries/StellaOps.Evidence.Pack/EvidenceResolver.cs
Normal file
@@ -0,0 +1,256 @@
|
||||
// <copyright file="EvidenceResolver.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Evidence.Pack.Models;
|
||||
|
||||
namespace StellaOps.Evidence.Pack;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IEvidenceResolver"/> with pluggable type resolvers.
|
||||
/// Sprint: SPRINT_20260109_011_005 Task: EVPK-004
|
||||
/// </summary>
|
||||
internal sealed class EvidenceResolver : IEvidenceResolver
|
||||
{
|
||||
private readonly ImmutableDictionary<string, ITypeResolver> _resolvers;
|
||||
private readonly ILogger<EvidenceResolver> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new EvidenceResolver.
|
||||
/// </summary>
|
||||
public EvidenceResolver(
|
||||
IEnumerable<ITypeResolver> resolvers,
|
||||
ILogger<EvidenceResolver> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
var builder = ImmutableDictionary.CreateBuilder<string, ITypeResolver>(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var resolver in resolvers)
|
||||
{
|
||||
foreach (var type in resolver.SupportedTypes)
|
||||
{
|
||||
builder[type] = resolver;
|
||||
}
|
||||
}
|
||||
|
||||
_resolvers = builder.ToImmutable();
|
||||
_logger.LogDebug("Initialized EvidenceResolver with {Count} type resolvers", _resolvers.Count);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ResolvedEvidence> ResolveAndSnapshotAsync(
|
||||
string type,
|
||||
string path,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("Resolving evidence: type={Type}, path={Path}", type, path);
|
||||
|
||||
if (!_resolvers.TryGetValue(type, out var resolver))
|
||||
{
|
||||
throw new UnsupportedEvidenceTypeException(type, _resolvers.Keys);
|
||||
}
|
||||
|
||||
var snapshot = await resolver.ResolveAsync(type, path, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var digest = ComputeSnapshotDigest(snapshot);
|
||||
|
||||
_logger.LogDebug("Resolved evidence: type={Type}, digest={Digest}", type, digest);
|
||||
|
||||
return new ResolvedEvidence
|
||||
{
|
||||
Snapshot = snapshot,
|
||||
Digest = digest,
|
||||
ResolvedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<EvidenceResolutionResult> VerifyEvidenceAsync(
|
||||
EvidenceItem evidence,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("Verifying evidence: {EvidenceId}", evidence.EvidenceId);
|
||||
|
||||
try
|
||||
{
|
||||
// Parse the URI to get type and path
|
||||
var (type, path) = ParseStellaUri(evidence.Uri);
|
||||
|
||||
if (!_resolvers.TryGetValue(type, out var resolver))
|
||||
{
|
||||
return new EvidenceResolutionResult
|
||||
{
|
||||
EvidenceId = evidence.EvidenceId,
|
||||
Uri = evidence.Uri,
|
||||
Resolved = false,
|
||||
DigestMatches = false,
|
||||
Error = $"Unsupported evidence type: {type}"
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve current state
|
||||
var currentSnapshot = await resolver.ResolveAsync(type, path, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var currentDigest = ComputeSnapshotDigest(currentSnapshot);
|
||||
var digestMatches = string.Equals(
|
||||
evidence.Digest,
|
||||
currentDigest,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Evidence {EvidenceId} verification: digestMatches={DigestMatches}",
|
||||
evidence.EvidenceId, digestMatches);
|
||||
|
||||
return new EvidenceResolutionResult
|
||||
{
|
||||
EvidenceId = evidence.EvidenceId,
|
||||
Uri = evidence.Uri,
|
||||
Resolved = true,
|
||||
DigestMatches = digestMatches,
|
||||
Error = digestMatches ? null : "Digest mismatch - evidence has changed since collection"
|
||||
};
|
||||
}
|
||||
catch (EvidenceNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Evidence not found: {EvidenceId}", evidence.EvidenceId);
|
||||
return new EvidenceResolutionResult
|
||||
{
|
||||
EvidenceId = evidence.EvidenceId,
|
||||
Uri = evidence.Uri,
|
||||
Resolved = false,
|
||||
DigestMatches = false,
|
||||
Error = ex.Message
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to verify evidence: {EvidenceId}", evidence.EvidenceId);
|
||||
return new EvidenceResolutionResult
|
||||
{
|
||||
EvidenceId = evidence.EvidenceId,
|
||||
Uri = evidence.Uri,
|
||||
Resolved = false,
|
||||
DigestMatches = false,
|
||||
Error = $"Verification failed: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SupportsType(string type)
|
||||
{
|
||||
return _resolvers.ContainsKey(type);
|
||||
}
|
||||
|
||||
private static (string type, string path) ParseStellaUri(string uri)
|
||||
{
|
||||
// Expected format: stella://type/path
|
||||
if (!uri.StartsWith("stella://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException($"Invalid evidence URI format: {uri}. Expected stella://type/path");
|
||||
}
|
||||
|
||||
var withoutScheme = uri[9..]; // Remove "stella://"
|
||||
var slashIndex = withoutScheme.IndexOf('/', StringComparison.Ordinal);
|
||||
|
||||
if (slashIndex <= 0)
|
||||
{
|
||||
throw new ArgumentException($"Invalid evidence URI format: {uri}. Missing path.");
|
||||
}
|
||||
|
||||
var type = withoutScheme[..slashIndex];
|
||||
var path = withoutScheme[(slashIndex + 1)..];
|
||||
|
||||
return (type, path);
|
||||
}
|
||||
|
||||
private static string ComputeSnapshotDigest(EvidenceSnapshot snapshot)
|
||||
{
|
||||
// Canonical JSON for digest computation
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = snapshot.Type,
|
||||
data = snapshot.Data.OrderBy(kvp => kvp.Key, StringComparer.Ordinal)
|
||||
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
|
||||
}, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
});
|
||||
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
|
||||
return $"sha256:{Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for type-specific evidence resolvers.
|
||||
/// </summary>
|
||||
public interface ITypeResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the evidence types this resolver can handle.
|
||||
/// </summary>
|
||||
IEnumerable<string> SupportedTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves evidence and creates a snapshot.
|
||||
/// </summary>
|
||||
Task<EvidenceSnapshot> ResolveAsync(
|
||||
string type,
|
||||
string path,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when an evidence type is not supported.
|
||||
/// </summary>
|
||||
public sealed class UnsupportedEvidenceTypeException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new UnsupportedEvidenceTypeException.
|
||||
/// </summary>
|
||||
public UnsupportedEvidenceTypeException(string type, IEnumerable<string> supportedTypes)
|
||||
: base($"Unsupported evidence type: {type}. Supported types: {string.Join(", ", supportedTypes)}")
|
||||
{
|
||||
Type = type;
|
||||
SupportedTypes = supportedTypes.ToImmutableArray();
|
||||
}
|
||||
|
||||
/// <summary>Gets the unsupported type.</summary>
|
||||
public string Type { get; }
|
||||
|
||||
/// <summary>Gets the supported types.</summary>
|
||||
public ImmutableArray<string> SupportedTypes { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when evidence cannot be found.
|
||||
/// </summary>
|
||||
public sealed class EvidenceNotFoundException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new EvidenceNotFoundException.
|
||||
/// </summary>
|
||||
public EvidenceNotFoundException(string type, string path)
|
||||
: base($"Evidence not found: {type}/{path}")
|
||||
{
|
||||
Type = type;
|
||||
Path = path;
|
||||
}
|
||||
|
||||
/// <summary>Gets the evidence type.</summary>
|
||||
public string Type { get; }
|
||||
|
||||
/// <summary>Gets the evidence path.</summary>
|
||||
public string Path { get; }
|
||||
}
|
||||
Reference in New Issue
Block a user