up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-28 09:40:40 +02:00
parent 1c6730a1d2
commit 05da719048
206 changed files with 34741 additions and 1751 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Cli.Services.Models;
@@ -9,11 +10,12 @@ using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
/// <summary>
/// HTTP client for Authority console endpoints (CLI-TEN-47-001).
/// HTTP client for Authority console endpoints (CLI-TEN-47-001, CLI-TEN-49-001).
/// </summary>
internal sealed class AuthorityConsoleClient : IAuthorityConsoleClient
{
private readonly HttpClient _httpClient;
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
public AuthorityConsoleClient(HttpClient httpClient)
{
@@ -38,4 +40,73 @@ internal sealed class AuthorityConsoleClient : IAuthorityConsoleClient
return result?.Tenants ?? Array.Empty<TenantInfo>();
}
public async Task<TokenMintResponse> MintTokenAsync(TokenMintRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "console/token/mint")
{
Content = JsonContent.Create(request, options: JsonOptions)
};
if (!string.IsNullOrWhiteSpace(request.Tenant))
{
httpRequest.Headers.Add("X-StellaOps-Tenant", request.Tenant.Trim().ToLowerInvariant());
}
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadFromJsonAsync<TokenMintResponse>(JsonOptions, cancellationToken)
.ConfigureAwait(false);
return result ?? throw new InvalidOperationException("Token mint response was empty.");
}
public async Task<TokenDelegateResponse> DelegateTokenAsync(TokenDelegateRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "console/token/delegate")
{
Content = JsonContent.Create(request, options: JsonOptions)
};
if (!string.IsNullOrWhiteSpace(request.Tenant))
{
httpRequest.Headers.Add("X-StellaOps-Tenant", request.Tenant.Trim().ToLowerInvariant());
}
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadFromJsonAsync<TokenDelegateResponse>(JsonOptions, cancellationToken)
.ConfigureAwait(false);
return result ?? throw new InvalidOperationException("Token delegation response was empty.");
}
public async Task<TokenIntrospectionResponse?> IntrospectTokenAsync(string? tenant, CancellationToken cancellationToken)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "console/token/introspect");
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.Add("X-StellaOps-Tenant", tenant.Trim().ToLowerInvariant());
}
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
return null;
}
return await response.Content
.ReadFromJsonAsync<TokenIntrospectionResponse>(JsonOptions, cancellationToken)
.ConfigureAwait(false);
}
}

View File

