feat: Implement ScannerSurfaceSecretConfigurator for web service options
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added ScannerSurfaceSecretConfigurator to configure ScannerWebServiceOptions using surface secrets.
- Integrated ISurfaceSecretProvider to fetch and apply secrets for artifact store configuration.
- Enhanced logging for secret retrieval and application processes.

feat: Implement ScannerStorageSurfaceSecretConfigurator for worker options

- Introduced ScannerStorageSurfaceSecretConfigurator to configure ScannerStorageOptions with surface secrets.
- Utilized ISurfaceSecretProvider to retrieve and apply secrets for object store settings.
- Improved logging for secret handling and configuration.

feat: Create SurfaceManifestPublisher for publishing surface manifests

- Developed SurfaceManifestPublisher to handle the creation and storage of surface manifests.
- Implemented methods for serializing manifest documents and storing payloads in the object store.
- Added dual write functionality for mirror storage of manifests.

feat: Add SurfaceManifestStageExecutor for processing scan stages

- Created SurfaceManifestStageExecutor to execute the manifest publishing stage in scan jobs.
- Integrated with SurfaceManifestPublisher to publish manifests based on collected payloads.
- Enhanced logging for job processing and manifest storage.

feat: Define SurfaceManifest models for manifest structure

- Established SurfaceManifestDocument, SurfaceManifestSource, SurfaceManifestArtifact, and SurfaceManifestStorage records.
- Implemented serialization attributes for JSON handling of manifest models.

feat: Implement CasAccessSecret and SurfaceSecretParser for secret handling

- Created CasAccessSecret record to represent surface access secrets.
- Developed SurfaceSecretParser to parse and validate surface secrets from JSON payloads.

test: Add unit tests for CasAccessSecretParser

- Implemented tests for parsing CasAccessSecret from JSON payloads and metadata fallbacks.
- Verified expected values and behavior for secret parsing logic.

test: Add unit tests for ScannerSurfaceSecretConfigurator

- Created tests for ScannerSurfaceSecretConfigurator to ensure correct application of surface secrets to web service options.
- Validated artifact store settings after configuration.

test: Add unit tests for ScannerStorageSurfaceSecretConfigurator

- Implemented tests for ScannerStorageSurfaceSecretConfigurator to verify correct application of surface secrets to storage options.
- Ensured accurate configuration of object store settings.
This commit is contained in:
master
2025-11-06 18:49:23 +02:00
parent e536492da9
commit 18f28168f0
33 changed files with 2066 additions and 621 deletions

View File

