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:
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);
|
||||
Reference in New Issue
Block a user