using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.IO.Abstractions; using System.IO.Compression; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Net.Http; using Microsoft.Extensions.Logging; using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Authentication; using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Configuration; using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Discovery; using System.Formats.Tar; namespace StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Fetch; public sealed class OciAttestationFetcher { private readonly IHttpClientFactory _httpClientFactory; private readonly IFileSystem _fileSystem; private readonly ILogger _logger; public OciAttestationFetcher( IHttpClientFactory httpClientFactory, IFileSystem fileSystem, ILogger logger) { _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async IAsyncEnumerable FetchAsync( OciAttestationDiscoveryResult discovery, OciOpenVexAttestationConnectorOptions options, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(discovery); ArgumentNullException.ThrowIfNull(options); foreach (var target in discovery.Targets) { cancellationToken.ThrowIfCancellationRequested(); bool yieldedOffline = false; if (target.OfflineBundle is not null && target.OfflineBundle.Exists) { await foreach (var offlineDocument in ReadOfflineAsync(target, cancellationToken)) { yieldedOffline = true; yield return offlineDocument; } if (!discovery.AllowNetworkFallback) { continue; } } if (discovery.PreferOffline && yieldedOffline && !discovery.AllowNetworkFallback) { continue; } if (!discovery.PreferOffline || discovery.AllowNetworkFallback || !yieldedOffline) { await foreach (var registryDocument in FetchFromRegistryAsync(discovery, options, target, cancellationToken)) { yield return registryDocument; } } } } private async IAsyncEnumerable ReadOfflineAsync( OciAttestationTarget target, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { var offline = target.OfflineBundle!; var path = _fileSystem.Path.GetFullPath(offline.Path); if (!_fileSystem.File.Exists(path)) { if (offline.Exists) { _logger.LogWarning("Offline bundle {Path} disappeared before processing.", path); } yield break; } var extension = _fileSystem.Path.GetExtension(path).ToLowerInvariant(); var subjectDigest = target.Image.Digest ?? target.ExpectedSubjectDigest; if (string.Equals(extension, ".json", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".dsse", StringComparison.OrdinalIgnoreCase)) { var bytes = await _fileSystem.File.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false); var metadata = BuildOfflineMetadata(target, path, entryName: null, subjectDigest); yield return new OciAttestationDocument( new Uri(path, UriKind.Absolute), bytes, metadata, subjectDigest, null, null, "offline"); yield break; } if (string.Equals(extension, ".tgz", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".gz", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".tar", StringComparison.OrdinalIgnoreCase)) { await foreach (var document in ReadTarArchiveAsync(target, path, subjectDigest, cancellationToken)) { yield return document; } yield break; } // Default: treat as binary blob. var fallbackBytes = await _fileSystem.File.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false); var fallbackMetadata = BuildOfflineMetadata(target, path, entryName: null, subjectDigest); yield return new OciAttestationDocument( new Uri(path, UriKind.Absolute), fallbackBytes, fallbackMetadata, subjectDigest, null, null, "offline"); } private async IAsyncEnumerable ReadTarArchiveAsync( OciAttestationTarget target, string path, string? subjectDigest, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { await using var fileStream = _fileSystem.File.OpenRead(path); Stream archiveStream = fileStream; if (path.EndsWith(".gz", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) { archiveStream = new GZipStream(fileStream, CompressionMode.Decompress, leaveOpen: false); } using var tarReader = new TarReader(archiveStream, leaveOpen: false); TarEntry? entry; while ((entry = await tarReader.GetNextEntryAsync(copyData: false, cancellationToken).ConfigureAwait(false)) is not null) { if (entry.EntryType is not TarEntryType.RegularFile || entry.DataStream is null) { continue; } await using var entryStream = entry.DataStream; using var buffer = new MemoryStream(); await entryStream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false); var metadata = BuildOfflineMetadata(target, path, entry.Name, subjectDigest); var sourceUri = new Uri($"{_fileSystem.Path.GetFullPath(path)}#{entry.Name}", UriKind.Absolute); yield return new OciAttestationDocument( sourceUri, buffer.ToArray(), metadata, subjectDigest, null, null, "offline"); } } private async IAsyncEnumerable FetchFromRegistryAsync( OciAttestationDiscoveryResult discovery, OciOpenVexAttestationConnectorOptions options, OciAttestationTarget target, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { var registryClient = new OciRegistryClient( _httpClientFactory, _logger, discovery.RegistryAuthorization, options); var subjectDigest = target.Image.Digest ?? target.ExpectedSubjectDigest; if (string.IsNullOrWhiteSpace(subjectDigest)) { subjectDigest = await registryClient.ResolveDigestAsync(target.Image, cancellationToken).ConfigureAwait(false); } if (string.IsNullOrWhiteSpace(subjectDigest)) { _logger.LogWarning("Unable to resolve subject digest for {Reference}; skipping registry fetch.", target.Image.Canonical); yield break; } if (!string.IsNullOrWhiteSpace(target.ExpectedSubjectDigest) && !string.Equals(target.ExpectedSubjectDigest, subjectDigest, StringComparison.OrdinalIgnoreCase)) { _logger.LogWarning( "Resolved digest {Resolved} does not match expected digest {Expected} for {Reference}.", subjectDigest, target.ExpectedSubjectDigest, target.Image.Canonical); } var descriptors = await registryClient.ListReferrersAsync(target.Image, subjectDigest, cancellationToken).ConfigureAwait(false); if (descriptors.Count == 0) { yield break; } foreach (var descriptor in descriptors) { cancellationToken.ThrowIfCancellationRequested(); var document = await registryClient.DownloadAttestationAsync(target.Image, descriptor, subjectDigest, cancellationToken).ConfigureAwait(false); if (document is not null) { yield return document; } } } private static ImmutableDictionary BuildOfflineMetadata( OciAttestationTarget target, string bundlePath, string? entryName, string? subjectDigest) { var builder = ImmutableDictionary.CreateBuilder(StringComparer.Ordinal); builder["oci.image.registry"] = target.Image.Registry; builder["oci.image.repository"] = target.Image.Repository; builder["oci.image.reference"] = target.Image.Canonical; if (!string.IsNullOrWhiteSpace(subjectDigest)) { builder["oci.image.subjectDigest"] = subjectDigest; } if (!string.IsNullOrWhiteSpace(target.ExpectedSubjectDigest)) { builder["oci.image.expectedSubjectDigest"] = target.ExpectedSubjectDigest!; } builder["oci.attestation.sourceKind"] = "offline"; builder["oci.attestation.source"] = bundlePath; if (!string.IsNullOrWhiteSpace(entryName)) { builder["oci.attestation.bundleEntry"] = entryName!; } return builder.ToImmutable(); } }