feat: Implement CVSS receipt management client and models
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
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
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
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
@@ -64,17 +64,18 @@ internal static class CommandFactory
|
||||
root.Add(BuildPromotionCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildDetscoreCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildObsCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildPackCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildExceptionsCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildOrchCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildSbomCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildNotifyCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildSbomerCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildRiskCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildReachabilityCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildApiCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildSdkCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildMirrorCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildPackCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildExceptionsCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildOrchCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildSbomCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildNotifyCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildSbomerCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildCvssCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildRiskCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildReachabilityCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildApiCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildSdkCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildMirrorCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildAirgapCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(SystemCommandBuilder.BuildSystemCommand(services, verboseOption, cancellationToken));
|
||||
|
||||
@@ -126,9 +127,79 @@ internal static class CommandFactory
|
||||
return CommandHandlers.HandleScannerDownloadAsync(services, channel, output, overwrite, install, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
scanner.Add(download);
|
||||
return scanner;
|
||||
}
|
||||
scanner.Add(download);
|
||||
return scanner;
|
||||
}
|
||||
|
||||
private static Command BuildCvssCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var cvss = new Command("cvss", "CVSS v4.0 receipt operations (score, show, history, export)." );
|
||||
|
||||
var score = new Command("score", "Create a CVSS v4 receipt for a vulnerability.");
|
||||
var vulnOption = new Option<string>("--vuln") { Description = "Vulnerability identifier (e.g., CVE).", IsRequired = true };
|
||||
var policyFileOption = new Option<string>("--policy-file") { Description = "Path to CvssPolicy JSON file.", IsRequired = true };
|
||||
var vectorOption = new Option<string>("--vector") { Description = "CVSS:4.0 vector string.", IsRequired = true };
|
||||
var jsonOption = new Option<bool>("--json") { Description = "Emit JSON output." };
|
||||
score.Add(vulnOption);
|
||||
score.Add(policyFileOption);
|
||||
score.Add(vectorOption);
|
||||
score.Add(jsonOption);
|
||||
score.SetAction((parseResult, _) =>
|
||||
{
|
||||
var vuln = parseResult.GetValue(vulnOption) ?? string.Empty;
|
||||
var policyPath = parseResult.GetValue(policyFileOption) ?? string.Empty;
|
||||
var vector = parseResult.GetValue(vectorOption) ?? string.Empty;
|
||||
var json = parseResult.GetValue(jsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
return CommandHandlers.HandleCvssScoreAsync(services, vuln, policyPath, vector, json, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
var show = new Command("show", "Fetch a CVSS receipt by ID.");
|
||||
var receiptArg = new Argument<string>("receipt-id") { Description = "Receipt identifier." };
|
||||
show.Add(receiptArg);
|
||||
var showJsonOption = new Option<bool>("--json") { Description = "Emit JSON output." };
|
||||
show.Add(showJsonOption);
|
||||
show.SetAction((parseResult, _) =>
|
||||
{
|
||||
var receiptId = parseResult.GetValue(receiptArg) ?? string.Empty;
|
||||
var json = parseResult.GetValue(showJsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
return CommandHandlers.HandleCvssShowAsync(services, receiptId, json, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
var history = new Command("history", "Show receipt amendment history.");
|
||||
history.Add(receiptArg);
|
||||
var historyJsonOption = new Option<bool>("--json") { Description = "Emit JSON output." };
|
||||
history.Add(historyJsonOption);
|
||||
history.SetAction((parseResult, _) =>
|
||||
{
|
||||
var receiptId = parseResult.GetValue(receiptArg) ?? string.Empty;
|
||||
var json = parseResult.GetValue(historyJsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
return CommandHandlers.HandleCvssHistoryAsync(services, receiptId, json, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
var export = new Command("export", "Export a CVSS receipt to JSON (pdf not yet supported).");
|
||||
export.Add(receiptArg);
|
||||
var formatOption = new Option<string>("--format") { Description = "json|pdf (json default)." };
|
||||
var outOption = new Option<string>("--out") { Description = "Output file path." };
|
||||
export.Add(formatOption);
|
||||
export.Add(outOption);
|
||||
export.SetAction((parseResult, _) =>
|
||||
{
|
||||
var receiptId = parseResult.GetValue(receiptArg) ?? string.Empty;
|
||||
var format = parseResult.GetValue(formatOption) ?? "json";
|
||||
var output = parseResult.GetValue(outOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
return CommandHandlers.HandleCvssExportAsync(services, receiptId, format, output, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
cvss.Add(score);
|
||||
cvss.Add(show);
|
||||
cvss.Add(history);
|
||||
cvss.Add(export);
|
||||
return cvss;
|
||||
}
|
||||
|
||||
private static Command BuildScanCommand(IServiceProvider services, StellaOpsCliOptions options, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -27,14 +27,17 @@ using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Output;
|
||||
using StellaOps.Cli.Prompts;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.Cli.Services.Models.AdvisoryAi;
|
||||
using StellaOps.Cli.Services.Models.Bun;
|
||||
using StellaOps.Cli.Services.Models.Ruby;
|
||||
using StellaOps.Cli.Telemetry;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using StellaOps.Cryptography.Kms;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.Cli.Services.Models.AdvisoryAi;
|
||||
using StellaOps.Cli.Services.Models.Bun;
|
||||
using StellaOps.Cli.Services.Models.Ruby;
|
||||
using StellaOps.Cli.Telemetry;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using StellaOps.Cryptography.Kms;
|
||||
using StellaOps.Policy.Scoring;
|
||||
using StellaOps.Policy.Scoring.Engine;
|
||||
using StellaOps.Policy.Scoring.Policies;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Node;
|
||||
@@ -67,12 +70,17 @@ internal static class CommandHandlers
|
||||
/// <summary>
|
||||
/// JSON serializer options for output (alias for JsonOptions).
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions JsonOutputOptions = JsonOptions;
|
||||
private static readonly JsonSerializerOptions JsonOutputOptions = JsonOptions;
|
||||
|
||||
private static readonly JsonSerializerOptions CompactJson = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Sets the verbosity level for logging.
|
||||
/// </summary>
|
||||
private static void SetVerbosity(IServiceProvider services, bool verbose)
|
||||
private static void SetVerbosity(IServiceProvider services, bool verbose)
|
||||
{
|
||||
// Configure logging level based on verbose flag
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
@@ -82,7 +90,215 @@ internal static class CommandHandlers
|
||||
var logger = loggerFactory.CreateLogger("StellaOps.Cli.Commands.CommandHandlers");
|
||||
logger.LogDebug("Verbose logging enabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleCvssScoreAsync(
|
||||
IServiceProvider services,
|
||||
string vulnerabilityId,
|
||||
string policyPath,
|
||||
string vector,
|
||||
bool json,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-score");
|
||||
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
|
||||
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
|
||||
|
||||
try
|
||||
{
|
||||
var policyJson = await File.ReadAllTextAsync(policyPath, cancellationToken).ConfigureAwait(false);
|
||||
var loader = new CvssPolicyLoader();
|
||||
var policyResult = loader.Load(policyJson, cancellationToken);
|
||||
if (!policyResult.IsValid || policyResult.Policy is null || string.IsNullOrWhiteSpace(policyResult.Hash))
|
||||
{
|
||||
var errors = string.Join("; ", policyResult.Errors.Select(e => $"{e.Path}: {e.Message}"));
|
||||
throw new InvalidOperationException($"Policy invalid: {errors}");
|
||||
}
|
||||
|
||||
var policy = policyResult.Policy with { Hash = policyResult.Hash };
|
||||
|
||||
var engine = scope.ServiceProvider.GetRequiredService<ICvssV4Engine>();
|
||||
var parsed = engine.ParseVector(vector);
|
||||
|
||||
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
|
||||
|
||||
var request = new CreateCvssReceipt(
|
||||
vulnerabilityId,
|
||||
policy,
|
||||
parsed.BaseMetrics,
|
||||
parsed.ThreatMetrics,
|
||||
parsed.EnvironmentalMetrics,
|
||||
parsed.SupplementalMetrics,
|
||||
Array.Empty<CvssEvidenceItem>(),
|
||||
SigningKey: null,
|
||||
CreatedBy: "cli",
|
||||
CreatedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
var receipt = await client.CreateReceiptAsync(request, cancellationToken).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException("CVSS receipt creation failed.");
|
||||
|
||||
if (json)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(receipt, CompactJson));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"✔ CVSS receipt {receipt.ReceiptId} created | Severity {receipt.Severity} | Effective {receipt.Scores.EffectiveScore:0.0}");
|
||||
Console.WriteLine($"Vector: {receipt.VectorString}");
|
||||
Console.WriteLine($"Policy: {receipt.PolicyRef.PolicyId} v{receipt.PolicyRef.Version} ({receipt.PolicyRef.Hash})");
|
||||
}
|
||||
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create CVSS receipt");
|
||||
Environment.ExitCode = 1;
|
||||
if (json)
|
||||
{
|
||||
var problem = new { error = "cvss_score_failed", message = ex.Message };
|
||||
Console.WriteLine(JsonSerializer.Serialize(problem, CompactJson));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleCvssShowAsync(
|
||||
IServiceProvider services,
|
||||
string receiptId,
|
||||
bool json,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-show");
|
||||
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
|
||||
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
|
||||
|
||||
try
|
||||
{
|
||||
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
|
||||
var receipt = await client.GetReceiptAsync(receiptId, cancellationToken).ConfigureAwait(false);
|
||||
if (receipt is null)
|
||||
{
|
||||
Environment.ExitCode = 5;
|
||||
Console.WriteLine(json
|
||||
? JsonSerializer.Serialize(new { error = "not_found", receiptId }, CompactJson)
|
||||
: $"✖ Receipt {receiptId} not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (json)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(receipt, CompactJson));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Receipt {receipt.ReceiptId} | Severity {receipt.Severity} | Effective {receipt.Scores.EffectiveScore:0.0}");
|
||||
Console.WriteLine($"Created {receipt.CreatedAt:u} by {receipt.CreatedBy}");
|
||||
Console.WriteLine($"Vector: {receipt.VectorString}");
|
||||
}
|
||||
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to fetch CVSS receipt {ReceiptId}", receiptId);
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleCvssHistoryAsync(
|
||||
IServiceProvider services,
|
||||
string receiptId,
|
||||
bool json,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-history");
|
||||
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
|
||||
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
|
||||
|
||||
try
|
||||
{
|
||||
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
|
||||
var history = await client.GetHistoryAsync(receiptId, cancellationToken).ConfigureAwait(false);
|
||||
if (json)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(history, CompactJson));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (history.Count == 0)
|
||||
{
|
||||
Console.WriteLine("(no history)");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var entry in history.OrderBy(h => h.Timestamp))
|
||||
{
|
||||
Console.WriteLine($"{entry.Timestamp:u} | {entry.Actor} | {entry.ChangeType} {entry.Field} => {entry.NewValue ?? ""} ({entry.Reason})");
|
||||
}
|
||||
}
|
||||
}
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to fetch CVSS receipt history {ReceiptId}", receiptId);
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleCvssExportAsync(
|
||||
IServiceProvider services,
|
||||
string receiptId,
|
||||
string format,
|
||||
string? output,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-export");
|
||||
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
|
||||
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
|
||||
|
||||
try
|
||||
{
|
||||
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
|
||||
var receipt = await client.GetReceiptAsync(receiptId, cancellationToken).ConfigureAwait(false);
|
||||
if (receipt is null)
|
||||
{
|
||||
Environment.ExitCode = 5;
|
||||
Console.WriteLine($"✖ Receipt {receiptId} not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Environment.ExitCode = 9;
|
||||
Console.WriteLine("Only json export is supported at this time.");
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPath = string.IsNullOrWhiteSpace(output)
|
||||
? $"cvss-receipt-{receipt.ReceiptId}.json"
|
||||
: output!;
|
||||
|
||||
var jsonPayload = JsonSerializer.Serialize(receipt, CompactJson);
|
||||
await File.WriteAllTextAsync(targetPath, jsonPayload, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Console.WriteLine($"✔ Exported receipt to {targetPath}");
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to export CVSS receipt {ReceiptId}", receiptId);
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task VerifyBundleAsync(string path, ILogger logger, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Telemetry;
|
||||
using StellaOps.AirGap.Policy;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Policy.Scoring.Engine;
|
||||
|
||||
namespace StellaOps.Cli;
|
||||
|
||||
@@ -213,8 +214,8 @@ internal static class Program
|
||||
// CLI-PARITY-41-002: Notify client for notification management
|
||||
services.AddHttpClient<INotifyClient, NotifyClient>(client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(60);
|
||||
}).AddEgressPolicyGuard("stellaops-cli", "notify-api");
|
||||
client.Timeout = TimeSpan.FromSeconds(60);
|
||||
}).AddEgressPolicyGuard("stellaops-cli", "notify-api");
|
||||
|
||||
// CLI-SBOM-60-001: Sbomer client for layer/compose operations
|
||||
services.AddHttpClient<ISbomerClient, SbomerClient>(client =>
|
||||
@@ -222,6 +223,14 @@ internal static class Program
|
||||
client.Timeout = TimeSpan.FromMinutes(5); // Composition may take longer
|
||||
}).AddEgressPolicyGuard("stellaops-cli", "sbomer-api");
|
||||
|
||||
// CLI-CVSS-190-010: CVSS receipt client (talks to Policy Gateway /api/cvss)
|
||||
services.AddHttpClient<ICvssClient, CvssClient>(client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(60);
|
||||
}).AddEgressPolicyGuard("stellaops-cli", "cvss-api");
|
||||
|
||||
services.AddSingleton<ICvssV4Engine, CvssV4Engine>();
|
||||
|
||||
// CLI-AIRGAP-56-001: Mirror bundle import service for air-gap operations
|
||||
services.AddSingleton<StellaOps.AirGap.Importer.Repositories.IBundleCatalogRepository,
|
||||
StellaOps.AirGap.Importer.Repositories.InMemoryBundleCatalogRepository>();
|
||||
|
||||
162
src/Cli/StellaOps.Cli/Services/CvssClient.cs
Normal file
162
src/Cli/StellaOps.Cli/Services/CvssClient.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Auth.Client;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Policy.Scoring;
|
||||
|
||||
namespace StellaOps.Cli.Services;
|
||||
|
||||
internal sealed class CvssClient : ICvssClient
|
||||
{
|
||||
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<CvssClient> logger;
|
||||
private readonly IStellaOpsTokenClient? tokenClient;
|
||||
private readonly object tokenSync = new();
|
||||
private string? cachedAccessToken;
|
||||
private DateTimeOffset cachedAccessTokenExpiresAt = DateTimeOffset.MinValue;
|
||||
|
||||
public CvssClient(HttpClient httpClient, StellaOpsCliOptions options, ILogger<CvssClient> 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.BackendUrl) && httpClient.BaseAddress is null)
|
||||
{
|
||||
if (Uri.TryCreate(options.BackendUrl, UriKind.Absolute, out var baseUri))
|
||||
{
|
||||
httpClient.BaseAddress = baseUri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CvssScoreReceipt?> CreateReceiptAsync(CreateCvssReceipt request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
EnsureConfigured();
|
||||
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/cvss/receipts")
|
||||
{
|
||||
Content = JsonContent.Create(request, options: SerializerOptions)
|
||||
};
|
||||
await AuthorizeRequestAsync(httpRequest, StellaOps.Auth.Abstractions.StellaOpsScopes.PolicyRun, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
return await ReadResponseAsync<CvssScoreReceipt>(response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<CvssScoreReceipt?> GetReceiptAsync(string receiptId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(receiptId);
|
||||
EnsureConfigured();
|
||||
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Get, $"/api/cvss/receipts/{Uri.EscapeDataString(receiptId)}");
|
||||
await AuthorizeRequestAsync(httpRequest, StellaOps.Auth.Abstractions.StellaOpsScopes.FindingsRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
return await ReadResponseAsync<CvssScoreReceipt>(response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ReceiptHistoryEntry>> GetHistoryAsync(string receiptId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(receiptId);
|
||||
EnsureConfigured();
|
||||
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Get, $"/api/cvss/receipts/{Uri.EscapeDataString(receiptId)}/history");
|
||||
await AuthorizeRequestAsync(httpRequest, StellaOps.Auth.Abstractions.StellaOpsScopes.FindingsRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
return await ReadResponseAsync<IReadOnlyList<ReceiptHistoryEntry>>(response, cancellationToken).ConfigureAwait(false)
|
||||
?? Array.Empty<ReceiptHistoryEntry>();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CvssPolicy>> ListPoliciesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureConfigured();
|
||||
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Get, "/api/cvss/policies");
|
||||
await AuthorizeRequestAsync(httpRequest, StellaOps.Auth.Abstractions.StellaOpsScopes.PolicyRun, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
return await ReadResponseAsync<IReadOnlyList<CvssPolicy>>(response, cancellationToken).ConfigureAwait(false)
|
||||
?? Array.Empty<CvssPolicy>();
|
||||
}
|
||||
|
||||
private async Task<T?> ReadResponseAsync<T>(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||
{
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
logger.LogWarning("CVSS request failed with {Status}: {Body}", (int)response.StatusCode, string.IsNullOrWhiteSpace(body) ? "<empty>" : body);
|
||||
return default;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<T>(SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void EnsureConfigured()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.BackendUrl) && httpClient.BaseAddress is null)
|
||||
{
|
||||
throw new InvalidOperationException("Backend URL not configured. Set STELLAOPS_BACKEND_URL or --backend-url.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AuthorizeRequestAsync(HttpRequestMessage request, string scope, CancellationToken cancellationToken)
|
||||
{
|
||||
var token = await GetAccessTokenAsync(scope, cancellationToken).ConfigureAwait(false);
|
||||
if (!string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string?> GetAccessTokenAsync(string scope, CancellationToken cancellationToken)
|
||||
{
|
||||
if (tokenClient is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
lock (tokenSync)
|
||||
{
|
||||
if (cachedAccessToken is not null && DateTimeOffset.UtcNow < cachedAccessTokenExpiresAt - TokenRefreshSkew)
|
||||
{
|
||||
return cachedAccessToken;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await tokenClient.GetAccessTokenAsync(scope, cancellationToken).ConfigureAwait(false);
|
||||
lock (tokenSync)
|
||||
{
|
||||
cachedAccessToken = result.AccessToken;
|
||||
cachedAccessTokenExpiresAt = result.ExpiresAt;
|
||||
}
|
||||
return result.AccessToken;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Token acquisition failed for scope {Scope}", scope);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Cli/StellaOps.Cli/Services/ICvssClient.cs
Normal file
17
src/Cli/StellaOps.Cli/Services/ICvssClient.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Policy.Scoring;
|
||||
|
||||
namespace StellaOps.Cli.Services;
|
||||
|
||||
internal interface ICvssClient
|
||||
{
|
||||
Task<CvssScoreReceipt?> CreateReceiptAsync(CreateCvssReceipt request, CancellationToken cancellationToken);
|
||||
|
||||
Task<CvssScoreReceipt?> GetReceiptAsync(string receiptId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<ReceiptHistoryEntry>> GetHistoryAsync(string receiptId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<CvssPolicy>> ListPoliciesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
19
src/Cli/StellaOps.Cli/Services/Models/CvssModels.cs
Normal file
19
src/Cli/StellaOps.Cli/Services/Models/CvssModels.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Policy.Scoring;
|
||||
|
||||
namespace StellaOps.Cli.Services.Models;
|
||||
|
||||
internal sealed record CreateCvssReceipt(
|
||||
[property: JsonPropertyName("vulnerabilityId")] string VulnerabilityId,
|
||||
[property: JsonPropertyName("policy")] CvssPolicy Policy,
|
||||
[property: JsonPropertyName("baseMetrics")] CvssBaseMetrics BaseMetrics,
|
||||
[property: JsonPropertyName("threatMetrics")] CvssThreatMetrics? ThreatMetrics,
|
||||
[property: JsonPropertyName("environmentalMetrics")] CvssEnvironmentalMetrics? EnvironmentalMetrics,
|
||||
[property: JsonPropertyName("supplementalMetrics")] CvssSupplementalMetrics? SupplementalMetrics,
|
||||
[property: JsonPropertyName("evidence")] IReadOnlyList<CvssEvidenceItem> Evidence,
|
||||
[property: JsonPropertyName("signingKey")] EnvelopeKey? SigningKey,
|
||||
[property: JsonPropertyName("createdBy")] string? CreatedBy,
|
||||
[property: JsonPropertyName("createdAt")] DateTimeOffset? CreatedAt);
|
||||
@@ -69,6 +69,7 @@
|
||||
<ProjectReference Include="../../Policy/__Libraries/StellaOps.Policy.Storage.Postgres/StellaOps.Policy.Storage.Postgres.csproj" />
|
||||
<ProjectReference Include="../../Notify/__Libraries/StellaOps.Notify.Storage.Postgres/StellaOps.Notify.Storage.Postgres.csproj" />
|
||||
<ProjectReference Include="../../Excititor/__Libraries/StellaOps.Excititor.Storage.Postgres/StellaOps.Excititor.Storage.Postgres.csproj" />
|
||||
<ProjectReference Include="../../Policy/StellaOps.Policy.Scoring/StellaOps.Policy.Scoring.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(StellaOpsEnableCryptoPro)' == 'true'">
|
||||
|
||||
Reference in New Issue
Block a user