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

This commit is contained in:
StellaOps Bot
2025-12-07 01:14:28 +02:00
parent 53889d85e7
commit 69651212ec
30 changed files with 815 additions and 109 deletions

View File

@@ -87,6 +87,31 @@ public sealed class AuthorityJwksServiceTests
Assert.Contains(second.Response.Keys, key => key.Kid == "key-2");
}
[Fact]
public void Jwks_IncludesSm2_WhenProviderSupportsIt()
{
var options = CreateAuthorityOptions();
var provider = new TestCryptoProvider();
provider.AddSm2Key("sm2-key");
var registry = new TestRegistry(provider);
using var cache = new MemoryCache(new MemoryCacheOptions());
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-10-30T12:00:00Z"));
var hash = CryptoHashFactory.CreateDefault();
var service = new AuthorityJwksService(
registry,
hash,
NullLogger<AuthorityJwksService>.Instance,
cache,
clock,
Options.Create(options));
var response = service.Get();
var sm2 = response.Response.Keys.Single(key => key.Kid == "sm2-key");
Assert.Equal(SignatureAlgorithms.Sm2, sm2.Alg);
Assert.Equal("SM2", sm2.Crv);
Assert.Equal("EC", sm2.Kty);
}
private static StellaOpsAuthorityOptions CreateAuthorityOptions()
{
return new StellaOpsAuthorityOptions
@@ -189,6 +214,19 @@ public sealed class AuthorityJwksServiceTests
keys[keyId] = new TestKey(keyId, parameters);
}
public void AddSm2Key(string keyId)
{
var curve = Org.BouncyCastle.Asn1.GM.GMNamedCurves.GetByName("SM2P256V1");
var domain = new Org.BouncyCastle.Crypto.Parameters.ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H, curve.GetSeed());
var generator = new Org.BouncyCastle.Crypto.Generators.ECKeyPairGenerator("EC");
generator.Init(new Org.BouncyCastle.Crypto.Generators.ECKeyGenerationParameters(domain, new Org.BouncyCastle.Security.SecureRandom()));
var pair = generator.GenerateKeyPair();
var privateDer = Org.BouncyCastle.Asn1.Pkcs.PrivateKeyInfoFactory.CreatePrivateKeyInfo(pair.Private).GetDerEncoded();
var keyRef = new CryptoKeyReference(keyId);
var signingKey = new CryptoSigningKey(keyRef, SignatureAlgorithms.Sm2, privateDer, DateTimeOffset.UtcNow);
keys[keyId] = new TestKey(keyId, signingKey.PublicParameters);
}
private sealed class TestKey
{
public TestKey(string keyId, ECParameters parameters)

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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>();

View 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;
}
}
}

View 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);
}

View 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);

View File

@@ -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'">

View File