@@ -18,10 +18,10 @@ using Microsoft.Extensions.Logging;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Services.Models.AdvisoryAi;
using StellaOps.Cli.Services.Models.Ruby;
using StellaOps.Cli.Services.Models.Transport;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Services.Models.AdvisoryAi;
using StellaOps.Cli.Services.Models.Ruby;
using StellaOps.Cli.Services.Models.Transport;
namespace StellaOps.Cli.Services;
@@ -32,12 +32,12 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
private static readonly IReadOnlyDictionary<string, object?> EmptyMetadata =
new ReadOnlyDictionary<string, object?>(new Dictionary<string, object?>(0, StringComparer.OrdinalIgnoreCase));
private const string OperatorReasonParameterName = "operator_reason";
private const string OperatorTicketParameterName = "operator_ticket";
private const string BackfillReasonParameterName = "backfill_reason";
private const string BackfillTicketParameterName = "backfill_ticket";
private const string AdvisoryScopesHeader = "X-StellaOps-Scopes";
private const string AdvisoryRunScope = "advisory:run";
private const string OperatorReasonParameterName = "operator_reason";
private const string OperatorTicketParameterName = "operator_ticket";
private const string BackfillReasonParameterName = "backfill_reason";
private const string BackfillTicketParameterName = "backfill_ticket";
private const string AdvisoryScopesHeader = "X-StellaOps-Scopes";
private const string AdvisoryRunScope = "advisory:run";
private readonly HttpClient _httpClient;
private readonly StellaOpsCliOptions _options;
@@ -859,9 +859,9 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
return MapPolicyFindingExplain(document);
}
public async Task<EntryTraceResponseModel?> GetEntryTraceAsync(string scanId, CancellationToken cancellationToken)
{
EnsureBackendConfigured();
public async Task<EntryTraceResponseModel?> GetEntryTraceAsync(string scanId, CancellationToken cancellationToken)
{
EnsureBackendConfigured();
if (string.IsNullOrWhiteSpace(scanId))
{
@@ -883,174 +883,174 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
throw new InvalidOperationException(failure);
}
var result = await response.Content.ReadFromJsonAsync<EntryTraceResponseModel>(SerializerOptions, cancellationToken).ConfigureAwait(false);
if (result is null)
{
throw new InvalidOperationException("EntryTrace response payload was empty.");
}
return result;
}
public async Task<RubyPackageInventoryModel?> GetRubyPackagesAsync(string scanId, CancellationToken cancellationToken)
{
EnsureBackendConfigured();
if (string.IsNullOrWhiteSpace(scanId))
{
throw new ArgumentException("Scan identifier is required.", nameof(scanId));
}
var encodedScanId = Uri.EscapeDataString(scanId);
using var request = CreateRequest(HttpMethod.Get, $"api/scans/{encodedScanId}/ruby-packages");
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
if (!response.IsSuccessStatusCode)
{
var failure = await CreateFailureMessageAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException(failure);
}
var inventory = await response.Content
.ReadFromJsonAsync<RubyPackageInventoryModel>(SerializerOptions, cancellationToken)
.ConfigureAwait(false);
if (inventory is null)
{
throw new InvalidOperationException("Ruby package response payload was empty.");
}
var normalizedScanId = string.IsNullOrWhiteSpace(inventory.ScanId) ? scanId : inventory.ScanId;
var normalizedDigest = inventory.ImageDigest ?? string.Empty;
var packages = inventory.Packages ?? Array.Empty<RubyPackageArtifactModel>();
return inventory with
{
ScanId = normalizedScanId,
ImageDigest = normalizedDigest,
Packages = packages
};
}
public async Task<AdvisoryPipelinePlanResponseModel> CreateAdvisoryPipelinePlanAsync(
AdvisoryAiTaskType taskType,
AdvisoryPipelinePlanRequestModel request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var taskSegment = taskType.ToString().ToLowerInvariant();
var relative = $"v1/advisory-ai/pipeline/{taskSegment}";
var payload = new AdvisoryPipelinePlanRequestModel
{
TaskType = taskType,
AdvisoryKey = string.IsNullOrWhiteSpace(request.AdvisoryKey) ? string.Empty : request.AdvisoryKey.Trim(),
ArtifactId = string.IsNullOrWhiteSpace(request.ArtifactId) ? null : request.ArtifactId!.Trim(),
ArtifactPurl = string.IsNullOrWhiteSpace(request.ArtifactPurl) ? null : request.ArtifactPurl!.Trim(),
PolicyVersion = string.IsNullOrWhiteSpace(request.PolicyVersion) ? null : request.PolicyVersion!.Trim(),
Profile = string.IsNullOrWhiteSpace(request.Profile) ? "default" : request.Profile!.Trim(),
PreferredSections = request.PreferredSections is null
? null
: request.PreferredSections
.Where(static section => !string.IsNullOrWhiteSpace(section))
.Select(static section => section.Trim())
.ToArray(),
ForceRefresh = request.ForceRefresh
};
using var httpRequest = CreateRequest(HttpMethod.Post, relative);
ApplyAdvisoryAiEndpoint(httpRequest, taskType);
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
httpRequest.Content = JsonContent.Create(payload, options: SerializerOptions);
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var failure = await CreateFailureMessageAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException(failure);
}
try
{
var plan = await response.Content.ReadFromJsonAsync<AdvisoryPipelinePlanResponseModel>(SerializerOptions, cancellationToken).ConfigureAwait(false);
if (plan is null)
{
throw new InvalidOperationException("Advisory AI plan response was empty.");
}
return plan;
}
catch (JsonException ex)
{
var raw = response.Content is null
? string.Empty
: await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Failed to parse advisory plan response. {ex.Message}", ex)
{
Data = { ["payload"] = raw }
};
}
}
public async Task<AdvisoryPipelineOutputModel?> TryGetAdvisoryPipelineOutputAsync(
string cacheKey,
AdvisoryAiTaskType taskType,
string profile,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(cacheKey))
{
throw new ArgumentException("Cache key is required.", nameof(cacheKey));
}
var encodedKey = Uri.EscapeDataString(cacheKey);
var taskSegment = Uri.EscapeDataString(taskType.ToString().ToLowerInvariant());
var resolvedProfile = string.IsNullOrWhiteSpace(profile) ? "default" : profile.Trim();
var relative = $"v1/advisory-ai/outputs/{encodedKey}?taskType={taskSegment}&profile={Uri.EscapeDataString(resolvedProfile)}";
using var request = CreateRequest(HttpMethod.Get, relative);
ApplyAdvisoryAiEndpoint(request, taskType);
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
if (!response.IsSuccessStatusCode)
{
var failure = await CreateFailureMessageAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException(failure);
}
try
{
return await response.Content.ReadFromJsonAsync<AdvisoryPipelineOutputModel>(SerializerOptions, cancellationToken).ConfigureAwait(false);
}
catch (JsonException ex)
{
var raw = response.Content is null
? string.Empty
: await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Failed to parse advisory output response. {ex.Message}", ex)
{
Data = { ["payload"] = raw }
};
}
}
public async Task<IReadOnlyList<ExcititorProviderSummary>> GetExcititorProvidersAsync(bool includeDisabled, CancellationToken cancellationToken)
{
EnsureBackendConfigured();
var result = await response.Content.ReadFromJsonAsync<EntryTraceResponseModel>(SerializerOptions, cancellationToken).ConfigureAwait(false);
if (result is null)
{
throw new InvalidOperationException("EntryTrace response payload was empty.");
}
return result;
}
public async Task<RubyPackageInventoryModel?> GetRubyPackagesAsync(string scanId, CancellationToken cancellationToken)
{
EnsureBackendConfigured();
if (string.IsNullOrWhiteSpace(scanId))
{
throw new ArgumentException("Scan identifier is required.", nameof(scanId));
}
var encodedScanId = Uri.EscapeDataString(scanId);
using var request = CreateRequest(HttpMethod.Get, $"api/scans/{encodedScanId}/ruby-packages");
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
if (!response.IsSuccessStatusCode)
{
var failure = await CreateFailureMessageAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException(failure);
}
var inventory = await response.Content
.ReadFromJsonAsync<RubyPackageInventoryModel>(SerializerOptions, cancellationToken)
.ConfigureAwait(false);
if (inventory is null)
{
throw new InvalidOperationException("Ruby package response payload was empty.");
}
var normalizedScanId = string.IsNullOrWhiteSpace(inventory.ScanId) ? scanId : inventory.ScanId;
var normalizedDigest = inventory.ImageDigest ?? string.Empty;
var packages = inventory.Packages ?? Array.Empty<RubyPackageArtifactModel>();
return inventory with
{
ScanId = normalizedScanId,
ImageDigest = normalizedDigest,
Packages = packages
};
}
public async Task<AdvisoryPipelinePlanResponseModel> CreateAdvisoryPipelinePlanAsync(
AdvisoryAiTaskType taskType,
AdvisoryPipelinePlanRequestModel request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var taskSegment = taskType.ToString().ToLowerInvariant();
var relative = $"v1/advisory-ai/pipeline/{taskSegment}";
var payload = new AdvisoryPipelinePlanRequestModel
{
TaskType = taskType,
AdvisoryKey = string.IsNullOrWhiteSpace(request.AdvisoryKey) ? string.Empty : request.AdvisoryKey.Trim(),
ArtifactId = string.IsNullOrWhiteSpace(request.ArtifactId) ? null : request.ArtifactId!.Trim(),
ArtifactPurl = string.IsNullOrWhiteSpace(request.ArtifactPurl) ? null : request.ArtifactPurl!.Trim(),
PolicyVersion = string.IsNullOrWhiteSpace(request.PolicyVersion) ? null : request.PolicyVersion!.Trim(),
Profile = string.IsNullOrWhiteSpace(request.Profile) ? "default" : request.Profile!.Trim(),
PreferredSections = request.PreferredSections is null
? null
: request.PreferredSections
.Where(static section => !string.IsNullOrWhiteSpace(section))
.Select(static section => section.Trim())
.ToArray(),
ForceRefresh = request.ForceRefresh
};
using var httpRequest = CreateRequest(HttpMethod.Post, relative);
ApplyAdvisoryAiEndpoint(httpRequest, taskType);
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
httpRequest.Content = JsonContent.Create(payload, options: SerializerOptions);
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var failure = await CreateFailureMessageAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException(failure);
}
try
{
var plan = await response.Content.ReadFromJsonAsync<AdvisoryPipelinePlanResponseModel>(SerializerOptions, cancellationToken).ConfigureAwait(false);
if (plan is null)
{
throw new InvalidOperationException("Advisory AI plan response was empty.");
}
return plan;
}
catch (JsonException ex)
{
var raw = response.Content is null
? string.Empty
: await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Failed to parse advisory plan response. {ex.Message}", ex)
{
Data = { ["payload"] = raw }
};
}
}
public async Task<AdvisoryPipelineOutputModel?> TryGetAdvisoryPipelineOutputAsync(
string cacheKey,
AdvisoryAiTaskType taskType,
string profile,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(cacheKey))
{
throw new ArgumentException("Cache key is required.", nameof(cacheKey));
}
var encodedKey = Uri.EscapeDataString(cacheKey);
var taskSegment = Uri.EscapeDataString(taskType.ToString().ToLowerInvariant());
var resolvedProfile = string.IsNullOrWhiteSpace(profile) ? "default" : profile.Trim();
var relative = $"v1/advisory-ai/outputs/{encodedKey}?taskType={taskSegment}&profile={Uri.EscapeDataString(resolvedProfile)}";
using var request = CreateRequest(HttpMethod.Get, relative);
ApplyAdvisoryAiEndpoint(request, taskType);
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
if (!response.IsSuccessStatusCode)
{
var failure = await CreateFailureMessageAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException(failure);
}
try
{
return await response.Content.ReadFromJsonAsync<AdvisoryPipelineOutputModel>(SerializerOptions, cancellationToken).ConfigureAwait(false);
}
catch (JsonException ex)
{
var raw = response.Content is null
? string.Empty
: await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Failed to parse advisory output response. {ex.Message}", ex)
{
Data = { ["payload"] = raw }
};
}
}
public async Task<IReadOnlyList<ExcititorProviderSummary>> GetExcititorProvidersAsync(bool includeDisabled, CancellationToken cancellationToken)
{
EnsureBackendConfigured();
var query = includeDisabled ? "?includeDisabled=true" : string.Empty;
using var request = CreateRequest(HttpMethod.Get, $"excititor/providers{query}");
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
@@ -1937,44 +1937,44 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
private void ApplyAdvisoryAiEndpoint(HttpRequestMessage request, AdvisoryAiTaskType taskType)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
var requestUri = request.RequestUri ?? throw new InvalidOperationException("Request URI was not initialized.");
if (!string.IsNullOrWhiteSpace(_options.AdvisoryAiUrl) &&
Uri.TryCreate(_options.AdvisoryAiUrl, UriKind.Absolute, out var advisoryBase))
{
if (!requestUri.IsAbsoluteUri)
{
request.RequestUri = new Uri(advisoryBase, requestUri.ToString());
}
}
else if (!string.IsNullOrWhiteSpace(_options.AdvisoryAiUrl))
{
throw new InvalidOperationException($"Advisory AI URL '{_options.AdvisoryAiUrl}' is not a valid absolute URI.");
}
else
{
EnsureBackendConfigured();
}
var taskScope = $"advisory:{taskType.ToString().ToLowerInvariant()}";
var combined = $"{AdvisoryRunScope} {taskScope}";
if (request.Headers.Contains(AdvisoryScopesHeader))
{
request.Headers.Remove(AdvisoryScopesHeader);
}
request.Headers.TryAddWithoutValidation(AdvisoryScopesHeader, combined);
}
private HttpRequestMessage CreateRequest(HttpMethod method, string relativeUri)
private void ApplyAdvisoryAiEndpoint(HttpRequestMessage request, AdvisoryAiTaskType taskType)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
var requestUri = request.RequestUri ?? throw new InvalidOperationException("Request URI was not initialized.");
if (!string.IsNullOrWhiteSpace(_options.AdvisoryAiUrl) &&
Uri.TryCreate(_options.AdvisoryAiUrl, UriKind.Absolute, out var advisoryBase))
{
if (!requestUri.IsAbsoluteUri)
{
request.RequestUri = new Uri(advisoryBase, requestUri.ToString());
}
}
else if (!string.IsNullOrWhiteSpace(_options.AdvisoryAiUrl))
{
throw new InvalidOperationException($"Advisory AI URL '{_options.AdvisoryAiUrl}' is not a valid absolute URI.");
}
else
{
EnsureBackendConfigured();
}
var taskScope = $"advisory:{taskType.ToString().ToLowerInvariant()}";
var combined = $"{AdvisoryRunScope} {taskScope}";
if (request.Headers.Contains(AdvisoryScopesHeader))
{
request.Headers.Remove(AdvisoryScopesHeader);
}
request.Headers.TryAddWithoutValidation(AdvisoryScopesHeader, combined);
}
private HttpRequestMessage CreateRequest(HttpMethod method, string relativeUri)
{
if (!Uri.TryCreate(relativeUri, UriKind.RelativeOrAbsolute, out var requestUri))
{
@@ -2857,4 +2857,469 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
var fallbackSeconds = Math.Min(60, Math.Pow(2, attempt));
return TimeSpan.FromSeconds(fallbackSeconds);
}
// CLI-VEX-30-001: VEX consensus list
public async Task<VexConsensusListResponse> ListVexConsensusAsync(VexConsensusListRequest request, string? tenant, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
EnsureBackendConfigured();
var queryParams = new List<string>();
if (!string.IsNullOrWhiteSpace(request.VulnerabilityId))
queryParams.Add($"vulnerabilityId={Uri.EscapeDataString(request.VulnerabilityId)}");
if (!string.IsNullOrWhiteSpace(request.ProductKey))
queryParams.Add($"productKey={Uri.EscapeDataString(request.ProductKey)}");
if (!string.IsNullOrWhiteSpace(request.Purl))
queryParams.Add($"purl={Uri.EscapeDataString(request.Purl)}");
if (!string.IsNullOrWhiteSpace(request.Status))
queryParams.Add($"status={Uri.EscapeDataString(request.Status)}");
if (!string.IsNullOrWhiteSpace(request.PolicyVersion))
queryParams.Add($"policyVersion={Uri.EscapeDataString(request.PolicyVersion)}");
if (request.Limit.HasValue)
queryParams.Add($"limit={request.Limit.Value}");
if (request.Offset.HasValue)
queryParams.Add($"offset={request.Offset.Value}");
var queryString = queryParams.Count > 0 ? "?" + string.Join("&", queryParams) : string.Empty;
var relative = $"api/vex/consensus{queryString}";
using var httpRequest = CreateRequest(HttpMethod.Get, relative);
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Trim());
}
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"VEX consensus list failed: {message}");
}
VexConsensusListResponse? result;
try
{
result = await response.Content.ReadFromJsonAsync<VexConsensusListResponse>(SerializerOptions, cancellationToken).ConfigureAwait(false);
}
catch (JsonException ex)
{
var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Failed to parse VEX consensus list response: {ex.Message}", ex)
{
Data = { ["payload"] = raw }
};
}
if (result is null)
{
throw new InvalidOperationException("VEX consensus list response was empty.");
}
return result;
}
// CLI-VEX-30-002: VEX consensus detail
public async Task<VexConsensusDetailResponse?> GetVexConsensusAsync(string vulnerabilityId, string productKey, string? tenant, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(vulnerabilityId))
{
throw new ArgumentException("Vulnerability ID must be provided.", nameof(vulnerabilityId));
}
if (string.IsNullOrWhiteSpace(productKey))
{
throw new ArgumentException("Product key must be provided.", nameof(productKey));
}
EnsureBackendConfigured();
var encodedVulnId = Uri.EscapeDataString(vulnerabilityId.Trim());
var encodedProductKey = Uri.EscapeDataString(productKey.Trim());
var relative = $"api/vex/consensus/{encodedVulnId}/{encodedProductKey}";
using var httpRequest = CreateRequest(HttpMethod.Get, relative);
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Trim());
}
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
if (!response.IsSuccessStatusCode)
{
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"VEX consensus get failed: {message}");
}
VexConsensusDetailResponse? result;
try
{
result = await response.Content.ReadFromJsonAsync<VexConsensusDetailResponse>(SerializerOptions, cancellationToken).ConfigureAwait(false);
}
catch (JsonException ex)
{
var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Failed to parse VEX consensus detail response: {ex.Message}", ex)
{
Data = { ["payload"] = raw }
};
}
return result;
}
// CLI-VEX-30-003: VEX simulation
public async Task<VexSimulationResponse> SimulateVexConsensusAsync(VexSimulationRequest request, string? tenant, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
EnsureBackendConfigured();
var relative = "api/vex/consensus/simulate";
using var httpRequest = CreateRequest(HttpMethod.Post, relative);
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Trim());
}
var jsonContent = JsonSerializer.Serialize(request, SerializerOptions);
httpRequest.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"VEX consensus simulation failed: {message}");
}
VexSimulationResponse? result;
try
{
result = await response.Content.ReadFromJsonAsync<VexSimulationResponse>(SerializerOptions, cancellationToken).ConfigureAwait(false);
}
catch (JsonException ex)
{
var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Failed to parse VEX simulation response: {ex.Message}", ex)
{
Data = { ["payload"] = raw }
};
}
if (result is null)
{
throw new InvalidOperationException("VEX simulation response was empty.");
}
return result;
}
// CLI-VEX-30-004: VEX export
public async Task<VexExportResponse> ExportVexConsensusAsync(VexExportRequest request, string? tenant, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
EnsureBackendConfigured();
var relative = "api/vex/consensus/export";
using var httpRequest = CreateRequest(HttpMethod.Post, relative);
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Trim());
}
var jsonContent = JsonSerializer.Serialize(request, SerializerOptions);
httpRequest.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"VEX consensus export failed: {message}");
}
VexExportResponse? result;
try
{
result = await response.Content.ReadFromJsonAsync<VexExportResponse>(SerializerOptions, cancellationToken).ConfigureAwait(false);
}
catch (JsonException ex)
{
var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Failed to parse VEX export response: {ex.Message}", ex)
{
Data = { ["payload"] = raw }
};
}
if (result is null)
{
throw new InvalidOperationException("VEX export response was empty.");
}
return result;
}
public async Task<Stream> DownloadVexExportAsync(string exportId, string? tenant, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(exportId))
{
throw new ArgumentException("Export ID must be provided.", nameof(exportId));
}
EnsureBackendConfigured();
var encodedExportId = Uri.EscapeDataString(exportId.Trim());
var relative = $"api/vex/consensus/export/{encodedExportId}/download";
using var httpRequest = CreateRequest(HttpMethod.Get, relative);
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Trim());
}
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"VEX export download failed: {message}");
}
return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
}
// CLI-VULN-29-001: Vulnerability explorer list
public async Task<VulnListResponse> ListVulnerabilitiesAsync(VulnListRequest request, string? tenant, CancellationToken cancellationToken)
{
EnsureBackendConfigured();
var queryParams = new List<string>();
if (!string.IsNullOrWhiteSpace(request.VulnerabilityId))
queryParams.Add($"vulnerabilityId={Uri.EscapeDataString(request.VulnerabilityId)}");
if (!string.IsNullOrWhiteSpace(request.Severity))
queryParams.Add($"severity={Uri.EscapeDataString(request.Severity)}");
if (!string.IsNullOrWhiteSpace(request.Status))
queryParams.Add($"status={Uri.EscapeDataString(request.Status)}");
if (!string.IsNullOrWhiteSpace(request.Purl))
queryParams.Add($"purl={Uri.EscapeDataString(request.Purl)}");
if (!string.IsNullOrWhiteSpace(request.Cpe))
queryParams.Add($"cpe={Uri.EscapeDataString(request.Cpe)}");
if (!string.IsNullOrWhiteSpace(request.SbomId))
queryParams.Add($"sbomId={Uri.EscapeDataString(request.SbomId)}");
if (!string.IsNullOrWhiteSpace(request.PolicyId))
queryParams.Add($"policyId={Uri.EscapeDataString(request.PolicyId)}");
if (request.PolicyVersion.HasValue)
queryParams.Add($"policyVersion={request.PolicyVersion.Value}");
if (!string.IsNullOrWhiteSpace(request.GroupBy))
queryParams.Add($"groupBy={Uri.EscapeDataString(request.GroupBy)}");
if (request.Limit.HasValue)
queryParams.Add($"limit={request.Limit.Value}");
if (request.Offset.HasValue)
queryParams.Add($"offset={request.Offset.Value}");
if (!string.IsNullOrWhiteSpace(request.Cursor))
queryParams.Add($"cursor={Uri.EscapeDataString(request.Cursor)}");
var relative = "api/vuln";
if (queryParams.Count > 0)
relative += "?" + string.Join("&", queryParams);
using var httpRequest = CreateRequest(HttpMethod.Get, relative);
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Trim());
}
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Failed to list vulnerabilities: {message}");
}
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var result = JsonSerializer.Deserialize<VulnListResponse>(json, SerializerOptions);
return result ?? new VulnListResponse(Array.Empty<VulnItem>(), 0, 0, 0, false);
}
// CLI-VULN-29-002: Vulnerability detail
public async Task<VulnDetailResponse?> GetVulnerabilityAsync(string vulnerabilityId, string? tenant, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(vulnerabilityId))
{
throw new ArgumentException("Vulnerability ID must be provided.", nameof(vulnerabilityId));
}
EnsureBackendConfigured();
var encodedVulnId = Uri.EscapeDataString(vulnerabilityId.Trim());
var relative = $"api/vuln/{encodedVulnId}";
using var httpRequest = CreateRequest(HttpMethod.Get, relative);
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Trim());
}
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
if (!response.IsSuccessStatusCode)
{
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Failed to get vulnerability details: {message}");
}
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
return JsonSerializer.Deserialize<VulnDetailResponse>(json, SerializerOptions);
}
// CLI-VULN-29-003: Vulnerability workflow operations
public async Task<VulnWorkflowResponse> ExecuteVulnWorkflowAsync(VulnWorkflowRequest request, string? tenant, CancellationToken cancellationToken)
{
EnsureBackendConfigured();
var relative = "api/vuln/workflow";
var jsonPayload = JsonSerializer.Serialize(request, SerializerOptions);
using var httpRequest = CreateRequest(HttpMethod.Post, relative);
httpRequest.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Trim());
}
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Workflow operation failed: {message}");
}
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var result = JsonSerializer.Deserialize<VulnWorkflowResponse>(json, SerializerOptions);
return result ?? new VulnWorkflowResponse(false, request.Action, 0);
}
// CLI-VULN-29-004: Vulnerability simulation
public async Task<VulnSimulationResponse> SimulateVulnerabilitiesAsync(VulnSimulationRequest request, string? tenant, CancellationToken cancellationToken)
{
EnsureBackendConfigured();
var relative = "api/vuln/simulate";
var jsonPayload = JsonSerializer.Serialize(request, SerializerOptions);
using var httpRequest = CreateRequest(HttpMethod.Post, relative);
httpRequest.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Trim());
}
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Vulnerability simulation failed: {message}");
}
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var result = JsonSerializer.Deserialize<VulnSimulationResponse>(json, SerializerOptions);
return result ?? new VulnSimulationResponse(Array.Empty<VulnSimulationDelta>(), new VulnSimulationSummary(0, 0, 0, 0, 0));
}
// CLI-VULN-29-005: Vulnerability export
public async Task<VulnExportResponse> ExportVulnerabilitiesAsync(VulnExportRequest request, string? tenant, CancellationToken cancellationToken)
{
EnsureBackendConfigured();
var relative = "api/vuln/export";
var jsonPayload = JsonSerializer.Serialize(request, SerializerOptions);
using var httpRequest = CreateRequest(HttpMethod.Post, relative);
httpRequest.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Trim());
}
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Vulnerability export failed: {message}");
}
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var result = JsonSerializer.Deserialize<VulnExportResponse>(json, SerializerOptions);
return result ?? throw new InvalidOperationException("Failed to parse export response.");
}
public async Task<Stream> DownloadVulnExportAsync(string exportId, string? tenant, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(exportId))
{
throw new ArgumentException("Export ID must be provided.", nameof(exportId));
}
EnsureBackendConfigured();
var encodedExportId = Uri.EscapeDataString(exportId.Trim());
var relative = $"api/vuln/export/{encodedExportId}/download";
using var httpRequest = CreateRequest(HttpMethod.Get, relative);
if (!string.IsNullOrWhiteSpace(tenant))
{
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Trim());
}
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException($"Vulnerability export download failed: {message}");
}
return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -6,7 +6,7 @@ using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
/// <summary>
/// Client for Authority console endpoints (CLI-TEN-47-001).
/// Client for Authority console endpoints (CLI-TEN-47-001, CLI-TEN-49-001).
/// </summary>
internal interface IAuthorityConsoleClient
{
@@ -14,4 +14,19 @@ internal interface IAuthorityConsoleClient
/// Lists available tenants for the authenticated principal.
/// </summary>
Task<IReadOnlyList<TenantInfo>> ListTenantsAsync(string tenant, CancellationToken cancellationToken);
/// <summary>
/// Mints a service account token (CLI-TEN-49-001).
/// </summary>
Task<TokenMintResponse> MintTokenAsync(TokenMintRequest request, CancellationToken cancellationToken);
/// <summary>
/// Delegates a token to another principal (CLI-TEN-49-001).
/// </summary>
Task<TokenDelegateResponse> DelegateTokenAsync(TokenDelegateRequest request, CancellationToken cancellationToken);
/// <summary>
/// Introspects the current token for impersonation/delegation info (CLI-TEN-49-001).
/// </summary>
Task<TokenIntrospectionResponse?> IntrospectTokenAsync(string? tenant, CancellationToken cancellationToken);
}

