Implement MongoDB-based storage for Pack Run approval, artifact, log, and state management
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 MongoPackRunApprovalStore for managing approval states with MongoDB. - Introduced MongoPackRunArtifactUploader for uploading and storing artifacts. - Created MongoPackRunLogStore to handle logging of pack run events. - Developed MongoPackRunStateStore for persisting and retrieving pack run states. - Implemented unit tests for MongoDB stores to ensure correct functionality. - Added MongoTaskRunnerTestContext for setting up MongoDB test environment. - Enhanced PackRunStateFactory to correctly initialize state with gate reasons.
This commit is contained in:
@@ -17,4 +17,6 @@ public static class ScanAnalysisKeys
|
||||
public const string EntryTraceNdjson = "analysis.entrytrace.ndjson";
|
||||
|
||||
public const string SurfaceManifest = "analysis.surface.manifest";
|
||||
|
||||
public const string RegistryCredentials = "analysis.registry.credentials";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Secrets;
|
||||
|
||||
public sealed record AttestationSecret(
|
||||
string KeyPem,
|
||||
string? CertificatePem,
|
||||
string? CertificateChainPem,
|
||||
string? RekorApiToken);
|
||||
|
||||
public static partial class SurfaceSecretParser
|
||||
{
|
||||
public static AttestationSecret ParseAttestationSecret(SurfaceSecretHandle handle)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handle);
|
||||
|
||||
var payload = handle.AsBytes();
|
||||
if (payload.IsEmpty)
|
||||
{
|
||||
throw new InvalidOperationException("Surface secret payload is empty.");
|
||||
}
|
||||
|
||||
using var document = JsonDocument.Parse(DecodeUtf8(payload));
|
||||
var root = document.RootElement;
|
||||
|
||||
var keyPem = GetString(root, "keyPem")
|
||||
?? GetString(root, "pem")
|
||||
?? GetMetadataValue(handle.Metadata, "keyPem")
|
||||
?? GetMetadataValue(handle.Metadata, "pem");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(keyPem))
|
||||
{
|
||||
throw new InvalidOperationException("Attestation secret must include a 'keyPem' value.");
|
||||
}
|
||||
|
||||
var certificatePem = GetString(root, "certificatePem")
|
||||
?? GetMetadataValue(handle.Metadata, "certificatePem");
|
||||
|
||||
var certificateChainPem = GetString(root, "certificateChainPem")
|
||||
?? GetMetadataValue(handle.Metadata, "certificateChainPem");
|
||||
|
||||
var rekorToken = GetString(root, "rekorToken")
|
||||
?? GetString(root, "rekorApiToken")
|
||||
?? GetMetadataValue(handle.Metadata, "rekorToken")
|
||||
?? GetMetadataValue(handle.Metadata, "rekorApiToken");
|
||||
|
||||
return new AttestationSecret(
|
||||
keyPem.Trim(),
|
||||
certificatePem?.Trim(),
|
||||
certificateChainPem?.Trim(),
|
||||
rekorToken?.Trim());
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public sealed record CasAccessSecret(
|
||||
string? SessionToken,
|
||||
bool? AllowInsecureTls);
|
||||
|
||||
public static class SurfaceSecretParser
|
||||
public static partial class SurfaceSecretParser
|
||||
{
|
||||
public static CasAccessSecret ParseCasAccessSecret(SurfaceSecretHandle handle)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Secrets;
|
||||
|
||||
public sealed record RegistryAccessSecret(
|
||||
IReadOnlyList<RegistryCredential> Entries,
|
||||
string? DefaultRegistry);
|
||||
|
||||
public sealed record RegistryCredential(
|
||||
string Registry,
|
||||
string? Username,
|
||||
string? Password,
|
||||
string? IdentityToken,
|
||||
string? RegistryToken,
|
||||
string? RefreshToken,
|
||||
DateTimeOffset? ExpiresAt,
|
||||
IReadOnlyCollection<string> Scopes,
|
||||
bool? AllowInsecureTls,
|
||||
IReadOnlyDictionary<string, string> Headers,
|
||||
string? Email);
|
||||
|
||||
public static partial class SurfaceSecretParser
|
||||
{
|
||||
public static RegistryAccessSecret ParseRegistryAccessSecret(SurfaceSecretHandle handle)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handle);
|
||||
|
||||
var entries = new List<RegistryCredential>();
|
||||
string? defaultRegistry = null;
|
||||
|
||||
var payload = handle.AsBytes();
|
||||
if (!payload.IsEmpty)
|
||||
{
|
||||
var jsonText = DecodeUtf8(payload);
|
||||
using var document = JsonDocument.Parse(jsonText);
|
||||
var root = document.RootElement;
|
||||
|
||||
defaultRegistry = GetString(root, "defaultRegistry") ?? GetMetadataValue(handle.Metadata, "defaultRegistry");
|
||||
|
||||
if (TryParseRegistryEntries(root, handle.Metadata, entries) ||
|
||||
TryParseAuthsObject(root, handle.Metadata, entries))
|
||||
{
|
||||
// entries already populated
|
||||
}
|
||||
else if (root.ValueKind == JsonValueKind.Object && root.GetRawText().Length > 2) // not empty object
|
||||
{
|
||||
entries.Add(ParseRegistryEntry(root, handle.Metadata, fallbackRegistry: null));
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.Count == 0 && TryCreateRegistryEntryFromMetadata(handle.Metadata, out var metadataEntry))
|
||||
{
|
||||
entries.Add(metadataEntry);
|
||||
}
|
||||
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Registry secret payload does not contain credentials.");
|
||||
}
|
||||
|
||||
defaultRegistry ??= GetMetadataValue(handle.Metadata, "defaultRegistry")
|
||||
?? entries[0].Registry;
|
||||
|
||||
return new RegistryAccessSecret(
|
||||
new ReadOnlyCollection<RegistryCredential>(entries),
|
||||
string.IsNullOrWhiteSpace(defaultRegistry) ? entries[0].Registry : defaultRegistry.Trim());
|
||||
}
|
||||
|
||||
private static bool TryParseRegistryEntries(
|
||||
JsonElement root,
|
||||
IReadOnlyDictionary<string, string> metadata,
|
||||
ICollection<RegistryCredential> entries)
|
||||
{
|
||||
if (!TryGetPropertyIgnoreCase(root, "entries", out var entriesElement) ||
|
||||
entriesElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var entryElement in entriesElement.EnumerateArray())
|
||||
{
|
||||
if (entryElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.Add(ParseRegistryEntry(entryElement, metadata, fallbackRegistry: null));
|
||||
}
|
||||
|
||||
return entries.Count > 0;
|
||||
}
|
||||
|
||||
private static bool TryParseAuthsObject(
|
||||
JsonElement root,
|
||||
IReadOnlyDictionary<string, string> metadata,
|
||||
ICollection<RegistryCredential> entries)
|
||||
{
|
||||
if (!TryGetPropertyIgnoreCase(root, "auths", out var authsElement) ||
|
||||
authsElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var property in authsElement.EnumerateObject())
|
||||
{
|
||||
if (property.Value.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.Add(ParseRegistryEntry(property.Value, metadata, property.Name));
|
||||
}
|
||||
|
||||
return entries.Count > 0;
|
||||
}
|
||||
|
||||
private static RegistryCredential ParseRegistryEntry(
|
||||
JsonElement element,
|
||||
IReadOnlyDictionary<string, string> metadata,
|
||||
string? fallbackRegistry)
|
||||
{
|
||||
var registry = GetString(element, "registry")
|
||||
?? GetString(element, "server")
|
||||
?? fallbackRegistry
|
||||
?? GetMetadataValue(metadata, "registry")
|
||||
?? throw new InvalidOperationException("Registry credential is missing a registry identifier.");
|
||||
|
||||
registry = registry.Trim();
|
||||
|
||||
var username = GetString(element, "username") ?? GetString(element, "user");
|
||||
var password = GetString(element, "password") ?? GetString(element, "pass");
|
||||
var token = GetString(element, "token") ?? GetString(element, "registryToken");
|
||||
var identityToken = GetString(element, "identityToken") ?? GetString(element, "identitytoken");
|
||||
var refreshToken = GetString(element, "refreshToken");
|
||||
var email = GetString(element, "email");
|
||||
var allowInsecure = GetBoolean(element, "allowInsecureTls");
|
||||
|
||||
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
PopulateHeaders(element, headers);
|
||||
PopulateMetadataHeaders(metadata, headers);
|
||||
|
||||
var scopes = new List<string>();
|
||||
PopulateScopes(element, scopes);
|
||||
PopulateMetadataScopes(metadata, scopes);
|
||||
|
||||
var expiresAt = ParseDateTime(element, "expiresAt");
|
||||
|
||||
var auth = GetString(element, "auth");
|
||||
if (!string.IsNullOrWhiteSpace(auth))
|
||||
{
|
||||
TryApplyBasicAuth(auth, ref username, ref password);
|
||||
}
|
||||
|
||||
username ??= GetMetadataValue(metadata, "username");
|
||||
password ??= GetMetadataValue(metadata, "password");
|
||||
token ??= GetMetadataValue(metadata, "token") ?? GetMetadataValue(metadata, "registryToken");
|
||||
identityToken ??= GetMetadataValue(metadata, "identityToken");
|
||||
refreshToken ??= GetMetadataValue(metadata, "refreshToken");
|
||||
email ??= GetMetadataValue(metadata, "email");
|
||||
|
||||
return new RegistryCredential(
|
||||
registry,
|
||||
username?.Trim(),
|
||||
password,
|
||||
identityToken,
|
||||
token,
|
||||
refreshToken,
|
||||
expiresAt,
|
||||
scopes.Count == 0 ? Array.Empty<string>() : new ReadOnlyCollection<string>(scopes),
|
||||
allowInsecure,
|
||||
new ReadOnlyDictionary<string, string>(headers),
|
||||
email);
|
||||
}
|
||||
|
||||
private static bool TryCreateRegistryEntryFromMetadata(
|
||||
IReadOnlyDictionary<string, string> metadata,
|
||||
out RegistryCredential entry)
|
||||
{
|
||||
var registry = GetMetadataValue(metadata, "registry");
|
||||
var username = GetMetadataValue(metadata, "username");
|
||||
var password = GetMetadataValue(metadata, "password");
|
||||
var identityToken = GetMetadataValue(metadata, "identityToken");
|
||||
var token = GetMetadataValue(metadata, "token") ?? GetMetadataValue(metadata, "registryToken");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(registry) &&
|
||||
string.IsNullOrWhiteSpace(username) &&
|
||||
string.IsNullOrWhiteSpace(password) &&
|
||||
string.IsNullOrWhiteSpace(identityToken) &&
|
||||
string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
entry = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
PopulateMetadataHeaders(metadata, headers);
|
||||
|
||||
var scopes = new List<string>();
|
||||
PopulateMetadataScopes(metadata, scopes);
|
||||
|
||||
entry = new RegistryCredential(
|
||||
registry?.Trim() ?? "registry.local",
|
||||
username?.Trim(),
|
||||
password,
|
||||
identityToken,
|
||||
token,
|
||||
GetMetadataValue(metadata, "refreshToken"),
|
||||
ParseDateTime(metadata, "expiresAt"),
|
||||
scopes.Count == 0 ? Array.Empty<string>() : new ReadOnlyCollection<string>(scopes),
|
||||
ParseBoolean(metadata, "allowInsecureTls"),
|
||||
new ReadOnlyDictionary<string, string>(headers),
|
||||
GetMetadataValue(metadata, "email"));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void PopulateScopes(JsonElement element, ICollection<string> scopes)
|
||||
{
|
||||
if (!TryGetPropertyIgnoreCase(element, "scopes", out var scopesElement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (scopesElement.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Array:
|
||||
foreach (var scope in scopesElement.EnumerateArray())
|
||||
{
|
||||
if (scope.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var value = scope.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
scopes.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case JsonValueKind.String:
|
||||
var text = scopesElement.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
foreach (var part in text.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
scopes.Add(part.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PopulateMetadataScopes(IReadOnlyDictionary<string, string> metadata, ICollection<string> scopes)
|
||||
{
|
||||
foreach (var (key, value) in metadata)
|
||||
{
|
||||
if (!key.StartsWith("scope", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
scopes.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryApplyBasicAuth(string auth, ref string? username, ref string? password)
|
||||
{
|
||||
try
|
||||
{
|
||||
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(auth));
|
||||
var separator = decoded.IndexOf(':');
|
||||
if (separator >= 0)
|
||||
{
|
||||
username ??= decoded[..separator];
|
||||
password ??= decoded[(separator + 1)..];
|
||||
}
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// ignore malformed auth; caller may still have explicit username/password fields
|
||||
}
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ParseDateTime(JsonElement element, string propertyName)
|
||||
{
|
||||
if (!TryGetPropertyIgnoreCase(element, propertyName, out var value) ||
|
||||
value.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var text = value.GetString();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DateTimeOffset.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ParseDateTime(IReadOnlyDictionary<string, string> metadata, string key)
|
||||
{
|
||||
var value = GetMetadataValue(metadata, key);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool? ParseBoolean(IReadOnlyDictionary<string, string> metadata, string key)
|
||||
{
|
||||
var value = GetMetadataValue(metadata, key);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bool.TryParse(value, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user