@@ -0,0 +1,85 @@
using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Tests.Testing;
using StellaOps.Policy.Scoring;
using StellaOps.Policy.Scoring.Engine;
using StellaOps.Policy.Scoring.Policies;
namespace StellaOps.Cli.Tests.Services;
public sealed class CvssClientTests
{
[Fact]
public async Task GetReceiptAsync_ParsesReceipt()
{
var sampleReceipt = CreateSampleReceipt();
var handler = new StubHttpMessageHandler((request, _) =>
{
Assert.Equal("/api/cvss/receipts/r1", request.RequestUri!.AbsolutePath);
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(sampleReceipt, new JsonSerializerOptions(JsonSerializerDefaults.Web))),
RequestMessage = request
};
return response;
});
var httpClient = new HttpClient(handler)
{
BaseAddress = new Uri("https://policy.example")
};
var options = new StellaOpsCliOptions { BackendUrl = "https://policy.example" };
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
var client = new CvssClient(httpClient, options, loggerFactory.CreateLogger<CvssClient>());
var result = await client.GetReceiptAsync("r1", CancellationToken.None);
Assert.NotNull(result);
Assert.Equal("r1", result!.ReceiptId);
Assert.Equal(sampleReceipt.VectorString, result.VectorString);
}
private static CvssScoreReceipt CreateSampleReceipt()
{
var engine = new CvssV4Engine();
var parsed = engine.ParseVector("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H");
var scores = engine.ComputeScores(parsed.BaseMetrics, parsed.ThreatMetrics, parsed.EnvironmentalMetrics);
return new CvssScoreReceipt
{
ReceiptId = "r1",
TenantId = "tenant-1",
VulnerabilityId = "CVE-2025-0001",
CreatedAt = DateTimeOffset.UtcNow,
CreatedBy = "cli",
ModifiedAt = null,
ModifiedBy = null,
BaseMetrics = parsed.BaseMetrics,
ThreatMetrics = parsed.ThreatMetrics,
EnvironmentalMetrics = parsed.EnvironmentalMetrics,
SupplementalMetrics = parsed.SupplementalMetrics,
Scores = scores,
VectorString = engine.BuildVectorString(parsed.BaseMetrics, parsed.ThreatMetrics, parsed.EnvironmentalMetrics, parsed.SupplementalMetrics),
Severity = engine.GetSeverity(scores.EffectiveScore),
PolicyRef = new CvssPolicyReference { PolicyId = "cvss-policy", Version = "1.0.0", Hash = "abc", ActivatedAt = DateTimeOffset.UtcNow },
InputHash = "deadbeef",
History = System.Collections.Immutable.ImmutableList<ReceiptHistoryEntry>.Empty,
Evidence = System.Collections.Immutable.ImmutableList<CvssEvidenceItem>.Empty,
AttestationRefs = System.Collections.Immutable.ImmutableList<string>.Empty,
ExportHash = null,
AmendsReceiptId = null,
SupersedesReceiptId = null,
SupersededReason = null,
IsActive = true
};
}
}

View File