View File

@@ -6,11 +6,11 @@ using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Services.Models.AdvisoryAi;
using StellaOps.Cli.Services.Models.Ruby;
namespace StellaOps.Cli.Services;
internal interface IBackendOperationsClient
{
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);
@@ -54,4 +54,33 @@ internal interface IBackendOperationsClient
Task<AdvisoryPipelinePlanResponseModel> CreateAdvisoryPipelinePlanAsync(AdvisoryAiTaskType taskType, AdvisoryPipelinePlanRequestModel request, CancellationToken cancellationToken);
Task<AdvisoryPipelineOutputModel?> TryGetAdvisoryPipelineOutputAsync(string cacheKey, AdvisoryAiTaskType taskType, string profile, CancellationToken cancellationToken);
// CLI-VEX-30-001: VEX consensus operations
Task<VexConsensusListResponse> ListVexConsensusAsync(VexConsensusListRequest request, string? tenant, CancellationToken cancellationToken);
// CLI-VEX-30-002: VEX consensus detail
Task<VexConsensusDetailResponse?> GetVexConsensusAsync(string vulnerabilityId, string productKey, string? tenant, CancellationToken cancellationToken);
// CLI-VEX-30-003: VEX simulation
Task<VexSimulationResponse> SimulateVexConsensusAsync(VexSimulationRequest request, string? tenant, CancellationToken cancellationToken);
// CLI-VEX-30-004: VEX export
Task<VexExportResponse> ExportVexConsensusAsync(VexExportRequest request, string? tenant, CancellationToken cancellationToken);
Task<Stream> DownloadVexExportAsync(string exportId, string? tenant, CancellationToken cancellationToken);
// CLI-VULN-29-001: Vulnerability explorer list
Task<VulnListResponse> ListVulnerabilitiesAsync(VulnListRequest request, string? tenant, CancellationToken cancellationToken);
// CLI-VULN-29-002: Vulnerability detail
Task<VulnDetailResponse?> GetVulnerabilityAsync(string vulnerabilityId, string? tenant, CancellationToken cancellationToken);
// CLI-VULN-29-003: Vulnerability workflow operations
Task<VulnWorkflowResponse> ExecuteVulnWorkflowAsync(VulnWorkflowRequest request, string? tenant, CancellationToken cancellationToken);
// CLI-VULN-29-004: Vulnerability simulation
Task<VulnSimulationResponse> SimulateVulnerabilitiesAsync(VulnSimulationRequest request, string? tenant, CancellationToken cancellationToken);
// CLI-VULN-29-005: Vulnerability export
Task<VulnExportResponse> ExportVulnerabilitiesAsync(VulnExportRequest request, string? tenant, CancellationToken cancellationToken);
Task<Stream> DownloadVulnExportAsync(string exportId, string? tenant, CancellationToken cancellationToken);
}

