Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Configuration;
namespace StellaOps.Cli.Services;
/// <summary>
/// Emits Authority configuration diagnostics discovered during CLI startup.
/// </summary>
internal static class AuthorityDiagnosticsReporter
{
public static void Emit(IConfiguration configuration, ILogger logger)
{
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(logger);
var basePath = Directory.GetCurrentDirectory();
EmitInternal(configuration, logger, basePath);
}
internal static void Emit(IConfiguration configuration, ILogger logger, string basePath)
{
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(basePath);
EmitInternal(configuration, logger, basePath);
}
private static void EmitInternal(IConfiguration configuration, ILogger logger, string basePath)
{
if (string.IsNullOrWhiteSpace(basePath))
{
basePath = Directory.GetCurrentDirectory();
}
var authoritySection = configuration.GetSection("Authority");
if (!authoritySection.Exists())
{
return;
}
var authorityOptions = new StellaOpsAuthorityOptions();
authoritySection.Bind(authorityOptions);
if (authorityOptions.Plugins.Descriptors.Count == 0)
{
return;
}
var resolvedBasePath = Path.GetFullPath(basePath);
IReadOnlyList<AuthorityPluginContext> contexts;
try
{
contexts = AuthorityPluginConfigurationLoader.Load(authorityOptions, resolvedBasePath);
}
catch (Exception ex)
{
logger.LogDebug(ex, "Failed to load Authority plug-in configuration for diagnostics.");
return;
}
if (contexts.Count == 0)
{
return;
}
IReadOnlyList<AuthorityConfigurationDiagnostic> diagnostics;
try
{
diagnostics = AuthorityPluginConfigurationAnalyzer.Analyze(contexts);
}
catch (Exception ex)
{
logger.LogDebug(ex, "Failed to analyze Authority plug-in configuration for diagnostics.");
return;
}
if (diagnostics.Count == 0)
{
return;
}
var contextLookup = new Dictionary<string, AuthorityPluginContext>(StringComparer.OrdinalIgnoreCase);
foreach (var context in contexts)
{
if (context?.Manifest?.Name is { Length: > 0 } name && !contextLookup.ContainsKey(name))
{
contextLookup[name] = context;
}
}
foreach (var diagnostic in diagnostics)
{
var level = diagnostic.Severity switch
{
AuthorityConfigurationDiagnosticSeverity.Error => LogLevel.Error,
AuthorityConfigurationDiagnosticSeverity.Warning => LogLevel.Warning,
_ => LogLevel.Information
};
if (!logger.IsEnabled(level))
{
continue;
}
if (contextLookup.TryGetValue(diagnostic.PluginName, out var context) &&
context?.Manifest?.ConfigPath is { Length: > 0 } configPath)
{
logger.Log(level, "{DiagnosticMessage} (config: {ConfigPath})", diagnostic.Message, configPath);
}
else
{
logger.Log(level, "{DiagnosticMessage}", diagnostic.Message);
}
}
}
}

View File