@@ -1,5 +1,5 @@
using System.Collections.Concurrent;
using MongoDB.Bson;
using System.IO;
namespace StellaOps.Concelier.Connector.Common.Fetch;
@@ -8,9 +8,9 @@ namespace StellaOps.Concelier.Connector.Common.Fetch;
/// </summary>
public sealed class RawDocumentStorage
{
private readonly ConcurrentDictionary<ObjectId, byte[]> _blobs = new();
private readonly ConcurrentDictionary<Guid, byte[]> _blobs = new();
public Task<ObjectId> UploadAsync(
public Task<Guid> UploadAsync(
string sourceName,
string uri,
byte[] content,
@@ -18,19 +18,20 @@ public sealed class RawDocumentStorage
CancellationToken cancellationToken)
=> UploadAsync(sourceName, uri, content, contentType, expiresAt: null, cancellationToken);
public async Task<ObjectId> UploadAsync(
public async Task<Guid> UploadAsync(
string sourceName,
string uri,
byte[] content,
string? contentType,
DateTimeOffset? expiresAt,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
Guid? documentId = null)
{
ArgumentException.ThrowIfNullOrEmpty(sourceName);
ArgumentException.ThrowIfNullOrEmpty(uri);
ArgumentNullException.ThrowIfNull(content);
var id = ObjectId.GenerateNewId();
var id = documentId ?? Guid.NewGuid();
var copy = new byte[content.Length];
Buffer.BlockCopy(content, 0, copy, 0, content.Length);
_blobs[id] = copy;
@@ -38,17 +39,17 @@ public sealed class RawDocumentStorage
return id;
}
public Task<byte[]> DownloadAsync(ObjectId id, CancellationToken cancellationToken)
public Task<byte[]> DownloadAsync(Guid id, CancellationToken cancellationToken)
{
if (_blobs.TryGetValue(id, out var bytes))
{
return Task.FromResult(bytes);
}
throw new MongoDB.Driver.GridFSFileNotFoundException($"Blob {id} not found.");
throw new FileNotFoundException($"Blob {id} not found.");
}
public async Task DeleteAsync(ObjectId id, CancellationToken cancellationToken)
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken)
{
_blobs.TryRemove(id, out _);
await Task.CompletedTask.ConfigureAwait(false);

View File

@@ -147,31 +147,33 @@ public sealed class SourceFetchService
}
}
var gridFsId = await _rawDocumentStorage.UploadAsync(
var existing = await _documentStore.FindBySourceAndUriAsync(request.SourceName, request.RequestUri.ToString(), cancellationToken).ConfigureAwait(false);
var recordId = existing?.Id ?? Guid.NewGuid();
var payloadId = await _rawDocumentStorage.UploadAsync(
request.SourceName,
request.RequestUri.ToString(),
contentBytes,
contentType,
expiresAt,
cancellationToken).ConfigureAwait(false);
var existing = await _documentStore.FindBySourceAndUriAsync(request.SourceName, request.RequestUri.ToString(), cancellationToken).ConfigureAwait(false);
var recordId = existing?.Id ?? Guid.NewGuid();
var record = new DocumentRecord(
recordId,
request.SourceName,
cancellationToken,
recordId).ConfigureAwait(false);
var record = new DocumentRecord(
recordId,
request.SourceName,
request.RequestUri.ToString(),
fetchedAt,
contentHash,
DocumentStatuses.PendingParse,
contentType,
headers,
metadata,
response.Headers.ETag?.Tag,
response.Content.Headers.LastModified,
gridFsId,
expiresAt);
headers,
metadata,
response.Headers.ETag?.Tag,
response.Content.Headers.LastModified,
payloadId,
expiresAt,
Payload: contentBytes);
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
SourceDiagnostics.RecordHttpRequest(request.SourceName, request.ClientName, response.StatusCode, sendResult.Attempts, duration, contentBytes.LongLength, rateLimitRemaining);

View File

@@ -4,7 +4,7 @@ using System.Security.Cryptography;
using System.Linq;
using Microsoft.Extensions.Logging;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.MergeEvents;
using StellaOps.Concelier.Storage.Mongo.MergeEvents;
/// <summary>
/// Persists merge events with canonical before/after hashes for auditability.

View File

@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Storage.Mongo
@@ -12,6 +13,17 @@ namespace StellaOps.Concelier.Storage.Mongo
public const string Failed = "failed";
}
public static class MongoStorageDefaults
{
public static class Collections
{
public const string AdvisoryStatements = "advisory_statements";
public const string AdvisoryRaw = "advisory_raw";
public const string Alias = "aliases";
public const string MergeEvent = "merge_events";
}
}
public sealed record MongoStorageOptions
{
public string DefaultTenant { get; init; } = "default";
@@ -87,7 +99,7 @@ namespace StellaOps.Concelier.Storage.Mongo
Guid DocumentId,
string SourceName,
string Format,
string Payload,
MongoDB.Bson.BsonDocument Payload,
DateTimeOffset CreatedAt);
public interface IDtoStore
@@ -117,9 +129,9 @@ namespace StellaOps.Concelier.Storage.Mongo
{
private readonly ConcurrentDictionary<Guid, byte[]> _blobs = new();
public Task<Guid> UploadAsync(string sourceName, string uri, byte[] content, string? contentType, DateTimeOffset? expiresAt, CancellationToken cancellationToken)
public Task<Guid> UploadAsync(string sourceName, string uri, byte[] content, string? contentType, DateTimeOffset? expiresAt, CancellationToken cancellationToken, Guid? documentId = null)
{
var id = Guid.NewGuid();
var id = documentId ?? Guid.NewGuid();
_blobs[id] = content.ToArray();
return Task.FromResult(id);
}
@@ -143,12 +155,12 @@ namespace StellaOps.Concelier.Storage.Mongo
}
}
public sealed record SourceStateRecord(string SourceName, string? CursorJson, DateTimeOffset UpdatedAt);
public sealed record SourceStateRecord(string SourceName, MongoDB.Bson.BsonDocument? Cursor, DateTimeOffset UpdatedAt);
public interface ISourceStateRepository
{
Task<SourceStateRecord?> TryGetAsync(string sourceName, CancellationToken cancellationToken);
Task UpdateCursorAsync(string sourceName, string cursorJson, DateTimeOffset completedAt, CancellationToken cancellationToken);
Task UpdateCursorAsync(string sourceName, MongoDB.Bson.BsonDocument cursor, DateTimeOffset completedAt, CancellationToken cancellationToken);
Task MarkFailureAsync(string sourceName, DateTimeOffset now, TimeSpan backoff, string reason, CancellationToken cancellationToken);
}
@@ -162,9 +174,9 @@ namespace StellaOps.Concelier.Storage.Mongo
return Task.FromResult<SourceStateRecord?>(record);
}
public Task UpdateCursorAsync(string sourceName, string cursorJson, DateTimeOffset completedAt, CancellationToken cancellationToken)
public Task UpdateCursorAsync(string sourceName, MongoDB.Bson.BsonDocument cursor, DateTimeOffset completedAt, CancellationToken cancellationToken)
{
_states[sourceName] = new SourceStateRecord(sourceName, cursorJson, completedAt);
_states[sourceName] = new SourceStateRecord(sourceName, cursor.DeepClone(), completedAt);
return Task.CompletedTask;
}
@@ -225,7 +237,15 @@ namespace StellaOps.Concelier.Storage.Mongo.Advisories
namespace StellaOps.Concelier.Storage.Mongo.Aliases
{
public static class AliasStoreConstants
{
public const string PrimaryScheme = "PRIMARY";
public const string UnscopedScheme = "UNSCOPED";
}
public sealed record AliasEntry(string Scheme, string Value);
public sealed record AliasRecord(string AdvisoryKey, string Scheme, string Value);
public sealed record AliasCollision(string Scheme, string Value, IReadOnlyList<string> AdvisoryKeys);
public interface IAliasStore
{
@@ -387,17 +407,52 @@ namespace StellaOps.Concelier.Storage.Mongo.Exporting
namespace StellaOps.Concelier.Storage.Mongo.MergeEvents
{
public sealed record MergeEventRecord(string AdvisoryKey, string EventType, DateTimeOffset CreatedAt);
public sealed record MergeEventRecord(
Guid Id,
string AdvisoryKey,
byte[] BeforeHash,
byte[] AfterHash,
DateTimeOffset MergedAt,
IReadOnlyList<Guid> InputDocumentIds,
IReadOnlyList<MergeFieldDecision> FieldDecisions);
public sealed record MergeFieldDecision(
string Field,
string? SelectedSource,
string DecisionReason,
DateTimeOffset? SelectedModified,
IReadOnlyList<string> ConsideredSources);
public interface IMergeEventStore
{
Task AppendAsync(MergeEventRecord record, CancellationToken cancellationToken);
Task<IReadOnlyList<MergeEventRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken);
}
public sealed class InMemoryMergeEventStore : IMergeEventStore
{
private readonly ConcurrentBag<MergeEventRecord> _records = new();
public Task AppendAsync(MergeEventRecord record, CancellationToken cancellationToken)
{
_records.Add(record);
return Task.CompletedTask;
}
public Task<IReadOnlyList<MergeEventRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken)
{
var records = _records
.Where(r => string.Equals(r.AdvisoryKey, advisoryKey, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(r => r.MergedAt)
.Take(limit)
.ToArray();
return Task.FromResult<IReadOnlyList<MergeEventRecord>>(records);
}
}
}
namespace StellaOps.Concelier.Storage.Mongo
{
public static class MongoStorageDefaults
{
public static class Collections
{
public const string AdvisoryStatements = "advisory_statements";
public const string AdvisoryRaw = "advisory_raw";
}
}
// Already defined above; kept for backward compatibility with legacy using directives.
}

View File

@@ -50,7 +50,7 @@ public sealed class PostgresDocumentStore : IDocumentStore
MetadataJson: record.Metadata is null ? null : JsonSerializer.Serialize(record.Metadata, _json),
Etag: record.Etag,
LastModified: record.LastModified,
Payload: Array.Empty<byte>(), // payload handled via RawDocumentStorage; keep pointer zero-length here
Payload: record.Payload ?? Array.Empty<byte>(),
CreatedAt: record.CreatedAt,
UpdatedAt: DateTimeOffset.UtcNow,
ExpiresAt: record.ExpiresAt);
@@ -82,7 +82,8 @@ public sealed class PostgresDocumentStore : IDocumentStore
: JsonSerializer.Deserialize<Dictionary<string, string>>(row.MetadataJson, _json),
row.Etag,
row.LastModified,
PayloadId: null,
ExpiresAt: row.ExpiresAt);
PayloadId: row.Id,
ExpiresAt: row.ExpiresAt,
Payload: row.Payload);
}
}