@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using StellaOps.Scanner.Surface.FS;
namespace StellaOps.Scanner.WebService.Contracts;
@@ -28,60 +28,3 @@ public sealed record SurfacePointersDto
[JsonPropertyOrder(4)]
public SurfaceManifestDocument Manifest { get; init; } = new();
}
public sealed record SurfaceManifestDocument
{
[JsonPropertyName("schema")]
[JsonPropertyOrder(0)]
public string Schema { get; init; } = "stellaops.surface.manifest@1";
[JsonPropertyName("tenant")]
[JsonPropertyOrder(1)]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("imageDigest")]
[JsonPropertyOrder(2)]
public string ImageDigest { get; init; } = string.Empty;
[JsonPropertyName("generatedAt")]
[JsonPropertyOrder(3)]
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
[JsonPropertyName("artifacts")]
[JsonPropertyOrder(4)]
public IReadOnlyList<SurfaceManifestArtifact> Artifacts { get; init; } = Array.Empty<SurfaceManifestArtifact>();
}
public sealed record SurfaceManifestArtifact
{
[JsonPropertyName("kind")]
[JsonPropertyOrder(0)]
public string Kind { get; init; } = string.Empty;
[JsonPropertyName("uri")]
[JsonPropertyOrder(1)]
public string Uri { get; init; } = string.Empty;
[JsonPropertyName("digest")]
[JsonPropertyOrder(2)]
public string Digest { get; init; } = string.Empty;
[JsonPropertyName("mediaType")]
[JsonPropertyOrder(3)]
public string MediaType { get; init; } = string.Empty;
[JsonPropertyName("format")]
[JsonPropertyOrder(4)]
public string Format { get; init; } = string.Empty;
[JsonPropertyName("sizeBytes")]
[JsonPropertyOrder(5)]
public long SizeBytes { get; init; }
= 0;
[JsonPropertyName("view")]
[JsonPropertyOrder(6)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? View { get; init; }
= null;
}

View File

@@ -0,0 +1,118 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Storage;
namespace StellaOps.Scanner.WebService.Options;
internal sealed class ScannerStorageOptionsPostConfigurator : IPostConfigureOptions<ScannerStorageOptions>
{
private readonly IOptionsMonitor<ScannerWebServiceOptions> _webOptions;
private readonly ILogger<ScannerStorageOptionsPostConfigurator> _logger;
public ScannerStorageOptionsPostConfigurator(
IOptionsMonitor<ScannerWebServiceOptions> webOptions,
ILogger<ScannerStorageOptionsPostConfigurator> logger)
{
_webOptions = webOptions ?? throw new ArgumentNullException(nameof(webOptions));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void PostConfigure(string? name, ScannerStorageOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var source = _webOptions.CurrentValue?.ArtifactStore;
if (source is null)
{
return;
}
var target = options.ObjectStore ??= new ObjectStoreOptions();
if (!string.IsNullOrWhiteSpace(source.Driver))
{
target.Driver = source.Driver;
}
if (!string.IsNullOrWhiteSpace(source.Region))
{
target.Region = source.Region!;
}
if (!string.IsNullOrWhiteSpace(source.Bucket))
{
target.BucketName = source.Bucket!;
}
if (!string.IsNullOrWhiteSpace(source.RootPrefix))
{
target.RootPrefix = source.RootPrefix;
}
if (!string.IsNullOrWhiteSpace(source.Endpoint))
{
if (target.IsRustFsDriver())
{
target.RustFs ??= new RustFsOptions();
target.RustFs.BaseUrl = source.Endpoint;
}
else
{
target.ServiceUrl = source.Endpoint;
}
}
if (target.IsRustFsDriver())
{
if (target.RustFs is null)
{
target.RustFs = new RustFsOptions();
}
target.RustFs.AllowInsecureTls = source.AllowInsecureTls;
if (!string.IsNullOrWhiteSpace(source.ApiKeyHeader))
{
target.RustFs.ApiKeyHeader = source.ApiKeyHeader!;
}
if (!string.IsNullOrWhiteSpace(source.ApiKey))
{
target.RustFs.ApiKey = source.ApiKey;
}
if (!string.IsNullOrWhiteSpace(source.Endpoint))
{
target.RustFs.BaseUrl = source.Endpoint!;
}
}
if (!string.IsNullOrWhiteSpace(source.AccessKey))
{
target.AccessKeyId = source.AccessKey;
}
if (!string.IsNullOrWhiteSpace(source.SecretKey))
{
target.SecretAccessKey = source.SecretKey;
}
if (source.Headers is { Count: > 0 })
{
foreach (var (key, value) in source.Headers)
{
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
{
continue;
}
target.Headers[key] = value;
}
}
_logger.LogDebug(
"Mirrored artifact store settings into scanner storage options (driver: {Driver}, bucket: {Bucket}).",
target.Driver,
target.BucketName);
}
}

View File

@@ -0,0 +1,124 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.Surface.Secrets;
namespace StellaOps.Scanner.WebService.Options;
internal sealed class ScannerSurfaceSecretConfigurator : IConfigureOptions<ScannerWebServiceOptions>
{
private const string ComponentName = "Scanner.WebService";
private readonly ISurfaceSecretProvider _secretProvider;
private readonly ISurfaceEnvironment _surfaceEnvironment;
private readonly ILogger<ScannerSurfaceSecretConfigurator> _logger;
public ScannerSurfaceSecretConfigurator(
ISurfaceSecretProvider secretProvider,
ISurfaceEnvironment surfaceEnvironment,
ILogger<ScannerSurfaceSecretConfigurator> logger)
{
_secretProvider = secretProvider ?? throw new ArgumentNullException(nameof(secretProvider));
_surfaceEnvironment = surfaceEnvironment ?? throw new ArgumentNullException(nameof(surfaceEnvironment));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void Configure(ScannerWebServiceOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var tenant = _surfaceEnvironment.Settings.Secrets.Tenant;
var request = new SurfaceSecretRequest(
Tenant: tenant,
Component: ComponentName,
SecretType: "cas-access");
CasAccessSecret? secret = null;
try
{
using var handle = _secretProvider.GetAsync(request).AsTask().GetAwaiter().GetResult();
secret = SurfaceSecretParser.ParseCasAccessSecret(handle);
}
catch (SurfaceSecretNotFoundException)
{
_logger.LogDebug("Surface secret 'cas-access' not found for {Component}; retaining configured artifact store settings.", ComponentName);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to resolve surface secret 'cas-access' for {Component}.", ComponentName);
}
if (secret is null)
{
return;
}
ApplySecret(options.ArtifactStore ??= new ScannerWebServiceOptions.ArtifactStoreOptions(), secret);
}
private void ApplySecret(ScannerWebServiceOptions.ArtifactStoreOptions artifactStore, CasAccessSecret secret)
{
if (!string.IsNullOrWhiteSpace(secret.Driver))
{
artifactStore.Driver = secret.Driver;
}
if (!string.IsNullOrWhiteSpace(secret.Endpoint))
{
artifactStore.Endpoint = secret.Endpoint!;
}
if (secret.AllowInsecureTls is { } insecure)
{
artifactStore.AllowInsecureTls = insecure;
artifactStore.UseTls = !insecure;
}
if (!string.IsNullOrWhiteSpace(secret.Region))
{
artifactStore.Region = secret.Region;
}
if (!string.IsNullOrWhiteSpace(secret.Bucket))
{
artifactStore.Bucket = secret.Bucket!;
}
if (!string.IsNullOrWhiteSpace(secret.RootPrefix))
{
artifactStore.RootPrefix = secret.RootPrefix!;
}
if (!string.IsNullOrWhiteSpace(secret.ApiKeyHeader))
{
artifactStore.ApiKeyHeader = secret.ApiKeyHeader!;
}
if (!string.IsNullOrWhiteSpace(secret.ApiKey))
{
artifactStore.ApiKey = secret.ApiKey;
}
if (!string.IsNullOrWhiteSpace(secret.AccessKeyId) && !string.IsNullOrWhiteSpace(secret.SecretAccessKey))
{
artifactStore.AccessKey = secret.AccessKeyId!;
artifactStore.SecretKey = secret.SecretAccessKey!;
}
foreach (var header in secret.Headers)
{
if (string.IsNullOrWhiteSpace(header.Key) || string.IsNullOrWhiteSpace(header.Value))
{
continue;
}
artifactStore.Headers[header.Key] = header.Value;
}
_logger.LogInformation(
"Surface secret 'cas-access' applied for {Component} (driver: {Driver}, bucket: {Bucket}).",
ComponentName,
artifactStore.Driver,
artifactStore.Bucket);
}
}

View File

@@ -30,13 +30,7 @@ public static class ScannerWebServiceOptionsPostConfigure
options.ArtifactStore ??= new ScannerWebServiceOptions.ArtifactStoreOptions();
var artifactStore = options.ArtifactStore;
if (string.IsNullOrWhiteSpace(artifactStore.SecretKey)
&& !string.IsNullOrWhiteSpace(artifactStore.SecretKeyFile))
{
artifactStore.SecretKey = ReadSecretFile(artifactStore.SecretKeyFile!, contentRootPath);
}
options.Signing ??= new ScannerWebServiceOptions.SigningOptions();
options.Signing ??= new ScannerWebServiceOptions.SigningOptions();
var signing = options.Signing;
if (string.IsNullOrWhiteSpace(signing.KeyPem)
&& !string.IsNullOrWhiteSpace(signing.KeyPemFile))

View File

@@ -97,6 +97,7 @@ builder.Services.AddSurfaceEnvironment(options =>
builder.Services.AddSurfaceValidation();
builder.Services.AddSurfaceFileCache();
builder.Services.AddSurfaceSecrets();
builder.Services.AddSingleton<IConfigureOptions<ScannerWebServiceOptions>, ScannerSurfaceSecretConfigurator>();
builder.Services.AddSingleton<IConfigureOptions<SurfaceCacheOptions>>(sp =>
new SurfaceCacheOptionsConfigurator(sp.GetRequiredService<ISurfaceEnvironment>()));
builder.Services.AddSingleton<ISurfacePointerService, SurfacePointerService>();
@@ -179,6 +180,7 @@ builder.Services.AddScannerStorage(storageOptions =>
storageOptions.ObjectStore.RustFs.BaseUrl = string.Empty;
}
});
builder.Services.AddSingleton<IPostConfigureOptions<ScannerStorageOptions>, ScannerStorageOptionsPostConfigurator>();
builder.Services.AddSingleton<RuntimeEventRateLimiter>();
builder.Services.AddSingleton<IRuntimeEventIngestionService, RuntimeEventIngestionService>();
builder.Services.AddSingleton<IRuntimeAttestationVerifier, RuntimeAttestationVerifier>();

View File

@@ -6,7 +6,7 @@
| SCANNER-SURFACE-02 | DONE (2025-11-05) | Scanner WebService Guild | SURFACE-FS-02 | Publish Surface.FS pointers (CAS URIs, manifests) via scan/report APIs and update attestation metadata.<br>2025-11-05: Surface pointers projected through scan/report endpoints, orchestrator samples + DSSE fixtures refreshed with manifest block, readiness tests updated to use validator stub. | OpenAPI updated; clients regenerated; integration tests validate pointer presence and tenancy. |
| SCANNER-ENV-02 | TODO (2025-11-06) | Scanner WebService Guild, Ops Guild | SURFACE-ENV-02 | Wire Surface.Env helpers into WebService hosting (cache roots, feature flags) and document configuration.<br>2025-11-02: Cache root resolution switched to helper; feature flag bindings updated; Helm/Compose updates pending review.<br>2025-11-05 14:55Z: Aligning readiness checks, docs, and Helm/Compose templates with Surface.Env outputs and planning test coverage for configuration fallbacks.<br>2025-11-06 17:05Z: Surface.Env documentation/README refreshed; warning catalogue captured for ops handoff.<br>2025-11-06 07:45Z: Helm values (dev/stage/prod/airgap/mirror) and Compose examples updated with `SCANNER_SURFACE_*` defaults plus rollout warning note in `deploy/README.md`.<br>2025-11-06 07:55Z: Paused; follow-up automation captured under `DEVOPS-OPENSSL-11-001/002` and pending Surface.Env readiness tests. | Service uses helper; env table documented; helm/compose templates updated. |
> 2025-11-05 19:18Z: Added configurator to project wiring and unit test ensuring Surface.Env cache root is honoured.
| SCANNER-SECRETS-02 | DOING (2025-11-02) | Scanner WebService Guild, Security Guild | SURFACE-SECRETS-02 | Replace ad-hoc secret wiring with Surface.Secrets for report/export operations (registry and CAS tokens).<br>2025-11-02: Export/report flows now depend on Surface.Secrets stub; integration tests in progress. | Secrets fetched through shared provider; unit/integration tests cover rotation + failure cases. |
| SCANNER-SECRETS-02 | DOING (2025-11-06) | Scanner WebService Guild, Security Guild | SURFACE-SECRETS-02 | Replace ad-hoc secret wiring with Surface.Secrets for report/export operations (registry and CAS tokens).<br>2025-11-02: Export/report flows now depend on Surface.Secrets stub; integration tests in progress.<br>2025-11-06: Restarting work to eliminate file-based secrets, plumb provider handles through report/export services, and extend failure/rotation tests.<br>2025-11-06 21:40Z: Added configurator + storage post-config to hydrate artifact/CAS credentials from `cas-access` secrets with unit coverage. | Secrets fetched through shared provider; unit/integration tests cover rotation + failure cases. |
| SCANNER-EVENTS-16-301 | BLOCKED (2025-10-26) | Scanner WebService Guild | ORCH-SVC-38-101, NOTIFY-SVC-38-001 | Emit orchestrator-compatible envelopes (`scanner.event.*`) and update integration tests to verify Notifier ingestion (no Redis queue coupling). | Tests assert envelope schema + orchestrator publish; Notifier consumer harness passes; docs updated with new event contract. Blocked by .NET 10 preview OpenAPI/Auth dependency drift preventing `dotnet test` completion. |
| SCANNER-EVENTS-16-302 | DONE (2025-11-06) | Scanner WebService Guild | SCANNER-EVENTS-16-301 | Extend orchestrator event links (report/policy/attestation) once endpoints are finalised across gateway + console.<br>2025-11-06 22:55Z: Dispatcher now honours configurable API/console base segments, JSON samples/docs refreshed, and `ReportEventDispatcherTests` extended. Tests: `StellaOps.Scanner.WebService.Tests` build until pre-existing `SurfaceCacheOptionsConfiguratorTests` ctor signature drift (tracked separately). | Links section covers UI/API targets; downstream consumers validated; docs/samples updated. |