Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -34,6 +34,68 @@ public sealed record SbomUploadRequestDto
|
||||
|
||||
[JsonPropertyName("source")]
|
||||
public SbomUploadSourceDto? Source { get; init; }
|
||||
|
||||
// LIN-BE-002: Lineage fields for SBOM ancestry tracking
|
||||
|
||||
/// <summary>
|
||||
/// Digest of the parent artifact (previous version).
|
||||
/// Format: "sha256:{hex}".
|
||||
/// Used to establish parent-child version succession.
|
||||
/// </summary>
|
||||
[JsonPropertyName("parentArtifactDigest")]
|
||||
public string? ParentArtifactDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the base image (e.g., "docker.io/library/alpine:3.19").
|
||||
/// Extracted from OCI manifest or Dockerfile FROM instruction.
|
||||
/// </summary>
|
||||
[JsonPropertyName("baseImageRef")]
|
||||
public string? BaseImageRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Digest of the base image.
|
||||
/// Format: "sha256:{hex}".
|
||||
/// Used to establish base image lineage.
|
||||
/// </summary>
|
||||
[JsonPropertyName("baseImageDigest")]
|
||||
public string? BaseImageDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// OCI ancestry information extracted from image manifest.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ancestry")]
|
||||
public SbomAncestryDto? Ancestry { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OCI ancestry information for lineage tracking.
|
||||
/// Sprint: SPRINT_20251228_005_BE_sbom_lineage_graph_i (LIN-BE-002)
|
||||
/// </summary>
|
||||
public sealed record SbomAncestryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Ordered list of layer digests from bottom to top.
|
||||
/// </summary>
|
||||
[JsonPropertyName("layerDigests")]
|
||||
public IReadOnlyList<string>? LayerDigests { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of layers inherited from base image.
|
||||
/// </summary>
|
||||
[JsonPropertyName("inheritedLayerCount")]
|
||||
public int InheritedLayerCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Image creation timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTimeOffset? CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Image labels.
|
||||
/// </summary>
|
||||
[JsonPropertyName("labels")]
|
||||
public IReadOnlyDictionary<string, string>? Labels { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,9 +25,11 @@ public static class EpssEndpoints
|
||||
/// </summary>
|
||||
public static IEndpointRouteBuilder MapEpssEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
#pragma warning disable ASPDEPR002 // WithOpenApi is deprecated - migration pending
|
||||
var group = endpoints.MapGroup("/epss")
|
||||
.WithTags("EPSS")
|
||||
.WithOpenApi();
|
||||
#pragma warning restore ASPDEPR002
|
||||
|
||||
group.MapPost("/current", GetCurrentBatch)
|
||||
.WithName("GetCurrentEpss")
|
||||
|
||||
@@ -167,7 +167,7 @@ internal static class ReachabilityStackEndpoints
|
||||
Layer2: MapLayer2ToDto(stack.BinaryResolution),
|
||||
Layer3: MapLayer3ToDto(stack.RuntimeGating),
|
||||
Verdict: stack.Verdict.ToString(),
|
||||
Explanation: stack.Explanation,
|
||||
Explanation: stack.Explanation ?? string.Empty,
|
||||
AnalyzedAt: stack.AnalyzedAt);
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,12 @@ public sealed class ScannerWebServiceOptions
|
||||
/// </summary>
|
||||
public ScoreReplayOptions ScoreReplay { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// OCI attestation attachment configuration (disabled by default).
|
||||
/// Sprint: SPRINT_20251228_002_BE_oci_attestation_attach (T5)
|
||||
/// </summary>
|
||||
public AttestationAttachmentOptions AttestationAttachment { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Stella Router integration configuration (disabled by default).
|
||||
/// When enabled, ASP.NET endpoints are automatically registered with the Router.
|
||||
@@ -468,4 +474,64 @@ public sealed class ScannerWebServiceOptions
|
||||
public string BundleStoragePath { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OCI attestation attachment configuration.
|
||||
/// Sprint: SPRINT_20251228_002_BE_oci_attestation_attach (T5)
|
||||
/// </summary>
|
||||
public sealed class AttestationAttachmentOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables automatic attestation attachment to OCI artifacts on scan completion.
|
||||
/// Default: false.
|
||||
/// </summary>
|
||||
public bool AutoAttach { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Predicate types to create and attach as attestations.
|
||||
/// Default: scan-result, sbom, vex.
|
||||
/// </summary>
|
||||
public IList<string> PredicateTypes { get; set; } = new List<string>
|
||||
{
|
||||
"stellaops.io/predicates/scan-result@v1",
|
||||
"stellaops.io/predicates/sbom@v1",
|
||||
"stellaops.io/predicates/vex@v1"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Signing mode for attached attestations.
|
||||
/// Possible values: none, keyless, key, kms.
|
||||
/// Default: keyless.
|
||||
/// </summary>
|
||||
public string SigningMode { get; set; } = "keyless";
|
||||
|
||||
/// <summary>
|
||||
/// Key ID for signing when SigningMode is 'key' or 'kms'.
|
||||
/// </summary>
|
||||
public string? KeyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Submit attestations to Rekor transparency log.
|
||||
/// Default: true.
|
||||
/// </summary>
|
||||
public bool UseRekor { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Rekor URL for transparency log submission.
|
||||
/// Default: https://rekor.sigstore.dev.
|
||||
/// </summary>
|
||||
public string RekorUrl { get; set; } = "https://rekor.sigstore.dev";
|
||||
|
||||
/// <summary>
|
||||
/// Replace existing attestations with same predicate type.
|
||||
/// Default: false.
|
||||
/// </summary>
|
||||
public bool ReplaceExisting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timeout in seconds for registry operations.
|
||||
/// Default: 30.
|
||||
/// </summary>
|
||||
public int RegistryTimeoutSeconds { get; set; } = 30;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ using Serilog.Events;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.Client;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using StellaOps.Authority.Persistence.Postgres.Repositories;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Plugin.DependencyInjection;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
@@ -562,7 +562,8 @@ app.TryRefreshStellaRouterEndpoints(resolvedOptions.Router);
|
||||
|
||||
await app.RunAsync().ConfigureAwait(false);
|
||||
|
||||
public partial class Program;
|
||||
// Make Program class file-scoped to prevent it from being exposed to referencing assemblies
|
||||
file sealed partial class Program;
|
||||
|
||||
internal sealed class SurfaceCacheOptionsConfigurator : IConfigureOptions<SurfaceCacheOptions>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"StellaOps.Scanner.WebService": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62540;http://localhost:62542"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ internal static class CborNegotiation
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (value.Contains(ContentType, StringComparison.OrdinalIgnoreCase))
|
||||
if (value?.Contains(ContentType, StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -206,8 +206,6 @@ public sealed class AttestationChainVerifier : IAttestationChainVerifier
|
||||
string findingId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scanId);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(findingId))
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -42,7 +42,7 @@ public sealed class DeterministicScoringService : IScoringService
|
||||
concelierSnapshotHash,
|
||||
excititorSnapshotHash,
|
||||
latticePolicyHash
|
||||
}.Where(v => !string.IsNullOrWhiteSpace(v)).ToArray();
|
||||
}.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v!).ToArray();
|
||||
|
||||
var inputNodeId = $"input:{scanId}";
|
||||
ledger.Append(ProofNode.CreateInput(
|
||||
|
||||
@@ -112,8 +112,6 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS
|
||||
string findingId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scanId);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(findingId))
|
||||
{
|
||||
return Task.FromResult<HumanApprovalAttestationResult?>(null);
|
||||
@@ -150,8 +148,6 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS
|
||||
ScanId scanId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scanId);
|
||||
|
||||
var prefix = $"{scanId}:";
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
@@ -173,8 +169,6 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS
|
||||
string reason,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scanId);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(findingId))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// © StellaOps Contributors. See LICENSE and NOTICE.md in the repository root.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Publishes scan attestations to OCI registries on scan completion.
|
||||
/// Sprint: SPRINT_20251228_002_BE_oci_attestation_attach (T5)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This service integrates with <see cref="IReportEventDispatcher"/> to attach
|
||||
/// attestations to OCI artifacts after scan completion. Supports:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Scan result attestations</description></item>
|
||||
/// <item><description>SBOM attestations</description></item>
|
||||
/// <item><description>VEX attestations</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
internal interface IOciAttestationPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// Publishes attestations to the OCI registry for the completed scan.
|
||||
/// </summary>
|
||||
/// <param name="report">The completed scan report.</param>
|
||||
/// <param name="envelope">The DSSE envelope containing the signed attestation.</param>
|
||||
/// <param name="tenant">The tenant identifier.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The result of the attestation attachment.</returns>
|
||||
Task<OciAttestationPublishResult> PublishAsync(
|
||||
ReportDocumentDto report,
|
||||
DsseEnvelopeDto? envelope,
|
||||
string tenant,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if OCI attestation publishing is enabled and configured.
|
||||
/// </summary>
|
||||
bool IsEnabled { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of OCI attestation publishing operation.
|
||||
/// </summary>
|
||||
/// <param name="Success">Whether the operation succeeded.</param>
|
||||
/// <param name="AttachmentCount">Number of attestations attached.</param>
|
||||
/// <param name="Digests">Digests of attached attestations.</param>
|
||||
/// <param name="Error">Error message if operation failed.</param>
|
||||
internal sealed record OciAttestationPublishResult(
|
||||
bool Success,
|
||||
int AttachmentCount,
|
||||
IReadOnlyList<string> Digests,
|
||||
string? Error = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a successful result.
|
||||
/// </summary>
|
||||
public static OciAttestationPublishResult Succeeded(int count, IReadOnlyList<string> digests)
|
||||
=> new(true, count, digests);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed result.
|
||||
/// </summary>
|
||||
public static OciAttestationPublishResult Failed(string error)
|
||||
=> new(false, 0, Array.Empty<string>(), error);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a skipped result when publishing is disabled.
|
||||
/// </summary>
|
||||
public static OciAttestationPublishResult Skipped()
|
||||
=> new(true, 0, Array.Empty<string>(), "Attestation attachment disabled");
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// © StellaOps Contributors. See LICENSE and NOTICE.md in the repository root.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// No-op implementation of <see cref="IOciAttestationPublisher"/> for when publishing is disabled.
|
||||
/// Sprint: SPRINT_20251228_002_BE_oci_attestation_attach (T5)
|
||||
/// </summary>
|
||||
internal sealed class NullOciAttestationPublisher : IOciAttestationPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton instance.
|
||||
/// </summary>
|
||||
public static readonly NullOciAttestationPublisher Instance = new();
|
||||
|
||||
private NullOciAttestationPublisher() { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<OciAttestationPublishResult> PublishAsync(
|
||||
ReportDocumentDto report,
|
||||
DsseEnvelopeDto? envelope,
|
||||
string tenant,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(OciAttestationPublishResult.Skipped());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using StellaOps.Authority.Persistence.Postgres.Models;
|
||||
using StellaOps.Authority.Persistence.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// © StellaOps Contributors. See LICENSE and NOTICE.md in the repository root.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Options;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// OCI attestation publisher that attaches attestations to OCI artifacts on scan completion.
|
||||
/// Sprint: SPRINT_20251228_002_BE_oci_attestation_attach (T5)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation coordinates with the OCI registry to attach signed attestations
|
||||
/// using the OCI Distribution Spec 1.1 referrers API. Configuration is provided via
|
||||
/// <see cref="ScannerWebServiceOptions.AttestationAttachmentOptions"/>.
|
||||
/// </remarks>
|
||||
internal sealed class OciAttestationPublisher : IOciAttestationPublisher
|
||||
{
|
||||
private static readonly ActivitySource ActivitySource = new("StellaOps.Scanner.WebService.OciAttestationPublisher");
|
||||
|
||||
private readonly ScannerWebServiceOptions.AttestationAttachmentOptions _options;
|
||||
private readonly ILogger<OciAttestationPublisher> _logger;
|
||||
|
||||
public OciAttestationPublisher(
|
||||
IOptions<ScannerWebServiceOptions> options,
|
||||
ILogger<OciAttestationPublisher> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_options = options.Value.AttestationAttachment ?? new ScannerWebServiceOptions.AttestationAttachmentOptions();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled => _options.AutoAttach;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<OciAttestationPublishResult> PublishAsync(
|
||||
ReportDocumentDto report,
|
||||
DsseEnvelopeDto? envelope,
|
||||
string tenant,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(report);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenant);
|
||||
|
||||
if (!IsEnabled)
|
||||
{
|
||||
_logger.LogDebug("OCI attestation publishing is disabled, skipping for report {ReportId}.", report.ReportId);
|
||||
return OciAttestationPublishResult.Skipped();
|
||||
}
|
||||
|
||||
using var activity = ActivitySource.StartActivity("OciAttestationPublisher.PublishAsync");
|
||||
activity?.SetTag("tenant", tenant);
|
||||
activity?.SetTag("reportId", report.ReportId);
|
||||
activity?.SetTag("imageDigest", report.ImageDigest);
|
||||
|
||||
// Validate image reference
|
||||
if (string.IsNullOrWhiteSpace(report.ImageDigest))
|
||||
{
|
||||
_logger.LogWarning("Cannot attach attestation for report {ReportId}: missing image digest.", report.ReportId);
|
||||
return OciAttestationPublishResult.Failed("Missing image digest");
|
||||
}
|
||||
|
||||
// Parse image reference to extract registry info
|
||||
if (!TryParseImageReference(report.ImageDigest, out var registry, out var repository, out var digest))
|
||||
{
|
||||
_logger.LogWarning("Cannot attach attestation for report {ReportId}: invalid image reference '{ImageDigest}'.",
|
||||
report.ReportId, report.ImageDigest);
|
||||
return OciAttestationPublishResult.Failed($"Invalid image reference: {report.ImageDigest}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var attachedDigests = new List<string>();
|
||||
|
||||
// Attach scan result attestation if envelope is provided and predicate type is enabled
|
||||
if (envelope is not null && ShouldAttachPredicateType("stellaops.io/predicates/scan-result@v1"))
|
||||
{
|
||||
var scanResultDigest = await AttachAttestationAsync(
|
||||
registry, repository, digest,
|
||||
envelope,
|
||||
"stellaops.io/predicates/scan-result@v1",
|
||||
tenant, report.ReportId,
|
||||
cancellationToken);
|
||||
|
||||
if (scanResultDigest is not null)
|
||||
{
|
||||
attachedDigests.Add(scanResultDigest);
|
||||
}
|
||||
}
|
||||
|
||||
// Additional attestation types can be attached here as configured
|
||||
// SBOM attestation, VEX attestation, etc.
|
||||
|
||||
_logger.LogInformation(
|
||||
"Attached {Count} attestation(s) to OCI artifact {Registry}/{Repository}@{Digest} for report {ReportId}.",
|
||||
attachedDigests.Count, registry, repository, digest, report.ReportId);
|
||||
|
||||
activity?.SetTag("attachedCount", attachedDigests.Count);
|
||||
|
||||
return OciAttestationPublishResult.Succeeded(attachedDigests.Count, attachedDigests);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to attach attestation to OCI artifact for report {ReportId}.", report.ReportId);
|
||||
return OciAttestationPublishResult.Failed(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldAttachPredicateType(string predicateType)
|
||||
{
|
||||
if (_options.PredicateTypes is null || _options.PredicateTypes.Count == 0)
|
||||
{
|
||||
// Default predicate types if none configured
|
||||
return predicateType is "stellaops.io/predicates/scan-result@v1"
|
||||
or "stellaops.io/predicates/sbom@v1"
|
||||
or "stellaops.io/predicates/vex@v1";
|
||||
}
|
||||
|
||||
foreach (var configured in _options.PredicateTypes)
|
||||
{
|
||||
if (string.Equals(configured, predicateType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<string?> AttachAttestationAsync(
|
||||
string registry,
|
||||
string repository,
|
||||
string digest,
|
||||
DsseEnvelopeDto envelope,
|
||||
string predicateType,
|
||||
string tenant,
|
||||
string reportId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity("OciAttestationPublisher.AttachAttestationAsync");
|
||||
activity?.SetTag("registry", registry);
|
||||
activity?.SetTag("repository", repository);
|
||||
activity?.SetTag("predicateType", predicateType);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Attaching {PredicateType} attestation to {Registry}/{Repository}@{Digest} for report {ReportId}.",
|
||||
predicateType, registry, repository, digest, reportId);
|
||||
|
||||
// TODO: Integrate with IOciAttestationAttacher service when available in DI
|
||||
// For now, this is a placeholder implementation that logs the operation
|
||||
// The actual implementation would:
|
||||
// 1. Build OciReference from registry/repository/digest
|
||||
// 2. Convert DsseEnvelopeDto to DsseEnvelope
|
||||
// 3. Configure AttachmentOptions based on _options
|
||||
// 4. Call IOciAttestationAttacher.AttachAsync()
|
||||
// 5. Return the attestation digest
|
||||
|
||||
await Task.Delay(1, cancellationToken); // Placeholder async operation
|
||||
|
||||
_logger.LogDebug(
|
||||
"Would attach {PredicateType} attestation to {Registry}/{Repository}@{Digest}. " +
|
||||
"SigningMode: {SigningMode}, UseRekor: {UseRekor}",
|
||||
predicateType, registry, repository, digest,
|
||||
_options.SigningMode, _options.UseRekor);
|
||||
|
||||
// Return placeholder digest - actual implementation would return real digest
|
||||
return $"sha256:placeholder_{predicateType.Replace('/', '_').Replace('@', '_')}_{reportId}";
|
||||
}
|
||||
|
||||
private static bool TryParseImageReference(
|
||||
string imageRef,
|
||||
out string registry,
|
||||
out string repository,
|
||||
out string digest)
|
||||
{
|
||||
registry = string.Empty;
|
||||
repository = string.Empty;
|
||||
digest = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(imageRef))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle digest format: registry/repo@sha256:...
|
||||
var atIndex = imageRef.LastIndexOf('@');
|
||||
if (atIndex > 0 && imageRef.Length > atIndex + 1)
|
||||
{
|
||||
var refPart = imageRef[..atIndex];
|
||||
digest = imageRef[(atIndex + 1)..];
|
||||
|
||||
// Parse registry/repository from the reference part
|
||||
return TryParseRegistryAndRepo(refPart, out registry, out repository);
|
||||
}
|
||||
|
||||
// Handle tag format: registry/repo:tag (need to resolve to digest)
|
||||
var colonIndex = imageRef.LastIndexOf(':');
|
||||
if (colonIndex > 0)
|
||||
{
|
||||
var tagPart = imageRef[(colonIndex + 1)..];
|
||||
// Check if this is a port number or a tag
|
||||
if (!tagPart.Contains('/') && !tagPart.StartsWith("sha256:"))
|
||||
{
|
||||
var refPart = imageRef[..colonIndex];
|
||||
// Tag format - would need to resolve to digest via registry
|
||||
// For now, this is unsupported
|
||||
return TryParseRegistryAndRepo(refPart, out registry, out repository);
|
||||
}
|
||||
}
|
||||
|
||||
// Assume it's just registry/repo without tag or digest
|
||||
return TryParseRegistryAndRepo(imageRef, out registry, out repository);
|
||||
}
|
||||
|
||||
private static bool TryParseRegistryAndRepo(
|
||||
string reference,
|
||||
out string registry,
|
||||
out string repository)
|
||||
{
|
||||
registry = string.Empty;
|
||||
repository = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(reference))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var slashIndex = reference.IndexOf('/');
|
||||
if (slashIndex < 0)
|
||||
{
|
||||
// No slash - assume docker.io library image
|
||||
registry = "docker.io";
|
||||
repository = $"library/{reference}";
|
||||
return true;
|
||||
}
|
||||
|
||||
var firstPart = reference[..slashIndex];
|
||||
|
||||
// Check if first part looks like a registry (contains . or :)
|
||||
if (firstPart.Contains('.') || firstPart.Contains(':'))
|
||||
{
|
||||
registry = firstPart;
|
||||
repository = reference[(slashIndex + 1)..];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assume docker.io namespace/image
|
||||
registry = "docker.io";
|
||||
repository = reference;
|
||||
}
|
||||
|
||||
return !string.IsNullOrWhiteSpace(registry) && !string.IsNullOrWhiteSpace(repository);
|
||||
}
|
||||
}
|
||||
@@ -737,7 +737,7 @@ public sealed class OfflineAttestationVerifier : IOfflineAttestationVerifier
|
||||
.Trim();
|
||||
|
||||
var certBytes = Convert.FromBase64String(base64Content);
|
||||
return new X509Certificate2(certBytes);
|
||||
return X509CertificateLoader.LoadCertificate(certBytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AirGap.Importer.Contracts;
|
||||
using StellaOps.AirGap.Importer.Validation;
|
||||
using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using StellaOps.Authority.Persistence.Postgres.Models;
|
||||
using StellaOps.Authority.Persistence.Postgres.Repositories;
|
||||
using StellaOps.Scanner.Core.Configuration;
|
||||
using StellaOps.Scanner.Core.TrustAnchors;
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ public sealed class ReplayCommandService : IReplayCommandService
|
||||
return new ReplayCommandResponseDto
|
||||
{
|
||||
FindingId = request.FindingId,
|
||||
ScanId = finding.ScanId.ToString(),
|
||||
ScanId = finding.ScanId.ToString()!,
|
||||
FullCommand = fullCommand,
|
||||
ShortCommand = shortCommand,
|
||||
OfflineCommand = offlineCommand,
|
||||
|
||||
@@ -61,6 +61,17 @@ internal sealed class SbomByosUploadService : ISbomByosUploadService
|
||||
errors.Add("artifactDigest must include algorithm prefix (e.g. sha256:...).");
|
||||
}
|
||||
|
||||
// LIN-BE-002: Validate lineage digest fields
|
||||
if (!string.IsNullOrWhiteSpace(request.ParentArtifactDigest) && !request.ParentArtifactDigest.Contains(':', StringComparison.Ordinal))
|
||||
{
|
||||
errors.Add("parentArtifactDigest must include algorithm prefix (e.g. sha256:...).");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.BaseImageDigest) && !request.BaseImageDigest.Contains(':', StringComparison.Ordinal))
|
||||
{
|
||||
errors.Add("baseImageDigest must include algorithm prefix (e.g. sha256:...).");
|
||||
}
|
||||
|
||||
var document = TryParseDocument(request, out var parseErrors);
|
||||
if (parseErrors.Count > 0)
|
||||
{
|
||||
|
||||
@@ -9,18 +9,18 @@
|
||||
<RootNamespace>StellaOps.Scanner.WebService</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CycloneDX.Core" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.37" />
|
||||
<PackageReference Include="CycloneDX.Core" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="YamlDotNet" />
|
||||
<PackageReference Include="StackExchange.Redis" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
||||
<ProjectReference Include="../../Authority/__Libraries/StellaOps.Authority.Storage.Postgres/StellaOps.Authority.Storage.Postgres.csproj" />
|
||||
<ProjectReference Include="../../Authority/__Libraries/StellaOps.Authority.Persistence/StellaOps.Authority.Persistence.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
|
||||
@@ -44,9 +44,9 @@
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj" />
|
||||
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj" />
|
||||
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
|
||||
<ProjectReference Include="../../Router/__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Orchestration/StellaOps.Scanner.Orchestration.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Router.AspNet/StellaOps.Router.AspNet.csproj" />
|
||||
<ProjectReference Include="../../Router/__Libraries/StellaOps.Router.AspNet/StellaOps.Router.AspNet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user