View File

@@ -1,8 +1,10 @@
using System.Text.Json;
using Dapper;
using Microsoft.Extensions.Logging;
using StellaOps.Concelier.Storage.Postgres.Models;
using StellaOps.Infrastructure.Postgres;
using StellaOps.Infrastructure.Postgres.Connections;
using StellaOps.Infrastructure.Postgres.Repositories;
namespace StellaOps.Concelier.Storage.Postgres.Repositories;

View File

@@ -8,6 +8,10 @@
<EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Scanner.Analyzers.Lang.DotNet.Tests" />
</ItemGroup>
<ItemGroup>
<Compile Include="**\\*.cs" Exclude="obj\\**;bin\\**" />
<EmbeddedResource Include="**\\*.json" Exclude="obj\\**;bin\\**" />

View File

@@ -101,7 +101,7 @@ public sealed class GlobalJsonParserTests
var result = GlobalJsonParser.Parse(content);
Assert.Equal(GlobalJsonParser.Empty, result);
Assert.Equal(GlobalJsonResult.Empty, result);
}
[Fact]
@@ -111,7 +111,7 @@ public sealed class GlobalJsonParserTests
var result = GlobalJsonParser.Parse(content);
Assert.Equal(GlobalJsonParser.Empty, result);
Assert.Equal(GlobalJsonResult.Empty, result);
}
[Fact]
@@ -120,7 +120,7 @@ public sealed class GlobalJsonParserTests
var cancellationToken = TestContext.Current.CancellationToken;
var result = await GlobalJsonParser.ParseAsync("/nonexistent/global.json", cancellationToken);
Assert.Equal(GlobalJsonParser.Empty, result);
Assert.Equal(GlobalJsonResult.Empty, result);
}
[Fact]