View File

@@ -35,3 +35,60 @@ internal sealed record TenantProfile
[JsonPropertyName("lastUpdated")]
public DateTimeOffset? LastUpdated { get; init; }
}
// CLI-TEN-49-001: Token minting and delegation models
/// <summary>
/// Request to mint a service account token.
/// </summary>
internal sealed record TokenMintRequest(
[property: JsonPropertyName("serviceAccountId")] string ServiceAccountId,
[property: JsonPropertyName("scopes")] IReadOnlyList<string> Scopes,
[property: JsonPropertyName("expiresInSeconds")] int? ExpiresInSeconds = null,
[property: JsonPropertyName("tenant")] string? Tenant = null,
[property: JsonPropertyName("reason")] string? Reason = null);
/// <summary>
/// Response from token minting.
/// </summary>
internal sealed record TokenMintResponse(
[property: JsonPropertyName("accessToken")] string AccessToken,
[property: JsonPropertyName("tokenType")] string TokenType,
[property: JsonPropertyName("expiresAt")] DateTimeOffset ExpiresAt,
[property: JsonPropertyName("scopes")] IReadOnlyList<string> Scopes,
[property: JsonPropertyName("tokenId")] string? TokenId = null);
/// <summary>
/// Request to delegate a token to another principal.
/// </summary>
internal sealed record TokenDelegateRequest(
[property: JsonPropertyName("delegateTo")] string DelegateTo,
[property: JsonPropertyName("scopes")] IReadOnlyList<string> Scopes,
[property: JsonPropertyName("expiresInSeconds")] int? ExpiresInSeconds = null,
[property: JsonPropertyName("tenant")] string? Tenant = null,
[property: JsonPropertyName("reason")] string? Reason = null);
/// <summary>
/// Response from token delegation.
/// </summary>
internal sealed record TokenDelegateResponse(
[property: JsonPropertyName("accessToken")] string AccessToken,
[property: JsonPropertyName("tokenType")] string TokenType,
[property: JsonPropertyName("expiresAt")] DateTimeOffset ExpiresAt,
[property: JsonPropertyName("delegationId")] string DelegationId,
[property: JsonPropertyName("originalSubject")] string OriginalSubject,
[property: JsonPropertyName("delegatedSubject")] string DelegatedSubject,
[property: JsonPropertyName("scopes")] IReadOnlyList<string> Scopes);
/// <summary>
/// Token introspection response for impersonation banner.
/// </summary>
internal sealed record TokenIntrospectionResponse(
[property: JsonPropertyName("active")] bool Active,
[property: JsonPropertyName("sub")] string? Subject = null,
[property: JsonPropertyName("clientId")] string? ClientId = null,
[property: JsonPropertyName("scope")] string? Scope = null,
[property: JsonPropertyName("exp")] long? ExpiresAt = null,
[property: JsonPropertyName("iat")] long? IssuedAt = null,
[property: JsonPropertyName("delegatedBy")] string? DelegatedBy = null,
[property: JsonPropertyName("delegationReason")] string? DelegationReason = null);