@@ -0,0 +1,223 @@
using System;
using System.Buffers.Text;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.Client;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
internal sealed class AuthorityRevocationClient : IAuthorityRevocationClient
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private static readonly TimeSpan TokenRefreshSkew = TimeSpan.FromSeconds(30);
private readonly HttpClient httpClient;
private readonly StellaOpsCliOptions options;
private readonly ILogger<AuthorityRevocationClient> logger;
private readonly IStellaOpsTokenClient? tokenClient;
private readonly object tokenSync = new();
private string? cachedAccessToken;
private DateTimeOffset cachedAccessTokenExpiresAt = DateTimeOffset.MinValue;
public AuthorityRevocationClient(
HttpClient httpClient,
StellaOpsCliOptions options,
ILogger<AuthorityRevocationClient> logger,
IStellaOpsTokenClient? tokenClient = null)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
this.options = options ?? throw new ArgumentNullException(nameof(options));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.tokenClient = tokenClient;
if (!string.IsNullOrWhiteSpace(options.Authority?.Url) && httpClient.BaseAddress is null && Uri.TryCreate(options.Authority.Url, UriKind.Absolute, out var authorityUri))
{
httpClient.BaseAddress = authorityUri;
}
}
public async Task<AuthorityRevocationExportResult> ExportAsync(bool verbose, CancellationToken cancellationToken)
{
EnsureAuthorityConfigured();
using var request = new HttpRequestMessage(HttpMethod.Get, "internal/revocations/export");
var accessToken = await AcquireAccessTokenAsync(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(accessToken))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var message = $"Authority export request failed with {(int)response.StatusCode} {response.ReasonPhrase}: {body}";
throw new InvalidOperationException(message);
}
var payload = await JsonSerializer.DeserializeAsync<ExportResponseDto>(
await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false),
SerializerOptions,
cancellationToken).ConfigureAwait(false);
if (payload is null)
{
throw new InvalidOperationException("Authority export response payload was empty.");
}
var bundleBytes = Convert.FromBase64String(payload.Bundle.Data);
var digest = payload.Digest?.Value ?? string.Empty;
if (verbose)
{
logger.LogInformation(
"Received revocation export sequence {Sequence} (sha256:{Digest}, signing key {KeyId}, provider {Provider}).",
payload.Sequence,
digest,
payload.SigningKeyId ?? "<unspecified>",
string.IsNullOrWhiteSpace(payload.Signature?.Provider) ? "default" : payload.Signature!.Provider);
}
return new AuthorityRevocationExportResult
{
BundleBytes = bundleBytes,
Signature = payload.Signature?.Value ?? string.Empty,
Digest = digest,
Sequence = payload.Sequence,
IssuedAt = payload.IssuedAt,
SigningKeyId = payload.SigningKeyId,
SigningProvider = payload.Signature?.Provider
};
}
private async Task<string?> AcquireAccessTokenAsync(CancellationToken cancellationToken)
{
if (tokenClient is null)
{
return null;
}
lock (tokenSync)
{
if (!string.IsNullOrEmpty(cachedAccessToken) && cachedAccessTokenExpiresAt - TokenRefreshSkew > DateTimeOffset.UtcNow)
{
return cachedAccessToken;
}
}
var scope = AuthorityTokenUtilities.ResolveScope(options);
var token = await RequestAccessTokenAsync(scope, cancellationToken).ConfigureAwait(false);
lock (tokenSync)
{
cachedAccessToken = token.AccessToken;
cachedAccessTokenExpiresAt = token.ExpiresAtUtc;
return cachedAccessToken;
}
}
private async Task<StellaOpsTokenResult> RequestAccessTokenAsync(string scope, CancellationToken cancellationToken)
{
if (options.Authority is null)
{
throw new InvalidOperationException("Authority credentials are not configured.");
}
if (!string.IsNullOrWhiteSpace(options.Authority.Username))
{
if (string.IsNullOrWhiteSpace(options.Authority.Password))
{
throw new InvalidOperationException("Authority password must be configured or run 'auth login'.");
}
return await tokenClient!.RequestPasswordTokenAsync(
options.Authority.Username,
options.Authority.Password!,
scope,
null,
cancellationToken).ConfigureAwait(false);
}
return await tokenClient!.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false);
}
private void EnsureAuthorityConfigured()
{
if (string.IsNullOrWhiteSpace(options.Authority?.Url))
{
throw new InvalidOperationException("Authority URL is not configured. Set STELLAOPS_AUTHORITY_URL or update stellaops.yaml.");
}
if (httpClient.BaseAddress is null)
{
if (!Uri.TryCreate(options.Authority.Url, UriKind.Absolute, out var baseUri))
{
throw new InvalidOperationException("Authority URL is invalid.");
}
httpClient.BaseAddress = baseUri;
}
}
private sealed class ExportResponseDto
{
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; set; } = string.Empty;
[JsonPropertyName("bundleId")]
public string BundleId { get; set; } = string.Empty;
[JsonPropertyName("sequence")]
public long Sequence { get; set; }
[JsonPropertyName("issuedAt")]
public DateTimeOffset IssuedAt { get; set; }
[JsonPropertyName("signingKeyId")]
public string? SigningKeyId { get; set; }
[JsonPropertyName("bundle")]
public ExportPayloadDto Bundle { get; set; } = new();
[JsonPropertyName("signature")]
public ExportSignatureDto? Signature { get; set; }
[JsonPropertyName("digest")]
public ExportDigestDto? Digest { get; set; }
}
private sealed class ExportPayloadDto
{
[JsonPropertyName("data")]
public string Data { get; set; } = string.Empty;
}
private sealed class ExportSignatureDto
{
[JsonPropertyName("algorithm")]
public string Algorithm { get; set; } = string.Empty;
[JsonPropertyName("keyId")]
public string KeyId { get; set; } = string.Empty;
[JsonPropertyName("provider")]
public string Provider { get; set; } = string.Empty;
[JsonPropertyName("value")]
public string Value { get; set; } = string.Empty;
}
private sealed class ExportDigestDto
{
[JsonPropertyName("value")]
public string Value { get; set; } = string.Empty;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,250 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
internal sealed class ConcelierObservationsClient : IConcelierObservationsClient
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private static readonly TimeSpan TokenRefreshSkew = TimeSpan.FromSeconds(30);
private readonly HttpClient httpClient;
private readonly StellaOpsCliOptions options;
private readonly ILogger<ConcelierObservationsClient> logger;
private readonly IStellaOpsTokenClient? tokenClient;
private readonly object tokenSync = new();
private string? cachedAccessToken;
private DateTimeOffset cachedAccessTokenExpiresAt = DateTimeOffset.MinValue;
public ConcelierObservationsClient(
HttpClient httpClient,
StellaOpsCliOptions options,
ILogger<ConcelierObservationsClient> logger,
IStellaOpsTokenClient? tokenClient = null)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
this.options = options ?? throw new ArgumentNullException(nameof(options));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.tokenClient = tokenClient;
if (!string.IsNullOrWhiteSpace(options.ConcelierUrl) && httpClient.BaseAddress is null)
{
if (Uri.TryCreate(options.ConcelierUrl, UriKind.Absolute, out var baseUri))
{
httpClient.BaseAddress = baseUri;
}
}
}
public async Task<AdvisoryObservationsResponse> GetObservationsAsync(
AdvisoryObservationsQuery query,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
EnsureConfigured();
var requestUri = BuildRequestUri(query);
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError(
"Failed to query observations (status {StatusCode}). Response: {Payload}",
(int)response.StatusCode,
string.IsNullOrWhiteSpace(payload) ? "<empty>" : payload);
response.EnsureSuccessStatusCode();
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var result = await JsonSerializer
.DeserializeAsync<AdvisoryObservationsResponse>(stream, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
return result ?? new AdvisoryObservationsResponse();
}
private static string BuildRequestUri(AdvisoryObservationsQuery query)
{
var builder = new StringBuilder("/concelier/observations?tenant=");
builder.Append(Uri.EscapeDataString(query.Tenant));
AppendValues(builder, "observationId", query.ObservationIds);
AppendValues(builder, "alias", query.Aliases);
AppendValues(builder, "purl", query.Purls);
AppendValues(builder, "cpe", query.Cpes);
if (query.Limit.HasValue && query.Limit.Value > 0)
{
builder.Append('&');
builder.Append("limit=");
builder.Append(query.Limit.Value.ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrWhiteSpace(query.Cursor))
{
builder.Append('&');
builder.Append("cursor=");
builder.Append(Uri.EscapeDataString(query.Cursor));
}
return builder.ToString();
static void AppendValues(StringBuilder builder, string name, IReadOnlyList<string> values)
{
if (values is null || values.Count == 0)
{
return;
}
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
builder.Append('&');
builder.Append(name);
builder.Append('=');
builder.Append(Uri.EscapeDataString(value));
}
}
}
private void EnsureConfigured()
{
if (!string.IsNullOrWhiteSpace(options.ConcelierUrl))
{
return;
}
throw new InvalidOperationException(
"ConcelierUrl is not configured. Set StellaOps:ConcelierUrl or STELLAOPS_CONCELIER_URL.");
}
private async Task AuthorizeRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await ResolveAccessTokenAsync(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(token))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
private async Task<string?> ResolveAccessTokenAsync(CancellationToken cancellationToken)
{
if (!string.IsNullOrWhiteSpace(options.ApiKey))
{
return options.ApiKey;
}
if (tokenClient is null || string.IsNullOrWhiteSpace(options.Authority.Url))
{
return null;
}
var now = DateTimeOffset.UtcNow;
lock (tokenSync)
{
if (!string.IsNullOrEmpty(cachedAccessToken) && now < cachedAccessTokenExpiresAt - TokenRefreshSkew)
{
return cachedAccessToken;
}
}
var (scope, cacheKey) = BuildScopeAndCacheKey(options);
var cachedEntry = await tokenClient.GetCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false);
if (cachedEntry is not null && now < cachedEntry.ExpiresAtUtc - TokenRefreshSkew)
{
lock (tokenSync)
{
cachedAccessToken = cachedEntry.AccessToken;
cachedAccessTokenExpiresAt = cachedEntry.ExpiresAtUtc;
return cachedAccessToken;
}
}
StellaOpsTokenResult token;
if (!string.IsNullOrWhiteSpace(options.Authority.Username))
{
if (string.IsNullOrWhiteSpace(options.Authority.Password))
{
throw new InvalidOperationException("Authority password must be configured when username is provided.");
}
token = await tokenClient.RequestPasswordTokenAsync(
options.Authority.Username,
options.Authority.Password!,
scope,
null,
cancellationToken).ConfigureAwait(false);
}
else
{
token = await tokenClient.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false);
}
await tokenClient.CacheTokenAsync(cacheKey, token.ToCacheEntry(), cancellationToken).ConfigureAwait(false);
lock (tokenSync)
{
cachedAccessToken = token.AccessToken;
cachedAccessTokenExpiresAt = token.ExpiresAtUtc;
return cachedAccessToken;
}
}
private static (string Scope, string CacheKey) BuildScopeAndCacheKey(StellaOpsCliOptions options)
{
var baseScope = AuthorityTokenUtilities.ResolveScope(options);
var finalScope = EnsureScope(baseScope, StellaOpsScopes.VulnRead);
var credential = !string.IsNullOrWhiteSpace(options.Authority.Username)
? $"user:{options.Authority.Username}"
: $"client:{options.Authority.ClientId}";
var cacheKey = $"{options.Authority.Url}|{credential}|{finalScope}";
return (finalScope, cacheKey);
}
private static string EnsureScope(string scopes, string required)
{
if (string.IsNullOrWhiteSpace(scopes))
{
return required;
}
var parts = scopes
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(static scope => scope.ToLowerInvariant())
.Distinct(StringComparer.Ordinal)
.ToList();
if (!parts.Contains(required, StringComparer.Ordinal))
{
parts.Add(required);
}
return string.Join(' ', parts);
}
}