View File

@@ -346,7 +346,7 @@ public sealed class NuGetConfigParserTests
var result = NuGetConfigParser.Parse(content);
Assert.Equal(NuGetConfigParser.Empty, result);
Assert.Equal(NuGetConfigResult.Empty, result);
}
[Fact]

View File

@@ -42,19 +42,19 @@ public sealed class PackagesConfigParserTests
}
[Fact]
public void ParsesAllowedVersions()
public void ParsesCondition()
{
var content = """
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" allowedVersions="[13.0,14.0)" />
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" />
</packages>
""";
var result = PackagesConfigParser.Parse(content);
Assert.Single(result.Packages);
Assert.Equal("[13.0,14.0)", result.Packages[0].AllowedVersions);
Assert.Equal("Newtonsoft.Json", result.Packages[0].PackageId);
}
[Fact]
@@ -100,7 +100,7 @@ public sealed class PackagesConfigParserTests
var result = PackagesConfigParser.Parse(content);
Assert.Equal(PackagesConfigParser.Empty, result);
Assert.Equal(PackagesConfigResult.Empty, result);
}
[Fact]
@@ -110,7 +110,7 @@ public sealed class PackagesConfigParserTests
var result = PackagesConfigParser.Parse(content);
Assert.Equal(PackagesConfigParser.Empty, result);
Assert.Equal(PackagesConfigResult.Empty, result);
}
[Fact]
@@ -119,7 +119,7 @@ public sealed class PackagesConfigParserTests
var cancellationToken = TestContext.Current.CancellationToken;
var result = await PackagesConfigParser.ParseAsync("/nonexistent/packages.config", cancellationToken);
Assert.Equal(PackagesConfigParser.Empty, result);
Assert.Equal(PackagesConfigResult.Empty, result);
}
[Fact]

