feat: Implement Policy Engine Evaluation Service and Cache with unit tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

Temp commit to debug
This commit is contained in:
master
2025-11-05 07:35:53 +00:00
parent 40e7f827da
commit 9253620833
125 changed files with 18735 additions and 17215 deletions

View File

@@ -0,0 +1,279 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Storage;
using StellaOps.Scanner.Storage.Catalog;
using StellaOps.Scanner.Storage.ObjectStore;
using StellaOps.Scanner.Storage.Repositories;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Options;
namespace StellaOps.Scanner.WebService.Services;
internal interface ISurfacePointerService
{
Task<SurfacePointersDto?> TryBuildAsync(string imageDigest, CancellationToken cancellationToken);
}
internal sealed class SurfacePointerService : ISurfacePointerService
{
private static readonly JsonSerializerOptions ManifestSerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
};
private readonly LinkRepository _linkRepository;
private readonly ArtifactRepository _artifactRepository;
private readonly IOptionsMonitor<ScannerWebServiceOptions> _optionsMonitor;
private readonly ISurfaceEnvironment _surfaceEnvironment;
private readonly TimeProvider _timeProvider;
private readonly ILogger<SurfacePointerService> _logger;
public SurfacePointerService(
LinkRepository linkRepository,
ArtifactRepository artifactRepository,
IOptionsMonitor<ScannerWebServiceOptions> optionsMonitor,
ISurfaceEnvironment surfaceEnvironment,
TimeProvider timeProvider,
ILogger<SurfacePointerService> logger)
{
_linkRepository = linkRepository ?? throw new ArgumentNullException(nameof(linkRepository));
_artifactRepository = artifactRepository ?? throw new ArgumentNullException(nameof(artifactRepository));
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
_surfaceEnvironment = surfaceEnvironment ?? throw new ArgumentNullException(nameof(surfaceEnvironment));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<SurfacePointersDto?> TryBuildAsync(string imageDigest, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(imageDigest))
{
return null;
}
var normalizedDigest = imageDigest.Trim();
List<LinkDocument> links;
try
{
links = await _linkRepository.ListBySourceAsync(LinkSourceType.Image, normalizedDigest, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load link documents for digest {Digest}.", normalizedDigest);
return null;
}
if (links.Count == 0)
{
return null;
}
var options = _optionsMonitor.CurrentValue ?? new ScannerWebServiceOptions();
var artifactStore = options.ArtifactStore ?? new ScannerWebServiceOptions.ArtifactStoreOptions();
var bucket = ResolveBucket(artifactStore);
var rootPrefix = artifactStore.RootPrefix ?? ScannerStorageDefaults.DefaultRootPrefix;
var tenant = _surfaceEnvironment.Settings.Tenant;
var generatedAt = _timeProvider.GetUtcNow();
var artifacts = ImmutableArray.CreateBuilder<SurfaceManifestArtifact>();
foreach (var link in links)
{
cancellationToken.ThrowIfCancellationRequested();
ArtifactDocument? artifactDocument;
try
{
artifactDocument = await _artifactRepository.GetAsync(link.ArtifactId, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load artifact document {ArtifactId}.", link.ArtifactId);
continue;
}
if (artifactDocument is null)
{
continue;
}
var objectKey = ArtifactObjectKeyBuilder.Build(
artifactDocument.Type,
artifactDocument.Format,
artifactDocument.BytesSha256,
rootPrefix);
var uri = BuildCasUri(bucket, objectKey);
var (kind, view) = MapKindAndView(artifactDocument);
var format = MapFormat(artifactDocument.Format);
var artifact = new SurfaceManifestArtifact
{
Kind = kind,
Uri = uri,
Digest = artifactDocument.BytesSha256,
MediaType = artifactDocument.MediaType,
Format = format,
SizeBytes = artifactDocument.SizeBytes,
View = view
};
artifacts.Add(artifact);
}
if (artifacts.Count == 0)
{
return null;
}
var orderedArtifacts = artifacts.OrderBy(a => a.Kind, StringComparer.Ordinal)
.ThenBy(a => a.Format, StringComparer.Ordinal)
.ThenBy(a => a.Digest, StringComparer.Ordinal)
.ToImmutableArray();
var manifest = new SurfaceManifestDocument
{
Tenant = tenant,
ImageDigest = normalizedDigest,
GeneratedAt = generatedAt,
Artifacts = orderedArtifacts
};
var manifestJson = JsonSerializer.SerializeToUtf8Bytes(manifest, ManifestSerializerOptions);
var manifestDigest = ComputeDigest(manifestJson);
var manifestUri = BuildManifestUri(bucket, rootPrefix, tenant, manifestDigest);
return new SurfacePointersDto
{
Tenant = tenant,
GeneratedAt = generatedAt,
ManifestDigest = manifestDigest,
ManifestUri = manifestUri,
Manifest = manifest with { GeneratedAt = generatedAt }
};
}
private static string ResolveBucket(ScannerWebServiceOptions.ArtifactStoreOptions artifactStore)
{
if (!string.IsNullOrWhiteSpace(artifactStore.Bucket))
{
return artifactStore.Bucket.Trim();
}
return ScannerStorageDefaults.DefaultBucketName;
}
private static string MapFormat(ArtifactDocumentFormat format)
=> format switch
{
ArtifactDocumentFormat.CycloneDxJson => "cdx-json",
ArtifactDocumentFormat.CycloneDxProtobuf => "cdx-protobuf",
ArtifactDocumentFormat.SpdxJson => "spdx-json",
ArtifactDocumentFormat.BomIndex => "bom-index",
ArtifactDocumentFormat.DsseJson => "dsse-json",
_ => format.ToString().ToLowerInvariant()
};
private static (string Kind, string? View) MapKindAndView(ArtifactDocument document)
{
if (document.Type == ArtifactDocumentType.ImageBom)
{
var view = ResolveView(document.MediaType);
var kind = string.Equals(view, "usage", StringComparison.OrdinalIgnoreCase)
? "sbom-usage"
: "sbom-inventory";
return (kind, view);
}
return document.Type switch
{
ArtifactDocumentType.LayerBom => ("layer-sbom", null),
ArtifactDocumentType.Diff => ("diff", null),
ArtifactDocumentType.Attestation => ("attestation", null),
ArtifactDocumentType.Index => ("bom-index", null),
_ => (document.Type.ToString().ToLowerInvariant(), null)
};
}
private static string? ResolveView(string mediaType)
{
if (string.IsNullOrWhiteSpace(mediaType))
{
return null;
}
if (mediaType.Contains("view=usage", StringComparison.OrdinalIgnoreCase))
{
return "usage";
}
if (mediaType.Contains("view=inventory", StringComparison.OrdinalIgnoreCase))
{
return "inventory";
}
return null;
}
private static string BuildCasUri(string bucket, string key)
{
var normalizedKey = string.IsNullOrWhiteSpace(key) ? string.Empty : key.Trim().TrimStart('/');
return $"cas://{bucket}/{normalizedKey}";
}
private static string BuildManifestUri(string bucket, string rootPrefix, string tenant, string manifestDigest)
{
var (algorithm, digestValue) = SplitDigest(manifestDigest);
var prefix = string.IsNullOrWhiteSpace(rootPrefix)
? "surface/manifests"
: $"{TrimTrailingSlash(rootPrefix)}/surface/manifests";
var head = digestValue.Length >= 4
? $"{digestValue[..2]}/{digestValue[2..4]}"
: digestValue;
var key = $"{prefix}/{tenant}/{algorithm}/{head}/{digestValue}.json";
return $"cas://{bucket}/{key}";
}
private static (string Algorithm, string Digest) SplitDigest(string digest)
{
if (string.IsNullOrWhiteSpace(digest))
{
return ("sha256", digest ?? string.Empty);
}
var parts = digest.Split(':', 2, StringSplitOptions.TrimEntries);
if (parts.Length == 2)
{
return (parts[0], parts[1]);
}
return ("sha256", digest);
}
private static string TrimTrailingSlash(string value)
=> string.IsNullOrWhiteSpace(value)
? string.Empty
: value.Trim().TrimEnd('/');
private static string ComputeDigest(ReadOnlySpan<byte> payload)
{
Span<byte> hash = stackalloc byte[32];
if (!SHA256.TryHashData(payload, hash, out _))
{
using var sha = SHA256.Create();
hash = sha.ComputeHash(payload.ToArray());
}
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}