View File

@@ -0,0 +1,258 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
// CLI-VEX-30-001: VEX consensus models for CLI
/// <summary>
/// VEX consensus list request parameters.
/// </summary>
internal sealed record VexConsensusListRequest(
[property: JsonPropertyName("vulnerabilityId")] string? VulnerabilityId = null,
[property: JsonPropertyName("productKey")] string? ProductKey = null,
[property: JsonPropertyName("purl")] string? Purl = null,
[property: JsonPropertyName("status")] string? Status = null,
[property: JsonPropertyName("policyVersion")] string? PolicyVersion = null,
[property: JsonPropertyName("limit")] int? Limit = null,
[property: JsonPropertyName("offset")] int? Offset = null);
/// <summary>
/// Paginated VEX consensus list response.
/// </summary>
internal sealed record VexConsensusListResponse(
[property: JsonPropertyName("items")] IReadOnlyList<VexConsensusItem> Items,
[property: JsonPropertyName("total")] int Total,
[property: JsonPropertyName("limit")] int Limit,
[property: JsonPropertyName("offset")] int Offset,
[property: JsonPropertyName("hasMore")] bool HasMore);
/// <summary>
/// VEX consensus item from the API.
/// </summary>
internal sealed record VexConsensusItem(
[property: JsonPropertyName("vulnerabilityId")] string VulnerabilityId,
[property: JsonPropertyName("product")] VexProductInfo Product,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("calculatedAt")] DateTimeOffset CalculatedAt,
[property: JsonPropertyName("sources")] IReadOnlyList<VexConsensusSourceInfo> Sources,
[property: JsonPropertyName("conflicts")] IReadOnlyList<VexConsensusConflictInfo>? Conflicts = null,
[property: JsonPropertyName("policyVersion")] string? PolicyVersion = null,
[property: JsonPropertyName("policyDigest")] string? PolicyDigest = null,
[property: JsonPropertyName("summary")] string? Summary = null);
/// <summary>
/// VEX product information.
/// </summary>
internal sealed record VexProductInfo(
[property: JsonPropertyName("key")] string Key,
[property: JsonPropertyName("name")] string? Name = null,
[property: JsonPropertyName("version")] string? Version = null,
[property: JsonPropertyName("purl")] string? Purl = null,
[property: JsonPropertyName("cpe")] string? Cpe = null);
/// <summary>
/// VEX consensus source (accepted claim).
/// </summary>
internal sealed record VexConsensusSourceInfo(
[property: JsonPropertyName("providerId")] string ProviderId,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("documentDigest")] string? DocumentDigest = null,
[property: JsonPropertyName("weight")] double Weight = 1.0,
[property: JsonPropertyName("justification")] string? Justification = null,
[property: JsonPropertyName("detail")] string? Detail = null,
[property: JsonPropertyName("confidence")] VexConfidenceInfo? Confidence = null);
/// <summary>
/// VEX consensus conflict (rejected claim).
/// </summary>
internal sealed record VexConsensusConflictInfo(
[property: JsonPropertyName("providerId")] string ProviderId,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("documentDigest")] string? DocumentDigest = null,
[property: JsonPropertyName("justification")] string? Justification = null,
[property: JsonPropertyName("detail")] string? Detail = null,
[property: JsonPropertyName("reason")] string? Reason = null);
/// <summary>
/// VEX confidence information.
/// </summary>
internal sealed record VexConfidenceInfo(
[property: JsonPropertyName("level")] string? Level = null,
[property: JsonPropertyName("score")] double? Score = null,
[property: JsonPropertyName("method")] string? Method = null);
// CLI-VEX-30-002: VEX consensus detail models
/// <summary>
/// Detailed VEX consensus response including quorum, evidence, rationale, and signature status.
/// </summary>
internal sealed record VexConsensusDetailResponse(
[property: JsonPropertyName("vulnerabilityId")] string VulnerabilityId,
[property: JsonPropertyName("product")] VexProductInfo Product,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("calculatedAt")] DateTimeOffset CalculatedAt,
[property: JsonPropertyName("sources")] IReadOnlyList<VexConsensusSourceInfo> Sources,
[property: JsonPropertyName("conflicts")] IReadOnlyList<VexConsensusConflictInfo>? Conflicts = null,
[property: JsonPropertyName("policyVersion")] string? PolicyVersion = null,
[property: JsonPropertyName("policyDigest")] string? PolicyDigest = null,
[property: JsonPropertyName("summary")] string? Summary = null,
[property: JsonPropertyName("quorum")] VexQuorumInfo? Quorum = null,
[property: JsonPropertyName("rationale")] VexRationaleInfo? Rationale = null,
[property: JsonPropertyName("signature")] VexSignatureInfo? Signature = null,
[property: JsonPropertyName("evidence")] IReadOnlyList<VexEvidenceInfo>? Evidence = null);
/// <summary>
/// VEX quorum information showing how consensus was reached.
/// </summary>
internal sealed record VexQuorumInfo(
[property: JsonPropertyName("required")] int Required,
[property: JsonPropertyName("achieved")] int Achieved,
[property: JsonPropertyName("threshold")] double Threshold,
[property: JsonPropertyName("totalWeight")] double TotalWeight,
[property: JsonPropertyName("weightAchieved")] double WeightAchieved,
[property: JsonPropertyName("participatingProviders")] IReadOnlyList<string>? ParticipatingProviders = null);
/// <summary>
/// VEX rationale explaining the consensus decision.
/// </summary>
internal sealed record VexRationaleInfo(
[property: JsonPropertyName("text")] string? Text = null,
[property: JsonPropertyName("justifications")] IReadOnlyList<string>? Justifications = null,
[property: JsonPropertyName("policyRules")] IReadOnlyList<string>? PolicyRules = null);
/// <summary>
/// VEX signature status information.
/// </summary>
internal sealed record VexSignatureInfo(
[property: JsonPropertyName("signed")] bool Signed,
[property: JsonPropertyName("algorithm")] string? Algorithm = null,
[property: JsonPropertyName("keyId")] string? KeyId = null,
[property: JsonPropertyName("signedAt")] DateTimeOffset? SignedAt = null,
[property: JsonPropertyName("verificationStatus")] string? VerificationStatus = null,
[property: JsonPropertyName("certificateChain")] IReadOnlyList<string>? CertificateChain = null);
/// <summary>
/// VEX evidence supporting the consensus decision.
/// </summary>
internal sealed record VexEvidenceInfo(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("providerId")] string ProviderId,
[property: JsonPropertyName("documentId")] string? DocumentId = null,
[property: JsonPropertyName("documentDigest")] string? DocumentDigest = null,
[property: JsonPropertyName("timestamp")] DateTimeOffset? Timestamp = null,
[property: JsonPropertyName("content")] string? Content = null);
// CLI-VEX-30-003: VEX simulation models
/// <summary>
/// VEX simulation request with trust/threshold overrides.
/// </summary>
internal sealed record VexSimulationRequest(
[property: JsonPropertyName("vulnerabilityId")] string? VulnerabilityId = null,
[property: JsonPropertyName("productKey")] string? ProductKey = null,
[property: JsonPropertyName("purl")] string? Purl = null,
[property: JsonPropertyName("trustOverrides")] IReadOnlyDictionary<string, double>? TrustOverrides = null,
[property: JsonPropertyName("thresholdOverride")] double? ThresholdOverride = null,
[property: JsonPropertyName("quorumOverride")] int? QuorumOverride = null,
[property: JsonPropertyName("excludeProviders")] IReadOnlyList<string>? ExcludeProviders = null,
[property: JsonPropertyName("includeOnly")] IReadOnlyList<string>? IncludeOnly = null);
/// <summary>
/// VEX simulation response showing before/after comparison.
/// </summary>
internal sealed record VexSimulationResponse(
[property: JsonPropertyName("items")] IReadOnlyList<VexSimulationResultItem> Items,
[property: JsonPropertyName("parameters")] VexSimulationParameters Parameters,
[property: JsonPropertyName("summary")] VexSimulationSummary Summary);
/// <summary>
/// Individual VEX simulation result showing the delta.
/// </summary>
internal sealed record VexSimulationResultItem(
[property: JsonPropertyName("vulnerabilityId")] string VulnerabilityId,
[property: JsonPropertyName("product")] VexProductInfo Product,
[property: JsonPropertyName("before")] VexSimulationState Before,
[property: JsonPropertyName("after")] VexSimulationState After,
[property: JsonPropertyName("changed")] bool Changed,
[property: JsonPropertyName("changeType")] string? ChangeType = null);
/// <summary>
/// VEX state for simulation comparison.
/// </summary>
internal sealed record VexSimulationState(
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("quorumAchieved")] int QuorumAchieved,
[property: JsonPropertyName("weightAchieved")] double WeightAchieved,
[property: JsonPropertyName("sources")] IReadOnlyList<string>? Sources = null);
/// <summary>
/// Parameters used in the simulation.
/// </summary>
internal sealed record VexSimulationParameters(
[property: JsonPropertyName("threshold")] double Threshold,
[property: JsonPropertyName("quorum")] int Quorum,
[property: JsonPropertyName("trustWeights")] IReadOnlyDictionary<string, double>? TrustWeights = null,
[property: JsonPropertyName("excludedProviders")] IReadOnlyList<string>? ExcludedProviders = null);
/// <summary>
/// Summary of simulation results.
/// </summary>
internal sealed record VexSimulationSummary(
[property: JsonPropertyName("totalEvaluated")] int TotalEvaluated,
[property: JsonPropertyName("totalChanged")] int TotalChanged,
[property: JsonPropertyName("statusUpgrades")] int StatusUpgrades,
[property: JsonPropertyName("statusDowngrades")] int StatusDowngrades,
[property: JsonPropertyName("noChange")] int NoChange);
// CLI-VEX-30-004: VEX export models
/// <summary>
/// VEX export request parameters.
/// </summary>
internal sealed record VexExportRequest(
[property: JsonPropertyName("vulnerabilityIds")] IReadOnlyList<string>? VulnerabilityIds = null,
[property: JsonPropertyName("productKeys")] IReadOnlyList<string>? ProductKeys = null,
[property: JsonPropertyName("purls")] IReadOnlyList<string>? Purls = null,
[property: JsonPropertyName("statuses")] IReadOnlyList<string>? Statuses = null,
[property: JsonPropertyName("policyVersion")] string? PolicyVersion = null,
[property: JsonPropertyName("signed")] bool Signed = true,
[property: JsonPropertyName("format")] string Format = "ndjson");
/// <summary>
/// VEX export response with download information.
/// </summary>
internal sealed record VexExportResponse(
[property: JsonPropertyName("exportId")] string ExportId,
[property: JsonPropertyName("downloadUrl")] string? DownloadUrl = null,
[property: JsonPropertyName("format")] string Format = "ndjson",
[property: JsonPropertyName("itemCount")] int ItemCount = 0,
[property: JsonPropertyName("signed")] bool Signed = false,
[property: JsonPropertyName("signatureAlgorithm")] string? SignatureAlgorithm = null,
[property: JsonPropertyName("signatureKeyId")] string? SignatureKeyId = null,
[property: JsonPropertyName("digest")] string? Digest = null,
[property: JsonPropertyName("digestAlgorithm")] string? DigestAlgorithm = null,
[property: JsonPropertyName("expiresAt")] DateTimeOffset? ExpiresAt = null);
/// <summary>
/// VEX export signature verification request.
/// </summary>
internal sealed record VexExportVerifyRequest(
[property: JsonPropertyName("filePath")] string FilePath,
[property: JsonPropertyName("signaturePath")] string? SignaturePath = null,
[property: JsonPropertyName("expectedDigest")] string? ExpectedDigest = null,
[property: JsonPropertyName("publicKeyPath")] string? PublicKeyPath = null);
/// <summary>
/// VEX export signature verification result.
/// </summary>
internal sealed record VexExportVerifyResult(
[property: JsonPropertyName("valid")] bool Valid,
[property: JsonPropertyName("signatureStatus")] string SignatureStatus,
[property: JsonPropertyName("digestMatch")] bool? DigestMatch = null,
[property: JsonPropertyName("actualDigest")] string? ActualDigest = null,
[property: JsonPropertyName("expectedDigest")] string? ExpectedDigest = null,
[property: JsonPropertyName("keyId")] string? KeyId = null,
[property: JsonPropertyName("signedAt")] DateTimeOffset? SignedAt = null,
[property: JsonPropertyName("errors")] IReadOnlyList<string>? Errors = null);