View File

@@ -35,6 +35,11 @@
</PackageReference>
</ItemGroup>
<!-- Global using directives for test framework -->
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Analyzers.Lang.DotNet\StellaOps.Scanner.Analyzers.Lang.DotNet.csproj" />
</ItemGroup>

View File

@@ -155,7 +155,7 @@ internal static class DotNetFixtureBuilder
sb.AppendLine("{");
sb.AppendLine(""" "version": 1,""");
sb.AppendLine(""" "dependencies": {""");
sb.AppendLine($""" "{targetFramework}": {{""");
sb.AppendLine($" \"{targetFramework}\": {{");
for (var i = 0; i < packages.Length; i++)
{
@@ -163,10 +163,10 @@ internal static class DotNetFixtureBuilder
var type = isDirect ? "Direct" : "Transitive";
var comma = i < packages.Length - 1 ? "," : "";
sb.AppendLine($""" "{packageId}": {{""");
sb.AppendLine($""" "type": "{type}",""");
sb.AppendLine($""" "resolved": "{version}",""");
sb.AppendLine($""" "contentHash": "sha512-test{i}==""");
sb.AppendLine($" \"{packageId}\": {{");
sb.AppendLine($" \"type\": \"{type}\",");
sb.AppendLine($" \"resolved\": \"{version}\",");
sb.AppendLine($" \"contentHash\": \"sha512-test{i}==\"");
sb.AppendLine($" }}{comma}");
}
@@ -216,18 +216,18 @@ internal static class DotNetFixtureBuilder
var sb = new StringBuilder();
sb.AppendLine("{");
sb.AppendLine(""" "sdk": {""");
sb.Append($""" "version": "{sdkVersion}"""");
sb.Append($" \"version\": \"{sdkVersion}\"");
if (!string.IsNullOrEmpty(rollForward))
{
sb.AppendLine(",");
sb.Append($""" "rollForward": "{rollForward}"""");
sb.Append($" \"rollForward\": \"{rollForward}\"");
}
if (allowPrerelease.HasValue)
{
sb.AppendLine(",");
sb.Append($""" "allowPrerelease": {allowPrerelease.Value.ToString().ToLowerInvariant()}""");
sb.Append($" \"allowPrerelease\": {allowPrerelease.Value.ToString().ToLowerInvariant()}");
}
sb.AppendLine();
@@ -319,22 +319,25 @@ internal static class DotNetFixtureBuilder
var bundleSignature = ".net core bundle"u8.ToArray();
// Create a file with MZ header and bundle markers
// Must be > 100KB (detector minimum) and put signature in last 64KB
var content = new byte[1024 * 200]; // 200KB
content[0] = 0x4D; // 'M'
content[1] = 0x5A; // 'Z'
// Add bundle signature
Array.Copy(bundleSignature, 0, content, 500, bundleSignature.Length);
// Add bundle signature in the LAST 64KB (detector searches there)
// Position it near the end of the file
var signaturePosition = content.Length - (32 * 1024); // 32KB from end
Array.Copy(bundleSignature, 0, content, signaturePosition, bundleSignature.Length);
// Add some System. namespace patterns
// Add some System. namespace patterns in the last 64KB
var systemPattern = "System.Runtime"u8.ToArray();
Array.Copy(systemPattern, 0, content, 1000, systemPattern.Length);
Array.Copy(systemPattern, 0, content, signaturePosition + 100, systemPattern.Length);
// Add .dll patterns
// Add .dll patterns in the last 64KB for embedded pattern detection
var dllPattern = ".dll"u8.ToArray();
for (var i = 0; i < 15; i++)
{
Array.Copy(dllPattern, 0, content, 2000 + i * 100, dllPattern.Length);
Array.Copy(dllPattern, 0, content, signaturePosition + 200 + i * 100, dllPattern.Length);
}
var filePath = Path.Combine(directory, bundleName);