123 lines
3.8 KiB
C#
123 lines
3.8 KiB
C#
|
|
|
|
using StellaOps.EvidenceLocker.Core.Builders;
|
|
using StellaOps.EvidenceLocker.Core.Domain;
|
|
using StellaOps.EvidenceLocker.Core.Repositories;
|
|
using System.Collections.Immutable;
|
|
using System.Text;
|
|
|
|
namespace StellaOps.EvidenceLocker.Infrastructure.Builders;
|
|
|
|
internal sealed class EvidenceBundleBuilder(
|
|
IEvidenceBundleRepository repository,
|
|
IMerkleTreeCalculator merkleTreeCalculator) : IEvidenceBundleBuilder
|
|
{
|
|
public async Task<EvidenceBundleBuildResult> BuildAsync(
|
|
EvidenceBundleBuildRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
var manifest = CreateManifest(request);
|
|
var rootHash = merkleTreeCalculator.CalculateRootHash(
|
|
manifest.Entries.Select(entry => $"{entry.CanonicalPath}|{entry.Sha256}"));
|
|
|
|
await repository.SetBundleAssemblyAsync(
|
|
request.BundleId,
|
|
request.TenantId,
|
|
EvidenceBundleStatus.Sealed,
|
|
rootHash,
|
|
request.CreatedAt,
|
|
cancellationToken);
|
|
|
|
return new EvidenceBundleBuildResult(rootHash, manifest);
|
|
}
|
|
|
|
private static EvidenceBundleManifest CreateManifest(EvidenceBundleBuildRequest request)
|
|
{
|
|
var normalizedMetadata = request.Metadata?.OrderBy(pair => pair.Key, StringComparer.Ordinal)
|
|
.ToImmutableDictionary(pair => pair.Key, pair => pair.Value)
|
|
?? ImmutableDictionary<string, string>.Empty;
|
|
|
|
var entries = request.Materials?
|
|
.Select(material => CreateEntry(material))
|
|
.OrderBy(entry => entry.CanonicalPath, StringComparer.Ordinal)
|
|
.ToImmutableArray() ?? ImmutableArray<EvidenceManifestEntry>.Empty;
|
|
|
|
return new EvidenceBundleManifest(
|
|
request.BundleId,
|
|
request.TenantId,
|
|
request.Kind,
|
|
request.CreatedAt,
|
|
normalizedMetadata,
|
|
entries);
|
|
}
|
|
|
|
private static EvidenceManifestEntry CreateEntry(EvidenceBundleMaterial material)
|
|
{
|
|
var canonicalSection = NormalizeSection(material.Section);
|
|
var canonicalPath = $"{canonicalSection}/{NormalizePath(material.Path)}";
|
|
|
|
var attributes = material.Attributes?
|
|
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
|
|
.ToImmutableDictionary(pair => pair.Key, pair => pair.Value)
|
|
?? ImmutableDictionary<string, string>.Empty;
|
|
|
|
return new EvidenceManifestEntry(
|
|
canonicalSection,
|
|
canonicalPath,
|
|
material.Sha256.ToLowerInvariant(),
|
|
material.SizeBytes,
|
|
material.MediaType,
|
|
attributes);
|
|
}
|
|
|
|
private static string NormalizeSection(string value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return "root";
|
|
}
|
|
|
|
return NormalizeSegment(value);
|
|
}
|
|
|
|
private static string NormalizePath(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
return "item";
|
|
}
|
|
|
|
var segments = value.Split(['/','\\'], StringSplitOptions.RemoveEmptyEntries);
|
|
var normalized = segments
|
|
.Where(segment => segment is not "." and not "..")
|
|
.Select(NormalizeSegment);
|
|
return string.Join('/', normalized);
|
|
}
|
|
|
|
private static string NormalizeSegment(string value)
|
|
{
|
|
Span<char> buffer = stackalloc char[value.Length];
|
|
var index = 0;
|
|
|
|
foreach (var ch in value.Trim())
|
|
{
|
|
if (char.IsLetterOrDigit(ch))
|
|
{
|
|
buffer[index++] = char.ToLowerInvariant(ch);
|
|
}
|
|
else if (ch is '-' or '_' || ch == '.')
|
|
{
|
|
buffer[index++] = ch;
|
|
}
|
|
else
|
|
{
|
|
buffer[index++] = '-';
|
|
}
|
|
}
|
|
|
|
return index > 0 ? new string(buffer[..index]) : "item";
|
|
}
|
|
}
|