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

@@ -0,0 +1,207 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Text.Json;
namespace StellaOps.Scanner.Surface.Secrets;
public sealed record CasAccessSecret(
string Driver,
string? Endpoint,
string? Region,
string? Bucket,
string? RootPrefix,
string? ApiKey,
string? ApiKeyHeader,
IReadOnlyDictionary<string, string> Headers,
string? AccessKeyId,
string? SecretAccessKey,
string? SessionToken,
bool? AllowInsecureTls);
public static class SurfaceSecretParser
{
public static CasAccessSecret ParseCasAccessSecret(SurfaceSecretHandle handle)
{
ArgumentNullException.ThrowIfNull(handle);
var payload = handle.AsBytes();
if (payload.IsEmpty)
{
throw new InvalidOperationException("Surface secret payload is empty.");
}
var jsonText = DecodeUtf8(payload);
using var document = JsonDocument.Parse(jsonText);
var root = document.RootElement;
string driver = GetString(root, "driver") ?? GetMetadataValue(handle.Metadata, "driver") ?? "s3";
string? endpoint = GetString(root, "endpoint") ?? GetMetadataValue(handle.Metadata, "endpoint");
string? region = GetString(root, "region") ?? GetMetadataValue(handle.Metadata, "region");
string? bucket = GetString(root, "bucket") ?? GetMetadataValue(handle.Metadata, "bucket");
string? rootPrefix = GetString(root, "rootPrefix") ?? GetMetadataValue(handle.Metadata, "rootPrefix");
string? apiKey = GetString(root, "apiKey") ?? GetMetadataValue(handle.Metadata, "apiKey");
string? apiKeyHeader = GetString(root, "apiKeyHeader") ?? GetMetadataValue(handle.Metadata, "apiKeyHeader");
string? accessKeyId = GetString(root, "accessKeyId") ?? GetMetadataValue(handle.Metadata, "accessKeyId");
string? secretAccessKey = GetString(root, "secretAccessKey") ?? GetMetadataValue(handle.Metadata, "secretAccessKey");
string? sessionToken = GetString(root, "sessionToken") ?? GetMetadataValue(handle.Metadata, "sessionToken");
bool? allowInsecureTls = GetBoolean(root, "allowInsecureTls");
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
PopulateHeaders(root, headers);
PopulateMetadataHeaders(handle.Metadata, headers);
return new CasAccessSecret(
driver.Trim(),
endpoint?.Trim(),
region?.Trim(),
bucket?.Trim(),
rootPrefix?.Trim(),
apiKey?.Trim(),
apiKeyHeader?.Trim(),
new ReadOnlyDictionary<string, string>(headers),
accessKeyId?.Trim(),
secretAccessKey?.Trim(),
sessionToken?.Trim(),
allowInsecureTls);
}
private static string DecodeUtf8(ReadOnlyMemory<byte> payload)
{
if (payload.IsEmpty)
{
return string.Empty;
}
try
{
return Encoding.UTF8.GetString(payload.Span);
}
catch (DecoderFallbackException ex)
{
throw new InvalidOperationException("Surface secret payload is not valid UTF-8 JSON.", ex);
}
}
private static string? GetString(JsonElement element, string propertyName)
{
if (TryGetPropertyIgnoreCase(element, propertyName, out var property))
{
return property.ValueKind switch
{
JsonValueKind.String => property.GetString(),
JsonValueKind.Number => property.GetRawText(),
JsonValueKind.True => bool.TrueString,
JsonValueKind.False => bool.FalseString,
_ => null
};
}
return null;
}
private static bool? GetBoolean(JsonElement element, string propertyName)
{
if (TryGetPropertyIgnoreCase(element, propertyName, out var property))
{
return property.ValueKind switch
{
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.String when bool.TryParse(property.GetString(), out var parsed) => parsed,
_ => null
};
}
return null;
}
private static bool TryGetPropertyIgnoreCase(JsonElement element, string propertyName, out JsonElement property)
{
if (element.ValueKind != JsonValueKind.Object)
{
property = default;
return false;
}
if (element.TryGetProperty(propertyName, out property))
{
return true;
}
foreach (var candidate in element.EnumerateObject())
{
if (string.Equals(candidate.Name, propertyName, StringComparison.OrdinalIgnoreCase))
{
property = candidate.Value;
return true;
}
}
property = default;
return false;
}
private static void PopulateHeaders(JsonElement element, IDictionary<string, string> headers)
{
if (!TryGetPropertyIgnoreCase(element, "headers", out var headersElement))
{
return;
}
if (headersElement.ValueKind != JsonValueKind.Object)
{
return;
}
foreach (var property in headersElement.EnumerateObject())
{
var value = property.Value.ValueKind switch
{
JsonValueKind.String => property.Value.GetString(),
JsonValueKind.Number => property.Value.GetRawText(),
JsonValueKind.True => bool.TrueString,
JsonValueKind.False => bool.FalseString,
_ => null
};
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
headers[property.Name] = value.Trim();
}
}
private static void PopulateMetadataHeaders(IReadOnlyDictionary<string, string> metadata, IDictionary<string, string> headers)
{
foreach (var (key, value) in metadata)
{
if (!key.StartsWith("header:", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var headerName = key["header:".Length..];
if (string.IsNullOrWhiteSpace(headerName) || string.IsNullOrWhiteSpace(value))
{
continue;
}
headers[headerName] = value;
}
}
private static string? GetMetadataValue(IReadOnlyDictionary<string, string> metadata, string key)
{
foreach (var (metadataKey, metadataValue) in metadata)
{
if (string.Equals(metadataKey, key, StringComparison.OrdinalIgnoreCase))
{
return metadataValue;
}
}
return null;
}
}