View File

@@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
// CLI-VULN-29-001: Vulnerability Explorer models for CLI
/// <summary>
/// Vulnerability list request parameters.
/// </summary>
internal sealed record VulnListRequest(
[property: JsonPropertyName("vulnerabilityId")] string? VulnerabilityId = null,
[property: JsonPropertyName("severity")] string? Severity = null,
[property: JsonPropertyName("status")] string? Status = null,
[property: JsonPropertyName("purl")] string? Purl = null,
[property: JsonPropertyName("cpe")] string? Cpe = null,
[property: JsonPropertyName("sbomId")] string? SbomId = null,
[property: JsonPropertyName("policyId")] string? PolicyId = null,
[property: JsonPropertyName("policyVersion")] int? PolicyVersion = null,
[property: JsonPropertyName("groupBy")] string? GroupBy = null,
[property: JsonPropertyName("limit")] int? Limit = null,
[property: JsonPropertyName("offset")] int? Offset = null,
[property: JsonPropertyName("cursor")] string? Cursor = null);
/// <summary>
/// Paginated vulnerability list response.
/// </summary>
internal sealed record VulnListResponse(
[property: JsonPropertyName("items")] IReadOnlyList<VulnItem> Items,
[property: JsonPropertyName("total")] int Total,
[property: JsonPropertyName("limit")] int Limit,
[property: JsonPropertyName("offset")] int Offset,
[property: JsonPropertyName("hasMore")] bool HasMore,
[property: JsonPropertyName("nextCursor")] string? NextCursor = null,
[property: JsonPropertyName("grouping")] VulnGroupingInfo? Grouping = null);
/// <summary>
/// Individual vulnerability item from the explorer.
/// </summary>
internal sealed record VulnItem(
[property: JsonPropertyName("vulnerabilityId")] string VulnerabilityId,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("severity")] VulnSeverityInfo Severity,
[property: JsonPropertyName("affectedPackages")] IReadOnlyList<VulnAffectedPackage> AffectedPackages,
[property: JsonPropertyName("vexStatus")] string? VexStatus = null,
[property: JsonPropertyName("policyFindingId")] string? PolicyFindingId = null,
[property: JsonPropertyName("aliases")] IReadOnlyList<string>? Aliases = null,
[property: JsonPropertyName("summary")] string? Summary = null,
[property: JsonPropertyName("publishedAt")] DateTimeOffset? PublishedAt = null,
[property: JsonPropertyName("updatedAt")] DateTimeOffset? UpdatedAt = null,
[property: JsonPropertyName("assignee")] string? Assignee = null,
[property: JsonPropertyName("dueDate")] DateTimeOffset? DueDate = null,
[property: JsonPropertyName("tags")] IReadOnlyList<string>? Tags = null);
/// <summary>
/// Vulnerability severity information.
/// </summary>
internal sealed record VulnSeverityInfo(
[property: JsonPropertyName("level")] string Level,
[property: JsonPropertyName("score")] double? Score = null,
[property: JsonPropertyName("vector")] string? Vector = null,
[property: JsonPropertyName("source")] string? Source = null);
/// <summary>
/// Affected package information.
/// </summary>
internal sealed record VulnAffectedPackage(
[property: JsonPropertyName("purl")] string? Purl = null,
[property: JsonPropertyName("cpe")] string? Cpe = null,
[property: JsonPropertyName("name")] string? Name = null,
[property: JsonPropertyName("version")] string? Version = null,
[property: JsonPropertyName("fixedIn")] string? FixedIn = null,
[property: JsonPropertyName("sbomId")] string? SbomId = null,
[property: JsonPropertyName("pathCount")] int? PathCount = null);
/// <summary>
/// Grouping information for aggregated results.
/// </summary>
internal sealed record VulnGroupingInfo(
[property: JsonPropertyName("field")] string Field,
[property: JsonPropertyName("groups")] IReadOnlyList<VulnGroup> Groups);
/// <summary>
/// A group in aggregated results.
/// </summary>
internal sealed record VulnGroup(
[property: JsonPropertyName("key")] string Key,
[property: JsonPropertyName("count")] int Count,
[property: JsonPropertyName("criticalCount")] int? CriticalCount = null,
[property: JsonPropertyName("highCount")] int? HighCount = null,
[property: JsonPropertyName("mediumCount")] int? MediumCount = null,
[property: JsonPropertyName("lowCount")] int? LowCount = null);
// CLI-VULN-29-002: Vulnerability detail models
/// <summary>
/// Detailed vulnerability response including evidence, rationale, paths, and ledger.
/// </summary>
internal sealed record VulnDetailResponse(
[property: JsonPropertyName("vulnerabilityId")] string VulnerabilityId,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("severity")] VulnSeverityInfo Severity,
[property: JsonPropertyName("affectedPackages")] IReadOnlyList<VulnAffectedPackage> AffectedPackages,
[property: JsonPropertyName("vexStatus")] string? VexStatus = null,
[property: JsonPropertyName("policyFindingId")] string? PolicyFindingId = null,
[property: JsonPropertyName("aliases")] IReadOnlyList<string>? Aliases = null,
[property: JsonPropertyName("summary")] string? Summary = null,
[property: JsonPropertyName("description")] string? Description = null,
[property: JsonPropertyName("publishedAt")] DateTimeOffset? PublishedAt = null,
[property: JsonPropertyName("updatedAt")] DateTimeOffset? UpdatedAt = null,
[property: JsonPropertyName("assignee")] string? Assignee = null,
[property: JsonPropertyName("dueDate")] DateTimeOffset? DueDate = null,
[property: JsonPropertyName("tags")] IReadOnlyList<string>? Tags = null,
[property: JsonPropertyName("evidence")] IReadOnlyList<VulnEvidenceInfo>? Evidence = null,
[property: JsonPropertyName("policyRationale")] VulnPolicyRationale? PolicyRationale = null,
[property: JsonPropertyName("dependencyPaths")] IReadOnlyList<VulnDependencyPath>? DependencyPaths = null,
[property: JsonPropertyName("ledger")] IReadOnlyList<VulnLedgerEntry>? Ledger = null,
[property: JsonPropertyName("references")] IReadOnlyList<VulnReference>? References = null);
/// <summary>
/// Evidence supporting the vulnerability assessment.
/// </summary>
internal sealed record VulnEvidenceInfo(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("source")] string Source,
[property: JsonPropertyName("documentId")] string? DocumentId = null,
[property: JsonPropertyName("documentDigest")] string? DocumentDigest = null,
[property: JsonPropertyName("timestamp")] DateTimeOffset? Timestamp = null,
[property: JsonPropertyName("content")] string? Content = null);
/// <summary>
/// Policy rationale explaining the status decision.
/// </summary>
internal sealed record VulnPolicyRationale(
[property: JsonPropertyName("policyId")] string PolicyId,
[property: JsonPropertyName("policyVersion")] int PolicyVersion,
[property: JsonPropertyName("rules")] IReadOnlyList<VulnPolicyRuleResult>? Rules = null,
[property: JsonPropertyName("summary")] string? Summary = null);
/// <summary>
/// Result of a policy rule evaluation.
/// </summary>
internal sealed record VulnPolicyRuleResult(
[property: JsonPropertyName("rule")] string Rule,
[property: JsonPropertyName("result")] string Result,
[property: JsonPropertyName("weight")] double? Weight = null,
[property: JsonPropertyName("reason")] string? Reason = null);
/// <summary>
/// Dependency path showing how the vulnerable package is included.
/// </summary>
internal sealed record VulnDependencyPath(
[property: JsonPropertyName("path")] IReadOnlyList<string> Path,
[property: JsonPropertyName("sbomId")] string? SbomId = null,
[property: JsonPropertyName("depth")] int? Depth = null);
/// <summary>
/// Ledger entry tracking vulnerability workflow history.
/// </summary>
internal sealed record VulnLedgerEntry(
[property: JsonPropertyName("timestamp")] DateTimeOffset Timestamp,
[property: JsonPropertyName("action")] string Action,
[property: JsonPropertyName("actor")] string? Actor = null,
[property: JsonPropertyName("fromStatus")] string? FromStatus = null,
[property: JsonPropertyName("toStatus")] string? ToStatus = null,
[property: JsonPropertyName("comment")] string? Comment = null,
[property: JsonPropertyName("metadata")] IReadOnlyDictionary<string, string>? Metadata = null);
/// <summary>
/// Reference link for the vulnerability.
/// </summary>
internal sealed record VulnReference(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("url")] string Url,
[property: JsonPropertyName("title")] string? Title = null);
// CLI-VULN-29-003: Vulnerability workflow models
/// <summary>
/// Workflow action request for vulnerability operations.
/// </summary>
internal sealed record VulnWorkflowRequest(
[property: JsonPropertyName("action")] string Action,
[property: JsonPropertyName("vulnerabilityIds")] IReadOnlyList<string>? VulnerabilityIds = null,
[property: JsonPropertyName("filter")] VulnFilterSpec? Filter = null,
[property: JsonPropertyName("assignee")] string? Assignee = null,
[property: JsonPropertyName("comment")] string? Comment = null,
[property: JsonPropertyName("dueDate")] DateTimeOffset? DueDate = null,
[property: JsonPropertyName("justification")] string? Justification = null,
[property: JsonPropertyName("fixVersion")] string? FixVersion = null,
[property: JsonPropertyName("idempotencyKey")] string? IdempotencyKey = null);
/// <summary>
/// Filter specification for bulk workflow operations.
/// </summary>
internal sealed record VulnFilterSpec(
[property: JsonPropertyName("severity")] string? Severity = null,
[property: JsonPropertyName("status")] string? Status = null,
[property: JsonPropertyName("purl")] string? Purl = null,
[property: JsonPropertyName("sbomId")] string? SbomId = null,
[property: JsonPropertyName("policyId")] string? PolicyId = null);
/// <summary>
/// Workflow action response with affected items.
/// </summary>
internal sealed record VulnWorkflowResponse(
[property: JsonPropertyName("success")] bool Success,
[property: JsonPropertyName("action")] string Action,
[property: JsonPropertyName("affectedCount")] int AffectedCount,
[property: JsonPropertyName("affectedIds")] IReadOnlyList<string>? AffectedIds = null,
[property: JsonPropertyName("errors")] IReadOnlyList<VulnWorkflowError>? Errors = null,
[property: JsonPropertyName("idempotencyKey")] string? IdempotencyKey = null);
/// <summary>
/// Error detail for workflow operations.
/// </summary>
internal sealed record VulnWorkflowError(
[property: JsonPropertyName("vulnerabilityId")] string VulnerabilityId,
[property: JsonPropertyName("code")] string Code,
[property: JsonPropertyName("message")] string Message);
// CLI-VULN-29-004: Vulnerability simulation models
/// <summary>
/// Simulation request for policy/VEX changes.
/// </summary>
internal sealed record VulnSimulationRequest(
[property: JsonPropertyName("policyId")] string? PolicyId = null,
[property: JsonPropertyName("policyVersion")] int? PolicyVersion = null,
[property: JsonPropertyName("vexOverrides")] IReadOnlyDictionary<string, string>? VexOverrides = null,
[property: JsonPropertyName("severityThreshold")] string? SeverityThreshold = null,
[property: JsonPropertyName("sbomIds")] IReadOnlyList<string>? SbomIds = null,
[property: JsonPropertyName("outputMarkdown")] bool OutputMarkdown = false);
/// <summary>
/// Simulation response showing deltas.
/// </summary>
internal sealed record VulnSimulationResponse(
[property: JsonPropertyName("items")] IReadOnlyList<VulnSimulationDelta> Items,
[property: JsonPropertyName("summary")] VulnSimulationSummary Summary,
[property: JsonPropertyName("markdownReport")] string? MarkdownReport = null);
/// <summary>
/// Individual delta in simulation results.
/// </summary>
internal sealed record VulnSimulationDelta(
[property: JsonPropertyName("vulnerabilityId")] string VulnerabilityId,
[property: JsonPropertyName("beforeStatus")] string BeforeStatus,
[property: JsonPropertyName("afterStatus")] string AfterStatus,
[property: JsonPropertyName("changed")] bool Changed,
[property: JsonPropertyName("changeReason")] string? ChangeReason = null);
/// <summary>
/// Summary of simulation results.
/// </summary>
internal sealed record VulnSimulationSummary(
[property: JsonPropertyName("totalEvaluated")] int TotalEvaluated,
[property: JsonPropertyName("totalChanged")] int TotalChanged,
[property: JsonPropertyName("statusUpgrades")] int StatusUpgrades,
[property: JsonPropertyName("statusDowngrades")] int StatusDowngrades,
[property: JsonPropertyName("noChange")] int NoChange);
// CLI-VULN-29-005: Vulnerability export models
/// <summary>
/// Export request for vulnerability evidence bundles.
/// </summary>
internal sealed record VulnExportRequest(
[property: JsonPropertyName("vulnerabilityIds")] IReadOnlyList<string>? VulnerabilityIds = null,
[property: JsonPropertyName("sbomIds")] IReadOnlyList<string>? SbomIds = null,
[property: JsonPropertyName("policyId")] string? PolicyId = null,
[property: JsonPropertyName("format")] string Format = "ndjson",
[property: JsonPropertyName("includeEvidence")] bool IncludeEvidence = true,
[property: JsonPropertyName("includeLedger")] bool IncludeLedger = true,
[property: JsonPropertyName("signed")] bool Signed = true);
/// <summary>
/// Export response with download information.
/// </summary>
internal sealed record VulnExportResponse(
[property: JsonPropertyName("exportId")] string ExportId,
[property: JsonPropertyName("downloadUrl")] string? DownloadUrl = null,
[property: JsonPropertyName("format")] string Format = "ndjson",
[property: JsonPropertyName("itemCount")] int ItemCount = 0,
[property: JsonPropertyName("signed")] bool Signed = false,
[property: JsonPropertyName("signatureAlgorithm")] string? SignatureAlgorithm = null,
[property: JsonPropertyName("signatureKeyId")] string? SignatureKeyId = null,
[property: JsonPropertyName("digest")] string? Digest = null,
[property: JsonPropertyName("digestAlgorithm")] string? DigestAlgorithm = null,
[property: JsonPropertyName("expiresAt")] DateTimeOffset? ExpiresAt = null);

