feat: Implement ScannerSurfaceSecretConfigurator for web service options
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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. |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user