View File

@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
internal interface IAuthorityRevocationClient
{
Task<AuthorityRevocationExportResult> ExportAsync(bool verbose, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
internal interface IBackendOperationsClient
{
Task<ScannerArtifactResult> DownloadScannerAsync(string channel, string outputPath, bool overwrite, bool verbose, CancellationToken cancellationToken);
Task UploadScanResultsAsync(string filePath, CancellationToken cancellationToken);
Task<JobTriggerResult> TriggerJobAsync(string jobKind, IDictionary<string, object?> parameters, CancellationToken cancellationToken);
Task<ExcititorOperationResult> ExecuteExcititorOperationAsync(string route, HttpMethod method, object? payload, CancellationToken cancellationToken);
Task<ExcititorExportDownloadResult> DownloadExcititorExportAsync(string exportId, string destinationPath, string? expectedDigestAlgorithm, string? expectedDigest, CancellationToken cancellationToken);
Task<IReadOnlyList<ExcititorProviderSummary>> GetExcititorProvidersAsync(bool includeDisabled, CancellationToken cancellationToken);
Task<RuntimePolicyEvaluationResult> EvaluateRuntimePolicyAsync(RuntimePolicyEvaluationRequest request, CancellationToken cancellationToken);
Task<PolicySimulationResult> SimulatePolicyAsync(string policyId, PolicySimulationInput input, CancellationToken cancellationToken);
Task<PolicyActivationResult> ActivatePolicyRevisionAsync(string policyId, int version, PolicyActivationRequest request, CancellationToken cancellationToken);
Task<OfflineKitDownloadResult> DownloadOfflineKitAsync(string? bundleId, string destinationDirectory, bool overwrite, bool resume, CancellationToken cancellationToken);
Task<OfflineKitImportResult> ImportOfflineKitAsync(OfflineKitImportRequest request, CancellationToken cancellationToken);
Task<OfflineKitStatus> GetOfflineKitStatusAsync(CancellationToken cancellationToken);
Task<AocIngestDryRunResponse> ExecuteAocIngestDryRunAsync(AocIngestDryRunRequest request, CancellationToken cancellationToken);
Task<AocVerifyResponse> ExecuteAocVerifyAsync(AocVerifyRequest request, CancellationToken cancellationToken);
Task<PolicyFindingsPage> GetPolicyFindingsAsync(PolicyFindingsQuery query, CancellationToken cancellationToken);
Task<PolicyFindingDocument> GetPolicyFindingAsync(string policyId, string findingId, CancellationToken cancellationToken);
Task<PolicyFindingExplainResult> GetPolicyFindingExplainAsync(string policyId, string findingId, string? mode, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,12 @@
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
internal interface IConcelierObservationsClient
{
Task<AdvisoryObservationsResponse> GetObservationsAsync(
AdvisoryObservationsQuery query,
CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Cli.Services;
internal interface IScannerExecutor
{
Task<ScannerExecutionResult> RunAsync(
string runner,
string entry,
string targetDirectory,
string resultsDirectory,
IReadOnlyList<string> arguments,
bool verbose,
CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Cli.Services;
internal interface IScannerInstaller
{
Task InstallAsync(string artifactPath, bool verbose, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
internal sealed record AdvisoryObservationsQuery(
string Tenant,
IReadOnlyList<string> ObservationIds,
IReadOnlyList<string> Aliases,
IReadOnlyList<string> Purls,
IReadOnlyList<string> Cpes,
int? Limit,
string? Cursor);
internal sealed class AdvisoryObservationsResponse
{
[JsonPropertyName("observations")]
public IReadOnlyList<AdvisoryObservationDocument> Observations { get; init; } =
Array.Empty<AdvisoryObservationDocument>();
[JsonPropertyName("linkset")]
public AdvisoryObservationLinksetAggregate Linkset { get; init; } =
new();
[JsonPropertyName("nextCursor")]
public string? NextCursor { get; init; }
[JsonPropertyName("hasMore")]
public bool HasMore { get; init; }
}
internal sealed class AdvisoryObservationDocument
{
[JsonPropertyName("observationId")]
public string ObservationId { get; init; } = string.Empty;
[JsonPropertyName("tenant")]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("source")]
public AdvisoryObservationSource Source { get; init; } = new();
[JsonPropertyName("upstream")]
public AdvisoryObservationUpstream Upstream { get; init; } = new();
[JsonPropertyName("linkset")]
public AdvisoryObservationLinkset Linkset { get; init; } = new();
[JsonPropertyName("createdAt")]
public DateTimeOffset CreatedAt { get; init; }
}
internal sealed class AdvisoryObservationSource
{
[JsonPropertyName("vendor")]
public string Vendor { get; init; } = string.Empty;
[JsonPropertyName("stream")]
public string Stream { get; init; } = string.Empty;
[JsonPropertyName("api")]
public string Api { get; init; } = string.Empty;
[JsonPropertyName("collectorVersion")]
public string? CollectorVersion { get; init; }
}
internal sealed class AdvisoryObservationUpstream
{
[JsonPropertyName("upstreamId")]
public string UpstreamId { get; init; } = string.Empty;
[JsonPropertyName("documentVersion")]
public string? DocumentVersion { get; init; }
}
internal sealed class AdvisoryObservationLinkset
{
[JsonPropertyName("aliases")]
public IReadOnlyList<string> Aliases { get; init; } = Array.Empty<string>();
[JsonPropertyName("purls")]
public IReadOnlyList<string> Purls { get; init; } = Array.Empty<string>();
[JsonPropertyName("cpes")]
public IReadOnlyList<string> Cpes { get; init; } = Array.Empty<string>();
[JsonPropertyName("references")]
public IReadOnlyList<AdvisoryObservationReference> References { get; init; } =
Array.Empty<AdvisoryObservationReference>();
}
internal sealed class AdvisoryObservationReference
{
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("url")]
public string Url { get; init; } = string.Empty;
}
internal sealed class AdvisoryObservationLinksetAggregate
{
[JsonPropertyName("aliases")]
public IReadOnlyList<string> Aliases { get; init; } = Array.Empty<string>();
[JsonPropertyName("purls")]
public IReadOnlyList<string> Purls { get; init; } = Array.Empty<string>();
[JsonPropertyName("cpes")]
public IReadOnlyList<string> Cpes { get; init; } = Array.Empty<string>();
[JsonPropertyName("references")]
public IReadOnlyList<AdvisoryObservationReference> References { get; init; } =
Array.Empty<AdvisoryObservationReference>();
}

View File

@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
internal sealed class AocIngestDryRunRequest
{
[JsonPropertyName("tenant")]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("source")]
public string Source { get; init; } = string.Empty;
[JsonPropertyName("document")]
public AocIngestDryRunDocument Document { get; init; } = new();
}
internal sealed class AocIngestDryRunDocument
{
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("content")]
public string Content { get; init; } = string.Empty;
[JsonPropertyName("contentType")]
public string ContentType { get; init; } = "application/json";
[JsonPropertyName("contentEncoding")]
public string? ContentEncoding { get; init; }
}
internal sealed class AocIngestDryRunResponse
{
[JsonPropertyName("source")]
public string? Source { get; init; }
[JsonPropertyName("tenant")]
public string? Tenant { get; init; }
[JsonPropertyName("guardVersion")]
public string? GuardVersion { get; init; }
[JsonPropertyName("status")]
public string? Status { get; init; }
[JsonPropertyName("document")]
public AocIngestDryRunDocumentResult Document { get; init; } = new();
[JsonPropertyName("violations")]
public IReadOnlyList<AocIngestDryRunViolation> Violations { get; init; } =
Array.Empty<AocIngestDryRunViolation>();
}
internal sealed class AocIngestDryRunDocumentResult
{
[JsonPropertyName("contentHash")]
public string? ContentHash { get; init; }
[JsonPropertyName("supersedes")]
public string? Supersedes { get; init; }
[JsonPropertyName("provenance")]
public AocIngestDryRunProvenance Provenance { get; init; } = new();
}
internal sealed class AocIngestDryRunProvenance
{
[JsonPropertyName("signature")]
public AocIngestDryRunSignature Signature { get; init; } = new();
}
internal sealed class AocIngestDryRunSignature
{
[JsonPropertyName("format")]
public string? Format { get; init; }
[JsonPropertyName("present")]
public bool Present { get; init; }
}
internal sealed class AocIngestDryRunViolation
{
[JsonPropertyName("code")]
public string Code { get; init; } = string.Empty;
[JsonPropertyName("message")]
public string Message { get; init; } = string.Empty;
[JsonPropertyName("path")]
public string? Path { get; init; }
}

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
internal sealed class AocVerifyRequest
{
[JsonPropertyName("tenant")]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("since")]
public string? Since { get; init; }
[JsonPropertyName("limit")]
public int? Limit { get; init; }
[JsonPropertyName("sources")]
public IReadOnlyList<string>? Sources { get; init; }
[JsonPropertyName("codes")]
public IReadOnlyList<string>? Codes { get; init; }
}
internal sealed class AocVerifyResponse
{
[JsonPropertyName("tenant")]
public string? Tenant { get; init; }
[JsonPropertyName("window")]
public AocVerifyWindow Window { get; init; } = new();
[JsonPropertyName("checked")]
public AocVerifyChecked Checked { get; init; } = new();
[JsonPropertyName("violations")]
public IReadOnlyList<AocVerifyViolation> Violations { get; init; } =
Array.Empty<AocVerifyViolation>();
[JsonPropertyName("metrics")]
public AocVerifyMetrics Metrics { get; init; } = new();
[JsonPropertyName("truncated")]
public bool? Truncated { get; init; }
}
internal sealed class AocVerifyWindow
{
[JsonPropertyName("from")]
public DateTimeOffset? From { get; init; }
[JsonPropertyName("to")]
public DateTimeOffset? To { get; init; }
}
internal sealed class AocVerifyChecked
{
[JsonPropertyName("advisories")]
public int Advisories { get; init; }
[JsonPropertyName("vex")]
public int Vex { get; init; }
}
internal sealed class AocVerifyViolation
{
[JsonPropertyName("code")]
public string Code { get; init; } = string.Empty;
[JsonPropertyName("count")]
public int Count { get; init; }
[JsonPropertyName("examples")]
public IReadOnlyList<AocVerifyViolationExample> Examples { get; init; } =
Array.Empty<AocVerifyViolationExample>();
}
internal sealed class AocVerifyViolationExample
{
[JsonPropertyName("source")]
public string? Source { get; init; }
[JsonPropertyName("documentId")]
public string? DocumentId { get; init; }
[JsonPropertyName("contentHash")]
public string? ContentHash { get; init; }
[JsonPropertyName("path")]
public string? Path { get; init; }
}
internal sealed class AocVerifyMetrics
{
[JsonPropertyName("ingestion_write_total")]
public int? IngestionWriteTotal { get; init; }
[JsonPropertyName("aoc_violation_total")]
public int? AocViolationTotal { get; init; }
}

View File

@@ -0,0 +1,20 @@
using System;
namespace StellaOps.Cli.Services.Models;
internal sealed class AuthorityRevocationExportResult
{
public required byte[] BundleBytes { get; init; }
public required string Signature { get; init; }
public required string Digest { get; init; }
public required long Sequence { get; init; }
public required DateTimeOffset IssuedAt { get; init; }
public string? SigningKeyId { get; init; }
public string? SigningProvider { get; init; }
}

View File

@@ -0,0 +1,6 @@
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorExportDownloadResult(
string Path,
long SizeBytes,
bool FromCache);

View File

@@ -0,0 +1,9 @@
using System.Text.Json;
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorOperationResult(
bool Success,
string Message,
string? Location,
JsonElement? Payload);

View File

@@ -0,0 +1,11 @@
using System;
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorProviderSummary(
string Id,
string Kind,
string DisplayName,
string TrustTier,
bool Enabled,
DateTimeOffset? LastIngestedAt);

View File

@@ -0,0 +1,9 @@
using StellaOps.Cli.Services.Models.Transport;
namespace StellaOps.Cli.Services.Models;
internal sealed record JobTriggerResult(
bool Success,
string Message,
string? Location,
JobRunResponse? Run);

View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record OfflineKitBundleDescriptor(
string BundleId,
string BundleName,
string BundleSha256,
long BundleSize,
Uri BundleDownloadUri,
string ManifestName,
string ManifestSha256,
Uri ManifestDownloadUri,
DateTimeOffset CapturedAt,
string? Channel,
string? Kind,
bool IsDelta,
string? BaseBundleId,
string? BundleSignatureName,
Uri? BundleSignatureDownloadUri,
string? ManifestSignatureName,
Uri? ManifestSignatureDownloadUri,
long? ManifestSize);
internal sealed record OfflineKitDownloadResult(
OfflineKitBundleDescriptor Descriptor,
string BundlePath,
string ManifestPath,
string? BundleSignaturePath,
string? ManifestSignaturePath,
string MetadataPath,
bool FromCache);
internal sealed record OfflineKitImportRequest(
string BundlePath,
string? ManifestPath,
string? BundleSignaturePath,
string? ManifestSignaturePath,
string? BundleId,
string? BundleSha256,
long? BundleSize,
DateTimeOffset? CapturedAt,
string? Channel,
string? Kind,
bool? IsDelta,
string? BaseBundleId,
string? ManifestSha256,
long? ManifestSize);
internal sealed record OfflineKitImportResult(
string? ImportId,
string? Status,
DateTimeOffset SubmittedAt,
string? Message);
internal sealed record OfflineKitStatus(
string? BundleId,
string? Channel,
string? Kind,
bool IsDelta,
string? BaseBundleId,
DateTimeOffset? CapturedAt,
DateTimeOffset? ImportedAt,
string? BundleSha256,
long? BundleSize,
IReadOnlyList<OfflineKitComponentStatus> Components);
internal sealed record OfflineKitComponentStatus(
string Name,
string? Version,
string? Digest,
DateTimeOffset? CapturedAt,
long? SizeBytes);
internal sealed record OfflineKitMetadataDocument
{
public string? BundleId { get; init; }
public string BundleName { get; init; } = string.Empty;
public string BundleSha256 { get; init; } = string.Empty;
public long BundleSize { get; init; }
public string BundlePath { get; init; } = string.Empty;
public DateTimeOffset CapturedAt { get; init; }
public DateTimeOffset DownloadedAt { get; init; }
public string? Channel { get; init; }
public string? Kind { get; init; }
public bool IsDelta { get; init; }
public string? BaseBundleId { get; init; }
public string ManifestName { get; init; } = string.Empty;
public string ManifestSha256 { get; init; } = string.Empty;
public long? ManifestSize { get; init; }
public string ManifestPath { get; init; } = string.Empty;
public string? BundleSignaturePath { get; init; }
public string? ManifestSignaturePath { get; init; }
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record PolicyActivationRequest(
bool RunNow,
DateTimeOffset? ScheduledAt,
string? Priority,
bool Rollback,
string? IncidentId,
string? Comment);
internal sealed record PolicyActivationResult(
string Status,
PolicyActivationRevision Revision);
internal sealed record PolicyActivationRevision(
string PolicyId,
int Version,
string Status,
bool RequiresTwoPersonApproval,
DateTimeOffset CreatedAt,
DateTimeOffset? ActivatedAt,
IReadOnlyList<PolicyActivationApproval> Approvals);
internal sealed record PolicyActivationApproval(
string ActorId,
DateTimeOffset ApprovedAt,
string? Comment);

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record PolicyFindingsQuery(
string PolicyId,
IReadOnlyList<string> SbomIds,
IReadOnlyList<string> Statuses,
IReadOnlyList<string> Severities,
string? Cursor,
int? Page,
int? PageSize,
DateTimeOffset? Since);
internal sealed record PolicyFindingsPage(
IReadOnlyList<PolicyFindingDocument> Items,
string? NextCursor,
int? TotalCount);
internal sealed record PolicyFindingDocument(
string FindingId,
string Status,
PolicyFindingSeverity Severity,
string SbomId,
IReadOnlyList<string> AdvisoryIds,
PolicyFindingVexMetadata? Vex,
int PolicyVersion,
DateTimeOffset UpdatedAt,
string? RunId);
internal sealed record PolicyFindingSeverity(string Normalized, double? Score);
internal sealed record PolicyFindingVexMetadata(string? WinningStatementId, string? Source, string? Status);
internal sealed record PolicyFindingExplainResult(
string FindingId,
int PolicyVersion,
IReadOnlyList<PolicyFindingExplainStep> Steps,
IReadOnlyList<PolicyFindingExplainHint> SealedHints);
internal sealed record PolicyFindingExplainStep(
string Rule,
string? Status,
string? Action,
double? Score,
IReadOnlyDictionary<string, string> Inputs,
IReadOnlyDictionary<string, string>? Evidence);
internal sealed record PolicyFindingExplainHint(string Message);

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record PolicySimulationInput(
int? BaseVersion,
int? CandidateVersion,
IReadOnlyList<string> SbomSet,
IReadOnlyDictionary<string, object?> Environment,
bool Explain);
internal sealed record PolicySimulationResult(
PolicySimulationDiff Diff,
string? ExplainUri);
internal sealed record PolicySimulationDiff(
string? SchemaVersion,
int Added,
int Removed,
int Unchanged,
IReadOnlyDictionary<string, PolicySimulationSeverityDelta> BySeverity,
IReadOnlyList<PolicySimulationRuleDelta> RuleHits);
internal sealed record PolicySimulationSeverityDelta(int? Up, int? Down);
internal sealed record PolicySimulationRuleDelta(string RuleId, string RuleName, int? Up, int? Down);

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record RuntimePolicyEvaluationRequest(
string? Namespace,
IReadOnlyDictionary<string, string> Labels,
IReadOnlyList<string> Images);
internal sealed record RuntimePolicyEvaluationResult(
int TtlSeconds,
DateTimeOffset? ExpiresAtUtc,
string? PolicyRevision,
IReadOnlyDictionary<string, RuntimePolicyImageDecision> Decisions);
internal sealed record RuntimePolicyImageDecision(
string PolicyVerdict,
bool? Signed,
bool? HasSbomReferrers,
IReadOnlyList<string> Reasons,
RuntimePolicyRekorReference? Rekor,
IReadOnlyDictionary<string, object?> AdditionalProperties);
internal sealed record RuntimePolicyRekorReference(string? Uuid, string? Url, bool? Verified);

View File

@@ -0,0 +1,3 @@
namespace StellaOps.Cli.Services.Models;
internal sealed record ScannerArtifactResult(string Path, long SizeBytes, bool FromCache);

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class JobRunResponse
{
public Guid RunId { get; set; }
public string Kind { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string Trigger { get; set; } = string.Empty;
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? StartedAt { get; set; }
public DateTimeOffset? CompletedAt { get; set; }
public string? Error { get; set; }
public TimeSpan? Duration { get; set; }
public IReadOnlyDictionary<string, object?> Parameters { get; set; } = new Dictionary<string, object?>(StringComparer.Ordinal);
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class JobTriggerRequest
{
public string Trigger { get; set; } = "cli";
public Dictionary<string, object?> Parameters { get; set; } = new(StringComparer.Ordinal);
}

View File

@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class OfflineKitBundleDescriptorTransport
{
public string? BundleId { get; set; }
public string? BundleName { get; set; }
public string? BundleSha256 { get; set; }
public long BundleSize { get; set; }
public string? BundleUrl { get; set; }
public string? BundlePath { get; set; }
public string? BundleSignatureName { get; set; }
public string? BundleSignatureUrl { get; set; }
public string? BundleSignaturePath { get; set; }
public string? ManifestName { get; set; }
public string? ManifestSha256 { get; set; }
public long? ManifestSize { get; set; }
public string? ManifestUrl { get; set; }
public string? ManifestPath { get; set; }
public string? ManifestSignatureName { get; set; }
public string? ManifestSignatureUrl { get; set; }
public string? ManifestSignaturePath { get; set; }
public DateTimeOffset? CapturedAt { get; set; }
public string? Channel { get; set; }
public string? Kind { get; set; }
public bool? IsDelta { get; set; }
public string? BaseBundleId { get; set; }
}
internal sealed class OfflineKitStatusBundleTransport
{
public string? BundleId { get; set; }
public string? Channel { get; set; }
public string? Kind { get; set; }
public bool? IsDelta { get; set; }
public string? BaseBundleId { get; set; }
public string? BundleSha256 { get; set; }
public long? BundleSize { get; set; }
public DateTimeOffset? CapturedAt { get; set; }
public DateTimeOffset? ImportedAt { get; set; }
}
internal sealed class OfflineKitStatusTransport
{
public OfflineKitStatusBundleTransport? Current { get; set; }
public List<OfflineKitComponentStatusTransport>? Components { get; set; }
}
internal sealed class OfflineKitComponentStatusTransport
{
public string? Name { get; set; }
public string? Version { get; set; }
public string? Digest { get; set; }
public DateTimeOffset? CapturedAt { get; set; }
public long? SizeBytes { get; set; }
}
internal sealed class OfflineKitImportResponseTransport
{
public string? ImportId { get; set; }
public string? Status { get; set; }
public DateTimeOffset? SubmittedAt { get; set; }
public string? Message { get; set; }
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class PolicyActivationRequestDocument
{
public string? Comment { get; set; }
public bool? RunNow { get; set; }
public DateTimeOffset? ScheduledAt { get; set; }
public string? Priority { get; set; }
public bool? Rollback { get; set; }
public string? IncidentId { get; set; }
}
internal sealed class PolicyActivationResponseDocument
{
public string? Status { get; set; }
public PolicyActivationRevisionDocument? Revision { get; set; }
}
internal sealed class PolicyActivationRevisionDocument
{
public string? PackId { get; set; }
public int? Version { get; set; }
public string? Status { get; set; }
public bool? RequiresTwoPersonApproval { get; set; }
public DateTimeOffset? CreatedAt { get; set; }
public DateTimeOffset? ActivatedAt { get; set; }
public List<PolicyActivationApprovalDocument>? Approvals { get; set; }
}
internal sealed class PolicyActivationApprovalDocument
{
public string? ActorId { get; set; }
public DateTimeOffset? ApprovedAt { get; set; }
public string? Comment { get; set; }
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class PolicyFindingsResponseDocument
{
public List<PolicyFindingDocumentDocument>? Items { get; set; }
public string? NextCursor { get; set; }
public int? TotalCount { get; set; }
}
internal sealed class PolicyFindingDocumentDocument
{
public string? FindingId { get; set; }
public string? Status { get; set; }
public PolicyFindingSeverityDocument? Severity { get; set; }
public string? SbomId { get; set; }
public List<string>? AdvisoryIds { get; set; }
public PolicyFindingVexDocument? Vex { get; set; }
public int? PolicyVersion { get; set; }
public DateTimeOffset? UpdatedAt { get; set; }
public string? RunId { get; set; }
}
internal sealed class PolicyFindingSeverityDocument
{
public string? Normalized { get; set; }
public double? Score { get; set; }
}
internal sealed class PolicyFindingVexDocument
{
public string? WinningStatementId { get; set; }
public string? Source { get; set; }
public string? Status { get; set; }
}
internal sealed class PolicyFindingExplainResponseDocument
{
public string? FindingId { get; set; }
public int? PolicyVersion { get; set; }
public List<PolicyFindingExplainStepDocument>? Steps { get; set; }
public List<PolicyFindingExplainHintDocument>? SealedHints { get; set; }
}
internal sealed class PolicyFindingExplainStepDocument
{
public string? Rule { get; set; }
public string? Status { get; set; }
public string? Action { get; set; }
public double? Score { get; set; }
public Dictionary<string, JsonElement>? Inputs { get; set; }
public Dictionary<string, JsonElement>? Evidence { get; set; }
}
internal sealed class PolicyFindingExplainHintDocument
{
public string? Message { get; set; }
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Text.Json;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class PolicySimulationRequestDocument
{
public int? BaseVersion { get; set; }
public int? CandidateVersion { get; set; }
public IReadOnlyList<string>? SbomSet { get; set; }
public Dictionary<string, JsonElement>? Env { get; set; }
public bool? Explain { get; set; }
}
internal sealed class PolicySimulationResponseDocument
{
public PolicySimulationDiffDocument? Diff { get; set; }
public string? ExplainUri { get; set; }
}
internal sealed class PolicySimulationDiffDocument
{
public string? SchemaVersion { get; set; }
public int? Added { get; set; }
public int? Removed { get; set; }
public int? Unchanged { get; set; }
public Dictionary<string, PolicySimulationSeverityDeltaDocument>? BySeverity { get; set; }
public List<PolicySimulationRuleDeltaDocument>? RuleHits { get; set; }
}
internal sealed class PolicySimulationSeverityDeltaDocument
{
public int? Up { get; set; }
public int? Down { get; set; }
}
internal sealed class PolicySimulationRuleDeltaDocument
{
public string? RuleId { get; set; }
public string? RuleName { get; set; }
public int? Up { get; set; }
public int? Down { get; set; }
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class ProblemDocument
{
public string? Type { get; set; }
public string? Title { get; set; }
public string? Detail { get; set; }
public int? Status { get; set; }
public string? Instance { get; set; }
public Dictionary<string, object?>? Extensions { get; set; }
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class RuntimePolicyEvaluationRequestDocument
{
[JsonPropertyName("namespace")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Namespace { get; set; }
[JsonPropertyName("labels")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, string>? Labels { get; set; }
[JsonPropertyName("images")]
public List<string> Images { get; set; } = new();
}
internal sealed class RuntimePolicyEvaluationResponseDocument
{
[JsonPropertyName("ttlSeconds")]
public int? TtlSeconds { get; set; }
[JsonPropertyName("expiresAtUtc")]
public DateTimeOffset? ExpiresAtUtc { get; set; }
[JsonPropertyName("policyRevision")]
public string? PolicyRevision { get; set; }
[JsonPropertyName("results")]
public Dictionary<string, RuntimePolicyEvaluationImageDocument>? Results { get; set; }
}
internal sealed class RuntimePolicyEvaluationImageDocument
{
[JsonPropertyName("policyVerdict")]
public string? PolicyVerdict { get; set; }
[JsonPropertyName("signed")]
public bool? Signed { get; set; }
[JsonPropertyName("hasSbomReferrers")]
public bool? HasSbomReferrers { get; set; }
// Legacy field kept for pre-contract-sync services.
[JsonPropertyName("hasSbom")]
public bool? HasSbomLegacy { get; set; }
[JsonPropertyName("reasons")]
public List<string>? Reasons { get; set; }
[JsonPropertyName("rekor")]
public RuntimePolicyRekorDocument? Rekor { get; set; }
[JsonExtensionData]
public Dictionary<string, JsonElement>? ExtensionData { get; set; }
}
internal sealed class RuntimePolicyRekorDocument
{
[JsonPropertyName("uuid")]
public string? Uuid { get; set; }
[JsonPropertyName("url")]
public string? Url { get; set; }
[JsonPropertyName("verified")]
public bool? Verified { get; set; }
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Net;
namespace StellaOps.Cli.Services;
internal sealed class PolicyApiException : Exception
{
public PolicyApiException(string message, HttpStatusCode statusCode, string? errorCode, Exception? innerException = null)
: base(message, innerException)
{
StatusCode = statusCode;
ErrorCode = errorCode;
}
public HttpStatusCode StatusCode { get; }
public string? ErrorCode { get; }
}

View File

@@ -0,0 +1,3 @@
namespace StellaOps.Cli.Services;
internal sealed record ScannerExecutionResult(int ExitCode, string ResultsPath, string RunMetadataPath);

View File

@@ -0,0 +1,329 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using System.Text.Json;
namespace StellaOps.Cli.Services;
internal sealed class ScannerExecutor : IScannerExecutor
{
private readonly ILogger<ScannerExecutor> _logger;
public ScannerExecutor(ILogger<ScannerExecutor> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<ScannerExecutionResult> RunAsync(
string runner,
string entry,
string targetDirectory,
string resultsDirectory,
IReadOnlyList<string> arguments,
bool verbose,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(targetDirectory))
{
throw new ArgumentException("Target directory must be provided.", nameof(targetDirectory));
}
runner = string.IsNullOrWhiteSpace(runner) ? "docker" : runner.Trim().ToLowerInvariant();
entry = entry?.Trim() ?? string.Empty;
var normalizedTarget = Path.GetFullPath(targetDirectory);
if (!Directory.Exists(normalizedTarget))
{
throw new DirectoryNotFoundException($"Scan target directory '{normalizedTarget}' does not exist.");
}
resultsDirectory = string.IsNullOrWhiteSpace(resultsDirectory)
? Path.Combine(Directory.GetCurrentDirectory(), "scan-results")
: Path.GetFullPath(resultsDirectory);
Directory.CreateDirectory(resultsDirectory);
var executionTimestamp = DateTimeOffset.UtcNow;
var baselineFiles = Directory.GetFiles(resultsDirectory, "*", SearchOption.AllDirectories);
var baseline = new HashSet<string>(baselineFiles, StringComparer.OrdinalIgnoreCase);
var startInfo = BuildProcessStartInfo(runner, entry, normalizedTarget, resultsDirectory, arguments);
using var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
var stdout = new List<string>();
var stderr = new List<string>();
process.OutputDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
stdout.Add(args.Data);
if (verbose)
{
_logger.LogInformation("[scan] {Line}", args.Data);
}
};
process.ErrorDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
stderr.Add(args.Data);
_logger.LogError("[scan] {Line}", args.Data);
};
_logger.LogInformation("Launching scanner via {Runner} (entry: {Entry})...", runner, entry);
if (!process.Start())
{
throw new InvalidOperationException("Failed to start scanner process.");
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
var completionTimestamp = DateTimeOffset.UtcNow;
if (process.ExitCode == 0)
{
_logger.LogInformation("Scanner completed successfully.");
}
else
{
_logger.LogWarning("Scanner exited with code {Code}.", process.ExitCode);
}
var resultsPath = ResolveResultsPath(resultsDirectory, executionTimestamp, baseline);
if (string.IsNullOrWhiteSpace(resultsPath))
{
resultsPath = CreatePlaceholderResult(resultsDirectory);
}
var metadataPath = WriteRunMetadata(
resultsDirectory,
executionTimestamp,
completionTimestamp,
runner,
entry,
normalizedTarget,
resultsPath,
arguments,
process.ExitCode,
stdout,
stderr);
return new ScannerExecutionResult(process.ExitCode, resultsPath, metadataPath);
}
private ProcessStartInfo BuildProcessStartInfo(
string runner,
string entry,
string targetDirectory,
string resultsDirectory,
IReadOnlyList<string> args)
{
return runner switch
{
"self" or "native" => BuildNativeStartInfo(entry, args),
"dotnet" => BuildDotNetStartInfo(entry, args),
"docker" => BuildDockerStartInfo(entry, targetDirectory, resultsDirectory, args),
_ => BuildCustomRunnerStartInfo(runner, entry, args)
};
}
private static ProcessStartInfo BuildNativeStartInfo(string binaryPath, IReadOnlyList<string> args)
{
if (string.IsNullOrWhiteSpace(binaryPath) || !File.Exists(binaryPath))
{
throw new FileNotFoundException("Scanner entrypoint not found.", binaryPath);
}
var startInfo = new ProcessStartInfo
{
FileName = binaryPath,
WorkingDirectory = Directory.GetCurrentDirectory()
};
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static ProcessStartInfo BuildDotNetStartInfo(string binaryPath, IReadOnlyList<string> args)
{
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
WorkingDirectory = Directory.GetCurrentDirectory()
};
startInfo.ArgumentList.Add(binaryPath);
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static ProcessStartInfo BuildDockerStartInfo(string image, string targetDirectory, string resultsDirectory, IReadOnlyList<string> args)
{
if (string.IsNullOrWhiteSpace(image))
{
throw new ArgumentException("Docker image must be provided when runner is 'docker'.", nameof(image));
}
var cwd = Directory.GetCurrentDirectory();
var startInfo = new ProcessStartInfo
{
FileName = "docker",
WorkingDirectory = cwd
};
startInfo.ArgumentList.Add("run");
startInfo.ArgumentList.Add("--rm");
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add($"{cwd}:{cwd}");
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add($"{targetDirectory}:/scan-target:ro");
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add($"{resultsDirectory}:/scan-results");
startInfo.ArgumentList.Add("-w");
startInfo.ArgumentList.Add(cwd);
startInfo.ArgumentList.Add(image);
startInfo.ArgumentList.Add("--target");
startInfo.ArgumentList.Add("/scan-target");
startInfo.ArgumentList.Add("--output");
startInfo.ArgumentList.Add("/scan-results/scan.json");
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static ProcessStartInfo BuildCustomRunnerStartInfo(string runner, string entry, IReadOnlyList<string> args)
{
var startInfo = new ProcessStartInfo
{
FileName = runner,
WorkingDirectory = Directory.GetCurrentDirectory()
};
if (!string.IsNullOrWhiteSpace(entry))
{
startInfo.ArgumentList.Add(entry);
}
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static string ResolveResultsPath(string resultsDirectory, DateTimeOffset startTimestamp, HashSet<string> baseline)
{
var candidates = Directory.GetFiles(resultsDirectory, "*", SearchOption.AllDirectories);
string? newest = null;
DateTimeOffset newestTimestamp = startTimestamp;
foreach (var candidate in candidates)
{
if (baseline.Contains(candidate))
{
continue;
}
var info = new FileInfo(candidate);
if (info.LastWriteTimeUtc >= newestTimestamp)
{
newestTimestamp = info.LastWriteTimeUtc;
newest = candidate;
}
}
return newest ?? string.Empty;
}
private static string CreatePlaceholderResult(string resultsDirectory)
{
var fileName = $"scan-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}.json";
var path = Path.Combine(resultsDirectory, fileName);
File.WriteAllText(path, "{\"status\":\"placeholder\"}");
return path;
}
private static string WriteRunMetadata(
string resultsDirectory,
DateTimeOffset startedAt,
DateTimeOffset completedAt,
string runner,
string entry,
string targetDirectory,
string resultsPath,
IReadOnlyList<string> arguments,
int exitCode,
IReadOnlyList<string> stdout,
IReadOnlyList<string> stderr)
{
var duration = completedAt - startedAt;
var payload = new
{
runner,
entry,
targetDirectory,
resultsPath,
arguments,
exitCode,
startedAt = startedAt,
completedAt = completedAt,
durationSeconds = Math.Round(duration.TotalSeconds, 3, MidpointRounding.AwayFromZero),
stdout,
stderr
};
var fileName = $"scan-run-{startedAt:yyyyMMddHHmmssfff}.json";
var path = Path.Combine(resultsDirectory, fileName);
var options = new JsonSerializerOptions
{
WriteIndented = true
};
var json = JsonSerializer.Serialize(payload, options);
File.WriteAllText(path, json);
return path;
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace StellaOps.Cli.Services;
internal sealed class ScannerInstaller : IScannerInstaller
{
private readonly ILogger<ScannerInstaller> _logger;
public ScannerInstaller(ILogger<ScannerInstaller> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task InstallAsync(string artifactPath, bool verbose, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(artifactPath) || !File.Exists(artifactPath))
{
throw new FileNotFoundException("Scanner artifact not found for installation.", artifactPath);
}
// Current implementation assumes docker-based scanner bundle.
var processInfo = new ProcessStartInfo
{
FileName = "docker",
ArgumentList = { "load", "-i", artifactPath },
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
using var process = new Process { StartInfo = processInfo, EnableRaisingEvents = true };
process.OutputDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
if (verbose)
{
_logger.LogInformation("[install] {Line}", args.Data);
}
};
process.ErrorDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
_logger.LogError("[install] {Line}", args.Data);
};
_logger.LogInformation("Installing scanner container from {Path}...", artifactPath);
if (!process.Start())
{
throw new InvalidOperationException("Failed to start container installation process.");
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"Container installation failed with exit code {process.ExitCode}.");
}
_logger.LogInformation("Scanner container installed successfully.");
}
}