View File

@@ -27,6 +27,7 @@ internal static class CliMetrics
private static readonly Counter<long> RubyInspectCounter = Meter.CreateCounter<long>("stellaops.cli.ruby.inspect.count");
private static readonly Counter<long> RubyResolveCounter = Meter.CreateCounter<long>("stellaops.cli.ruby.resolve.count");
private static readonly Counter<long> PhpInspectCounter = Meter.CreateCounter<long>("stellaops.cli.php.inspect.count");
private static readonly Counter<long> PythonInspectCounter = Meter.CreateCounter<long>("stellaops.cli.python.inspect.count");
private static readonly Histogram<double> CommandDurationHistogram = Meter.CreateHistogram<double>("stellaops.cli.command.duration.ms");
public static void RecordScannerDownload(string channel, bool fromCache)
@@ -150,6 +151,12 @@ internal static class CliMetrics
new("outcome", string.IsNullOrWhiteSpace(outcome) ? "unknown" : outcome)
});
public static void RecordPythonInspect(string outcome)
=> PythonInspectCounter.Add(1, new KeyValuePair<string, object?>[]
{
new("outcome", string.IsNullOrWhiteSpace(outcome) ? "unknown" : outcome)
});
public static IDisposable MeasureCommandDuration(string command)
{
var start = DateTime.UtcNow;