save work
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using StellaOps.Scanner.Analyzers.Native.Index;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Native;
|
||||
|
||||
@@ -17,7 +20,143 @@ public sealed record NativeComponentEmitResult(
|
||||
string? Version,
|
||||
NativeBinaryMetadata Metadata,
|
||||
bool IndexMatch,
|
||||
BuildIdLookupResult? LookupResult);
|
||||
BuildIdLookupResult? LookupResult)
|
||||
{
|
||||
public ComponentRecord ToComponentRecord(string layerDigest)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(layerDigest);
|
||||
ArgumentNullException.ThrowIfNull(Metadata);
|
||||
|
||||
var fileName = string.IsNullOrWhiteSpace(Name)
|
||||
? Path.GetFileName(Metadata.FilePath)
|
||||
: Name.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
fileName = Purl;
|
||||
}
|
||||
|
||||
var properties = new SortedDictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["stellaops:binary.format"] = Metadata.Format,
|
||||
["stellaops:binary.indexMatch"] = IndexMatch ? "true" : "false",
|
||||
};
|
||||
|
||||
AddIfNotEmpty(properties, "stellaops:binary.architecture", Metadata.Architecture);
|
||||
AddIfNotEmpty(properties, "stellaops:binary.platform", Metadata.Platform);
|
||||
AddIfNotEmpty(properties, "stellaops:binary.filePath", Metadata.FilePath);
|
||||
AddIfNotEmpty(properties, "stellaops:binary.fileDigest", Metadata.FileDigest);
|
||||
|
||||
if (Metadata.FileSize > 0)
|
||||
{
|
||||
properties["stellaops:binary.fileSizeBytes"] = Metadata.FileSize.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (Metadata.LayerIndex >= 0)
|
||||
{
|
||||
properties["stellaops:binary.layerIndex"] = Metadata.LayerIndex.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (Metadata.Is64Bit)
|
||||
{
|
||||
properties["stellaops:binary.is64Bit"] = "true";
|
||||
}
|
||||
|
||||
if (Metadata.IsSigned)
|
||||
{
|
||||
properties["stellaops:binary.isSigned"] = "true";
|
||||
}
|
||||
|
||||
AddIfNotEmpty(properties, "stellaops:binary.signatureDetails", Metadata.SignatureDetails);
|
||||
AddIfNotEmpty(properties, "stellaops:binary.productVersion", Metadata.ProductVersion);
|
||||
AddIfNotEmpty(properties, "stellaops:binary.fileVersion", Metadata.FileVersion);
|
||||
AddIfNotEmpty(properties, "stellaops:binary.companyName", Metadata.CompanyName);
|
||||
|
||||
AddDictionary(properties, "stellaops:binary.hardeningFlags", Metadata.HardeningFlags);
|
||||
AddList(properties, "stellaops:binary.imports", Metadata.Imports);
|
||||
AddList(properties, "stellaops:binary.exports", Metadata.Exports);
|
||||
|
||||
if (LookupResult is not null)
|
||||
{
|
||||
AddIfNotEmpty(properties, "stellaops:binary.index.sourceDistro", LookupResult.SourceDistro);
|
||||
properties["stellaops:binary.index.confidence"] = LookupResult.Confidence.ToString();
|
||||
}
|
||||
|
||||
var componentMetadata = new ComponentMetadata
|
||||
{
|
||||
BuildId = Metadata.BuildId,
|
||||
Properties = properties.Count == 0 ? null : properties,
|
||||
};
|
||||
|
||||
return new ComponentRecord
|
||||
{
|
||||
Identity = ComponentIdentity.Create(
|
||||
key: Purl,
|
||||
name: fileName,
|
||||
version: Version,
|
||||
purl: Purl,
|
||||
componentType: "file"),
|
||||
LayerDigest = layerDigest,
|
||||
Evidence = ImmutableArray.Create(ComponentEvidence.FromPath(Metadata.FilePath)),
|
||||
Dependencies = ImmutableArray<string>.Empty,
|
||||
Metadata = componentMetadata,
|
||||
Usage = ComponentUsage.Unused,
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddIfNotEmpty(IDictionary<string, string> properties, string key, string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
properties[key] = value.Trim();
|
||||
}
|
||||
|
||||
private static void AddDictionary(IDictionary<string, string> properties, string key, IReadOnlyDictionary<string, string>? dictionary)
|
||||
{
|
||||
if (dictionary is null || dictionary.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entries = dictionary
|
||||
.Where(pair => !string.IsNullOrWhiteSpace(pair.Key) && !string.IsNullOrWhiteSpace(pair.Value))
|
||||
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
|
||||
.Select(pair => $"{pair.Key}={pair.Value}")
|
||||
.ToArray();
|
||||
|
||||
if (entries.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
properties[key] = string.Join(",", entries);
|
||||
}
|
||||
|
||||
private static void AddList(IDictionary<string, string> properties, string key, IReadOnlyList<string>? items)
|
||||
{
|
||||
if (items is null || items.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalized = items
|
||||
.Where(static item => !string.IsNullOrWhiteSpace(item))
|
||||
.Select(static item => item.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(static item => item, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
properties[key] = string.Join(",", normalized);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for emitting native binary components for SBOM generation.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Analyzers.Native.Index;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Native;
|
||||
|
||||
@@ -183,7 +184,15 @@ public sealed record LayerComponentMapping(
|
||||
IReadOnlyList<NativeComponentEmitResult> Components,
|
||||
int TotalCount,
|
||||
int ResolvedCount,
|
||||
int UnresolvedCount);
|
||||
int UnresolvedCount)
|
||||
{
|
||||
public LayerComponentFragment ToFragment()
|
||||
{
|
||||
return LayerComponentFragment.Create(
|
||||
LayerDigest,
|
||||
Components.Select(component => component.ToComponentRecord(LayerDigest)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of mapping an entire container image to SBOM components.
|
||||
|
||||
5
src/Scanner/__Libraries/StellaOps.Scanner.Emit/TASKS.md
Normal file
5
src/Scanner/__Libraries/StellaOps.Scanner.Emit/TASKS.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Scanner Emit Local Tasks
|
||||
|
||||
| Task ID | Sprint | Status | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `BSE-009` | `docs/implplan/SPRINT_3500_0012_0001_binary_sbom_emission.md` | DONE | Added end-to-end integration test coverage for native binary SBOM emission (emit → fragments → CycloneDX). |
|
||||
@@ -6,6 +6,12 @@
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Cache.Abstractions;
|
||||
using StellaOps.Scanner.ProofSpine;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Attestation;
|
||||
|
||||
@@ -25,7 +31,16 @@ public static class ReachabilityAttestationServiceCollectionExtensions
|
||||
services.TryAddSingleton<ReachabilityWitnessDsseBuilder>();
|
||||
|
||||
// Register publisher
|
||||
services.TryAddSingleton<IReachabilityWitnessPublisher, ReachabilityWitnessPublisher>();
|
||||
services.TryAddSingleton<IReachabilityWitnessPublisher>(sp =>
|
||||
new ReachabilityWitnessPublisher(
|
||||
sp.GetRequiredService<IOptions<ReachabilityWitnessOptions>>(),
|
||||
sp.GetRequiredService<ICryptoHash>(),
|
||||
sp.GetRequiredService<ILogger<ReachabilityWitnessPublisher>>(),
|
||||
timeProvider: sp.GetService<TimeProvider>(),
|
||||
cas: sp.GetService<IFileContentAddressableStore>(),
|
||||
dsseSigningService: sp.GetService<IDsseSigningService>(),
|
||||
cryptoProfile: sp.GetService<ICryptoProfile>(),
|
||||
rekorClient: sp.GetService<IRekorClient>()));
|
||||
|
||||
// Register attesting writer (wraps RichGraphWriter)
|
||||
services.TryAddSingleton<AttestingRichGraphWriter>();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Replay.Core;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Attestation;
|
||||
|
||||
@@ -13,14 +13,6 @@ public sealed class ReachabilityWitnessDsseBuilder
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private static readonly JsonSerializerOptions CanonicalJsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new DSSE builder.
|
||||
/// </summary>
|
||||
@@ -98,7 +90,7 @@ public sealed class ReachabilityWitnessDsseBuilder
|
||||
public byte[] SerializeStatement(InTotoStatement statement)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(statement);
|
||||
return JsonSerializer.SerializeToUtf8Bytes(statement, CanonicalJsonOptions);
|
||||
return CanonicalJson.SerializeToUtf8Bytes(statement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -16,6 +16,16 @@ public sealed class ReachabilityWitnessOptions
|
||||
/// <summary>Whether to publish to Rekor transparency log</summary>
|
||||
public bool PublishToRekor { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Rekor backend base URL (required when <see cref="PublishToRekor"/> is enabled and tier is not air-gapped).
|
||||
/// </summary>
|
||||
public Uri? RekorUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor backend name used for labeling/logging.
|
||||
/// </summary>
|
||||
public string RekorBackendName { get; set; } = "primary";
|
||||
|
||||
/// <summary>Whether to store graph in CAS</summary>
|
||||
public bool StoreInCas { get; set; } = true;
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Replay.Core;
|
||||
using StellaOps.Scanner.Cache.Abstractions;
|
||||
using StellaOps.Scanner.ProofSpine;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Attestation;
|
||||
|
||||
@@ -13,6 +21,14 @@ public sealed class ReachabilityWitnessPublisher : IReachabilityWitnessPublisher
|
||||
private readonly ReachabilityWitnessDsseBuilder _dsseBuilder;
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
private readonly ILogger<ReachabilityWitnessPublisher> _logger;
|
||||
private readonly IFileContentAddressableStore? _cas;
|
||||
private readonly IDsseSigningService? _dsseSigningService;
|
||||
private readonly ICryptoProfile? _cryptoProfile;
|
||||
private readonly IRekorClient? _rekorClient;
|
||||
private static readonly JsonSerializerOptions DsseJsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new reachability witness publisher.
|
||||
@@ -21,7 +37,11 @@ public sealed class ReachabilityWitnessPublisher : IReachabilityWitnessPublisher
|
||||
IOptions<ReachabilityWitnessOptions> options,
|
||||
ICryptoHash cryptoHash,
|
||||
ILogger<ReachabilityWitnessPublisher> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
TimeProvider? timeProvider = null,
|
||||
IFileContentAddressableStore? cas = null,
|
||||
IDsseSigningService? dsseSigningService = null,
|
||||
ICryptoProfile? cryptoProfile = null,
|
||||
IRekorClient? rekorClient = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(cryptoHash);
|
||||
@@ -31,6 +51,10 @@ public sealed class ReachabilityWitnessPublisher : IReachabilityWitnessPublisher
|
||||
_cryptoHash = cryptoHash;
|
||||
_logger = logger;
|
||||
_dsseBuilder = new ReachabilityWitnessDsseBuilder(cryptoHash, timeProvider);
|
||||
_cas = cas;
|
||||
_dsseSigningService = dsseSigningService;
|
||||
_cryptoProfile = cryptoProfile;
|
||||
_rekorClient = rekorClient;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -61,11 +85,13 @@ public sealed class ReachabilityWitnessPublisher : IReachabilityWitnessPublisher
|
||||
}
|
||||
|
||||
string? casUri = null;
|
||||
string? casKey = null;
|
||||
|
||||
// Step 1: Store graph in CAS (if enabled)
|
||||
if (_options.StoreInCas)
|
||||
{
|
||||
casUri = await StoreInCasAsync(graphBytes, graphHash, cancellationToken).ConfigureAwait(false);
|
||||
casKey = ExtractHashDigest(graphHash);
|
||||
casUri = await StoreInCasAsync(graphBytes, casKey, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Step 2: Build in-toto statement
|
||||
@@ -86,8 +112,14 @@ public sealed class ReachabilityWitnessPublisher : IReachabilityWitnessPublisher
|
||||
graph.Nodes.Count,
|
||||
graph.Edges.Count);
|
||||
|
||||
// Step 3: Create DSSE envelope (placeholder - actual signing via Attestor service)
|
||||
var dsseEnvelope = CreateDsseEnvelope(statementBytes);
|
||||
// Step 3: Create DSSE envelope (signed where configured; deterministic fallback otherwise).
|
||||
var (envelope, dsseEnvelopeBytes) = await CreateDsseEnvelopeAsync(statement, statementBytes, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (_options.StoreInCas && casKey is not null)
|
||||
{
|
||||
await StoreDsseInCasAsync(dsseEnvelopeBytes, casKey, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Step 4: Submit to Rekor (if enabled and not air-gapped)
|
||||
long? rekorLogIndex = null;
|
||||
@@ -95,7 +127,7 @@ public sealed class ReachabilityWitnessPublisher : IReachabilityWitnessPublisher
|
||||
|
||||
if (_options.PublishToRekor && _options.Tier != AttestationTier.AirGapped)
|
||||
{
|
||||
(rekorLogIndex, rekorLogId) = await SubmitToRekorAsync(dsseEnvelope, cancellationToken).ConfigureAwait(false);
|
||||
(rekorLogIndex, rekorLogId) = await SubmitToRekorAsync(envelope, dsseEnvelopeBytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (_options.Tier == AttestationTier.AirGapped)
|
||||
{
|
||||
@@ -108,40 +140,157 @@ public sealed class ReachabilityWitnessPublisher : IReachabilityWitnessPublisher
|
||||
CasUri: casUri,
|
||||
RekorLogIndex: rekorLogIndex,
|
||||
RekorLogId: rekorLogId,
|
||||
DsseEnvelopeBytes: dsseEnvelope);
|
||||
DsseEnvelopeBytes: dsseEnvelopeBytes);
|
||||
}
|
||||
|
||||
private Task<string?> StoreInCasAsync(byte[] graphBytes, string graphHash, CancellationToken cancellationToken)
|
||||
private async Task<string?> StoreInCasAsync(byte[] graphBytes, string casKey, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: Integrate with actual CAS storage (BID-007)
|
||||
// For now, return a placeholder CAS URI based on hash
|
||||
var casUri = $"cas://local/{graphHash}";
|
||||
_logger.LogDebug("Stored graph in CAS: {CasUri}", casUri);
|
||||
return Task.FromResult<string?>(casUri);
|
||||
}
|
||||
|
||||
private byte[] CreateDsseEnvelope(byte[] statementBytes)
|
||||
{
|
||||
// TODO: Integrate with Attestor DSSE signing service (RWD-008)
|
||||
// For now, return unsigned envelope structure
|
||||
// In production, this would call the Attestor service to sign the statement
|
||||
|
||||
// Minimal DSSE envelope structure (unsigned)
|
||||
var envelope = new
|
||||
if (_cas is null)
|
||||
{
|
||||
payloadType = "application/vnd.in-toto+json",
|
||||
payload = Convert.ToBase64String(statementBytes),
|
||||
signatures = Array.Empty<object>() // Will be populated by Attestor
|
||||
_logger.LogWarning("CAS storage requested but no CAS store is configured; skipping graph CAS publication.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var existing = await _cas.TryGetAsync(casKey, cancellationToken).ConfigureAwait(false);
|
||||
if (existing is null)
|
||||
{
|
||||
await using var stream = new MemoryStream(graphBytes, writable: false);
|
||||
await _cas.PutAsync(new FileCasPutRequest(casKey, stream, leaveOpen: false), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var casUri = $"cas://reachability/graphs/{casKey}";
|
||||
_logger.LogDebug("Stored graph in CAS: {CasUri}", casUri);
|
||||
return casUri;
|
||||
}
|
||||
|
||||
private async Task StoreDsseInCasAsync(byte[] dsseBytes, string casKey, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_cas is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = $"{casKey}.dsse";
|
||||
var existing = await _cas.TryGetAsync(key, cancellationToken).ConfigureAwait(false);
|
||||
if (existing is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var stream = new MemoryStream(dsseBytes, writable: false);
|
||||
await _cas.PutAsync(new FileCasPutRequest(key, stream, leaveOpen: false), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<(DsseEnvelope Envelope, byte[] EnvelopeBytes)> CreateDsseEnvelopeAsync(
|
||||
InTotoStatement statement,
|
||||
byte[] statementBytes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string payloadType = "application/vnd.in-toto+json";
|
||||
|
||||
if (_dsseSigningService is not null)
|
||||
{
|
||||
var profile = _cryptoProfile ?? new InlineCryptoProfile(_options.SigningKeyId ?? "scanner-deterministic", "hs256");
|
||||
var signed = await _dsseSigningService.SignAsync(statement, payloadType, profile, cancellationToken).ConfigureAwait(false);
|
||||
return (signed, SerializeDsseEnvelope(signed));
|
||||
}
|
||||
|
||||
// Deterministic fallback signature: SHA-256 over the canonical statement bytes (no external key material).
|
||||
var signature = SHA256.HashData(statementBytes);
|
||||
var envelope = new DsseEnvelope(
|
||||
payloadType,
|
||||
Convert.ToBase64String(statementBytes),
|
||||
new[] { new DsseSignature(_options.SigningKeyId ?? "scanner-deterministic", Convert.ToBase64String(signature)) });
|
||||
return (envelope, SerializeDsseEnvelope(envelope));
|
||||
}
|
||||
|
||||
private async Task<(long? logIndex, string? logId)> SubmitToRekorAsync(
|
||||
DsseEnvelope envelope,
|
||||
byte[] envelopeBytes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_rekorClient is null)
|
||||
{
|
||||
_logger.LogWarning("Rekor submission requested but no Rekor client is configured; skipping.");
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
if (_options.RekorUrl is null)
|
||||
{
|
||||
_logger.LogWarning("Rekor submission requested but no RekorUrl is configured; skipping.");
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
var request = new AttestorSubmissionRequest();
|
||||
request.Bundle.Dsse.PayloadType = envelope.PayloadType;
|
||||
request.Bundle.Dsse.PayloadBase64 = envelope.Payload;
|
||||
|
||||
request.Bundle.Dsse.Signatures.Clear();
|
||||
foreach (var signature in envelope.Signatures)
|
||||
{
|
||||
request.Bundle.Dsse.Signatures.Add(new AttestorSubmissionRequest.DsseSignature
|
||||
{
|
||||
KeyId = signature.KeyId,
|
||||
Signature = signature.Sig
|
||||
});
|
||||
}
|
||||
|
||||
request.Meta.BundleSha256 = ComputeSha256Hex(envelopeBytes);
|
||||
request.Meta.LogPreference = _options.RekorBackendName;
|
||||
|
||||
var backend = new RekorBackend
|
||||
{
|
||||
Name = _options.RekorBackendName,
|
||||
Url = _options.RekorUrl
|
||||
};
|
||||
|
||||
return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(envelope);
|
||||
try
|
||||
{
|
||||
var response = await _rekorClient.SubmitAsync(request, backend, cancellationToken).ConfigureAwait(false);
|
||||
if (!string.IsNullOrWhiteSpace(response.Uuid))
|
||||
{
|
||||
_logger.LogInformation("Submitted reachability witness envelope to Rekor backend {Backend} as {Uuid}", backend.Name, response.Uuid);
|
||||
}
|
||||
|
||||
return (response.Index, response.Uuid);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to submit reachability witness envelope to Rekor backend {Backend}", backend.Name);
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<(long? logIndex, string? logId)> SubmitToRekorAsync(byte[] dsseEnvelope, CancellationToken cancellationToken)
|
||||
private static string ExtractHashDigest(string prefixedHash)
|
||||
{
|
||||
// TODO: Integrate with Rekor backend (RWD-008)
|
||||
// For now, return placeholder values
|
||||
_logger.LogDebug("Rekor submission placeholder - actual integration pending");
|
||||
return Task.FromResult<(long?, string?)>((null, null));
|
||||
var colonIndex = prefixedHash.IndexOf(':');
|
||||
return colonIndex >= 0 ? prefixedHash[(colonIndex + 1)..] : prefixedHash;
|
||||
}
|
||||
|
||||
private static string ComputeSha256Hex(ReadOnlySpan<byte> data)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(data, hash);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static byte[] SerializeDsseEnvelope(DsseEnvelope envelope)
|
||||
{
|
||||
var signatures = envelope.Signatures
|
||||
.OrderBy(static s => s.KeyId, StringComparer.Ordinal)
|
||||
.ThenBy(static s => s.Sig, StringComparer.Ordinal)
|
||||
.Select(static s => new { keyid = s.KeyId, sig = s.Sig })
|
||||
.ToArray();
|
||||
|
||||
var dto = new
|
||||
{
|
||||
payloadType = envelope.PayloadType,
|
||||
payload = envelope.Payload,
|
||||
signatures
|
||||
};
|
||||
|
||||
return JsonSerializer.SerializeToUtf8Bytes(dto, DsseJsonOptions);
|
||||
}
|
||||
|
||||
private sealed record InlineCryptoProfile(string KeyId, string Algorithm) : ICryptoProfile;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Cache\StellaOps.Scanner.Cache.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Scanner.ProofSpine\StellaOps.Scanner.ProofSpine.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Surface.Env\StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Scanner.SmartDiff\StellaOps.Scanner.SmartDiff.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Scanner.Analyzers.Native\StellaOps.Scanner.Analyzers.Native.csproj" />
|
||||
<ProjectReference Include="..\..\..\Attestor\StellaOps.Attestor\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user