Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -42,6 +42,7 @@ internal static class CommandFactory
|
||||
root.Add(BuildRubyCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildPhpCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildPythonCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildBunCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildDatabaseCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildSourcesCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildAocCommand(services, verboseOption, cancellationToken));
|
||||
@@ -72,10 +73,10 @@ internal static class CommandFactory
|
||||
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));
|
||||
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));
|
||||
|
||||
var pluginLogger = loggerFactory.CreateLogger<CliCommandModuleLoader>();
|
||||
var pluginLoader = new CliCommandModuleLoader(services, options, pluginLogger);
|
||||
@@ -370,6 +371,74 @@ internal static class CommandFactory
|
||||
return python;
|
||||
}
|
||||
|
||||
private static Command BuildBunCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var bun = new Command("bun", "Work with Bun analyzer outputs.");
|
||||
|
||||
var inspect = new Command("inspect", "Inspect a local Bun workspace.");
|
||||
var inspectRootOption = new Option<string?>("--root")
|
||||
{
|
||||
Description = "Path to the Bun workspace (defaults to current directory)."
|
||||
};
|
||||
var inspectFormatOption = new Option<string?>("--format")
|
||||
{
|
||||
Description = "Output format (table or json)."
|
||||
};
|
||||
|
||||
inspect.Add(inspectRootOption);
|
||||
inspect.Add(inspectFormatOption);
|
||||
inspect.SetAction((parseResult, _) =>
|
||||
{
|
||||
var root = parseResult.GetValue(inspectRootOption);
|
||||
var format = parseResult.GetValue(inspectFormatOption) ?? "table";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleBunInspectAsync(
|
||||
services,
|
||||
root,
|
||||
format,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
var resolve = new Command("resolve", "Fetch Bun packages for a completed scan.");
|
||||
var resolveImageOption = new Option<string?>("--image")
|
||||
{
|
||||
Description = "Image reference (digest or tag) used by the scan."
|
||||
};
|
||||
var resolveScanIdOption = new Option<string?>("--scan-id")
|
||||
{
|
||||
Description = "Explicit scan identifier."
|
||||
};
|
||||
var resolveFormatOption = new Option<string?>("--format")
|
||||
{
|
||||
Description = "Output format (table or json)."
|
||||
};
|
||||
|
||||
resolve.Add(resolveImageOption);
|
||||
resolve.Add(resolveScanIdOption);
|
||||
resolve.Add(resolveFormatOption);
|
||||
resolve.SetAction((parseResult, _) =>
|
||||
{
|
||||
var image = parseResult.GetValue(resolveImageOption);
|
||||
var scanId = parseResult.GetValue(resolveScanIdOption);
|
||||
var format = parseResult.GetValue(resolveFormatOption) ?? "table";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleBunResolveAsync(
|
||||
services,
|
||||
image,
|
||||
scanId,
|
||||
format,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
bun.Add(inspect);
|
||||
bun.Add(resolve);
|
||||
return bun;
|
||||
}
|
||||
|
||||
private static Command BuildKmsCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var kms = new Command("kms", "Manage file-backed signing keys.");
|
||||
|
||||
@@ -29,6 +29,7 @@ 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;
|
||||
@@ -40,6 +41,7 @@ using StellaOps.Scanner.Analyzers.Lang.Node;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Ruby;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Php;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Bun;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.PolicyDsl;
|
||||
|
||||
@@ -8327,6 +8329,191 @@ internal static class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleBunInspectAsync(
|
||||
IServiceProvider services,
|
||||
string? rootPath,
|
||||
string format,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("bun-inspect");
|
||||
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
|
||||
var previousLevel = verbosity.MinimumLevel;
|
||||
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
|
||||
|
||||
using var activity = CliActivitySource.Instance.StartActivity("cli.bun.inspect", ActivityKind.Internal);
|
||||
activity?.SetTag("stellaops.cli.command", "bun inspect");
|
||||
using var duration = CliMetrics.MeasureCommandDuration("bun inspect");
|
||||
|
||||
var outcome = "unknown";
|
||||
try
|
||||
{
|
||||
var normalizedFormat = string.IsNullOrWhiteSpace(format)
|
||||
? "table"
|
||||
: format.Trim().ToLowerInvariant();
|
||||
if (normalizedFormat is not ("table" or "json"))
|
||||
{
|
||||
throw new InvalidOperationException("Format must be either 'table' or 'json'.");
|
||||
}
|
||||
|
||||
var targetRoot = string.IsNullOrWhiteSpace(rootPath)
|
||||
? Directory.GetCurrentDirectory()
|
||||
: Path.GetFullPath(rootPath);
|
||||
if (!Directory.Exists(targetRoot))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"Directory '{targetRoot}' was not found.");
|
||||
}
|
||||
|
||||
logger.LogInformation("Inspecting Bun workspace in {Root}.", targetRoot);
|
||||
activity?.SetTag("stellaops.cli.bun.root", targetRoot);
|
||||
|
||||
var engine = new LanguageAnalyzerEngine(new ILanguageAnalyzer[] { new BunLanguageAnalyzer() });
|
||||
var context = new LanguageAnalyzerContext(targetRoot, TimeProvider.System);
|
||||
var result = await engine.AnalyzeAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
var report = BunInspectReport.Create(result.ToSnapshots());
|
||||
|
||||
activity?.SetTag("stellaops.cli.bun.package_count", report.Packages.Count);
|
||||
|
||||
if (string.Equals(normalizedFormat, "json", StringComparison.Ordinal))
|
||||
{
|
||||
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
Console.WriteLine(JsonSerializer.Serialize(report, options));
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderBunInspectReport(report);
|
||||
}
|
||||
|
||||
outcome = report.Packages.Count == 0 ? "empty" : "ok";
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
catch (DirectoryNotFoundException ex)
|
||||
{
|
||||
outcome = "not_found";
|
||||
logger.LogError(ex.Message);
|
||||
Environment.ExitCode = 71;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
outcome = "invalid";
|
||||
logger.LogError(ex.Message);
|
||||
Environment.ExitCode = 64;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
outcome = "error";
|
||||
logger.LogError(ex, "Bun inspect failed.");
|
||||
Environment.ExitCode = 70;
|
||||
}
|
||||
finally
|
||||
{
|
||||
verbosity.MinimumLevel = previousLevel;
|
||||
CliMetrics.RecordBunInspect(outcome);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleBunResolveAsync(
|
||||
IServiceProvider services,
|
||||
string? imageReference,
|
||||
string? scanId,
|
||||
string format,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("bun-resolve");
|
||||
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
|
||||
var previousLevel = verbosity.MinimumLevel;
|
||||
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
|
||||
|
||||
using var activity = CliActivitySource.Instance.StartActivity("cli.bun.resolve", ActivityKind.Client);
|
||||
activity?.SetTag("stellaops.cli.command", "bun resolve");
|
||||
using var duration = CliMetrics.MeasureCommandDuration("bun resolve");
|
||||
|
||||
var outcome = "unknown";
|
||||
try
|
||||
{
|
||||
var normalizedFormat = string.IsNullOrWhiteSpace(format)
|
||||
? "table"
|
||||
: format.Trim().ToLowerInvariant();
|
||||
if (normalizedFormat is not ("table" or "json"))
|
||||
{
|
||||
throw new InvalidOperationException("Format must be either 'table' or 'json'.");
|
||||
}
|
||||
|
||||
var identifier = !string.IsNullOrWhiteSpace(scanId)
|
||||
? scanId!.Trim()
|
||||
: imageReference?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
throw new InvalidOperationException("An --image or --scan-id value is required.");
|
||||
}
|
||||
|
||||
logger.LogInformation("Resolving Bun packages for scan {ScanId}.", identifier);
|
||||
activity?.SetTag("stellaops.cli.scan_id", identifier);
|
||||
|
||||
var inventory = await client.GetBunPackagesAsync(identifier, cancellationToken).ConfigureAwait(false);
|
||||
if (inventory is null)
|
||||
{
|
||||
outcome = "empty";
|
||||
Environment.ExitCode = 0;
|
||||
AnsiConsole.MarkupLine("[yellow]Bun package inventory is not available for scan {0}.[/]", Markup.Escape(identifier));
|
||||
return;
|
||||
}
|
||||
|
||||
var report = BunResolveReport.Create(inventory);
|
||||
|
||||
if (!report.HasPackages)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[yellow]No Bun packages found for scan {0}.[/]", Markup.Escape(identifier));
|
||||
}
|
||||
else if (string.Equals(normalizedFormat, "json", StringComparison.Ordinal))
|
||||
{
|
||||
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
Console.WriteLine(JsonSerializer.Serialize(report, options));
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderBunResolveReport(report);
|
||||
}
|
||||
|
||||
outcome = report.HasPackages ? "ok" : "empty";
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
outcome = "invalid";
|
||||
logger.LogError(ex.Message);
|
||||
Environment.ExitCode = 64;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
outcome = "network_error";
|
||||
logger.LogError(ex, "Failed to resolve Bun packages.");
|
||||
Environment.ExitCode = 69;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
outcome = "error";
|
||||
logger.LogError(ex, "Bun resolve failed.");
|
||||
Environment.ExitCode = 70;
|
||||
}
|
||||
finally
|
||||
{
|
||||
verbosity.MinimumLevel = previousLevel;
|
||||
CliMetrics.RecordBunResolve(outcome);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RenderPythonInspectReport(IReadOnlyList<LanguageComponentSnapshot> snapshots)
|
||||
{
|
||||
if (snapshots.Count == 0)
|
||||
@@ -8384,6 +8571,64 @@ internal static class CommandHandlers
|
||||
AnsiConsole.Write(table);
|
||||
}
|
||||
|
||||
private static void RenderBunInspectReport(BunInspectReport report)
|
||||
{
|
||||
if (!report.Packages.Any())
|
||||
{
|
||||
AnsiConsole.MarkupLine("[yellow]No Bun packages detected.[/]");
|
||||
return;
|
||||
}
|
||||
|
||||
var table = new Table().Border(TableBorder.Rounded);
|
||||
table.AddColumn("Package");
|
||||
table.AddColumn("Version");
|
||||
table.AddColumn("Source");
|
||||
table.AddColumn("Dev");
|
||||
table.AddColumn("Direct");
|
||||
|
||||
foreach (var entry in report.Packages)
|
||||
{
|
||||
var dev = entry.IsDev ? "[grey]yes[/]" : "-";
|
||||
var direct = entry.IsDirect ? "[blue]yes[/]" : "-";
|
||||
table.AddRow(
|
||||
Markup.Escape(entry.Name),
|
||||
Markup.Escape(entry.Version ?? "-"),
|
||||
Markup.Escape(entry.Source ?? "-"),
|
||||
dev,
|
||||
direct);
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
AnsiConsole.MarkupLine($"[grey]Total packages: {report.Packages.Count}[/]");
|
||||
}
|
||||
|
||||
private static void RenderBunResolveReport(BunResolveReport report)
|
||||
{
|
||||
if (!report.HasPackages)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[yellow]No Bun packages found.[/]");
|
||||
return;
|
||||
}
|
||||
|
||||
var table = new Table().Border(TableBorder.Rounded);
|
||||
table.AddColumn("Package");
|
||||
table.AddColumn("Version");
|
||||
table.AddColumn("Source");
|
||||
table.AddColumn("Integrity");
|
||||
|
||||
foreach (var entry in report.Packages)
|
||||
{
|
||||
table.AddRow(
|
||||
Markup.Escape(entry.Name),
|
||||
Markup.Escape(entry.Version ?? "-"),
|
||||
Markup.Escape(entry.Source ?? "-"),
|
||||
Markup.Escape(entry.Integrity ?? "-"));
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
AnsiConsole.MarkupLine($"[grey]Scan: {Markup.Escape(report.ScanId ?? "-")} • Total: {report.Packages.Count}[/]");
|
||||
}
|
||||
|
||||
private static void RenderRubyInspectReport(RubyInspectReport report)
|
||||
{
|
||||
if (!report.Packages.Any())
|
||||
@@ -8999,6 +9244,163 @@ internal static class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BunInspectReport
|
||||
{
|
||||
[JsonPropertyName("packages")]
|
||||
public IReadOnlyList<BunInspectEntry> Packages { get; }
|
||||
|
||||
private BunInspectReport(IReadOnlyList<BunInspectEntry> packages)
|
||||
{
|
||||
Packages = packages;
|
||||
}
|
||||
|
||||
public static BunInspectReport Create(IEnumerable<LanguageComponentSnapshot>? snapshots)
|
||||
{
|
||||
var source = snapshots?.ToArray() ?? Array.Empty<LanguageComponentSnapshot>();
|
||||
|
||||
var entries = source
|
||||
.Where(static snapshot => string.Equals(snapshot.Type, "npm", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(BunInspectEntry.FromSnapshot)
|
||||
.OrderBy(static entry => entry.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(static entry => entry.Version ?? string.Empty, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
return new BunInspectReport(entries);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record BunInspectEntry(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("version")] string? Version,
|
||||
[property: JsonPropertyName("source")] string? Source,
|
||||
[property: JsonPropertyName("isDev")] bool IsDev,
|
||||
[property: JsonPropertyName("isDirect")] bool IsDirect,
|
||||
[property: JsonPropertyName("resolved")] string? Resolved,
|
||||
[property: JsonPropertyName("integrity")] string? Integrity)
|
||||
{
|
||||
public static BunInspectEntry FromSnapshot(LanguageComponentSnapshot snapshot)
|
||||
{
|
||||
var metadata = BunMetadataHelpers.Clone(snapshot.Metadata);
|
||||
var source = BunMetadataHelpers.GetString(metadata, "source");
|
||||
var isDev = BunMetadataHelpers.GetBool(metadata, "dev") ?? false;
|
||||
var isDirect = BunMetadataHelpers.GetBool(metadata, "direct") ?? false;
|
||||
var resolved = BunMetadataHelpers.GetString(metadata, "resolved");
|
||||
var integrity = BunMetadataHelpers.GetString(metadata, "integrity");
|
||||
|
||||
return new BunInspectEntry(
|
||||
snapshot.Name ?? "-",
|
||||
snapshot.Version,
|
||||
source,
|
||||
isDev,
|
||||
isDirect,
|
||||
resolved,
|
||||
integrity);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BunMetadataHelpers
|
||||
{
|
||||
public static IDictionary<string, string?> Clone(IDictionary<string, string?>? metadata)
|
||||
{
|
||||
if (metadata is null || metadata.Count == 0)
|
||||
{
|
||||
return new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var clone = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var pair in metadata)
|
||||
{
|
||||
clone[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public static string? GetString(IDictionary<string, string?> metadata, string key)
|
||||
{
|
||||
if (metadata.TryGetValue(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
foreach (var pair in metadata)
|
||||
{
|
||||
if (string.Equals(pair.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool? GetBool(IDictionary<string, string?> metadata, string key)
|
||||
{
|
||||
var value = GetString(metadata, key);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bool.TryParse(value, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BunResolveReport
|
||||
{
|
||||
[JsonPropertyName("scanId")]
|
||||
public string? ScanId { get; }
|
||||
|
||||
[JsonPropertyName("packages")]
|
||||
public IReadOnlyList<BunResolveEntry> Packages { get; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasPackages => Packages.Count > 0;
|
||||
|
||||
private BunResolveReport(string? scanId, IReadOnlyList<BunResolveEntry> packages)
|
||||
{
|
||||
ScanId = scanId;
|
||||
Packages = packages;
|
||||
}
|
||||
|
||||
public static BunResolveReport Create(BunPackageInventory? inventory)
|
||||
{
|
||||
if (inventory is null)
|
||||
{
|
||||
return new BunResolveReport(null, Array.Empty<BunResolveEntry>());
|
||||
}
|
||||
|
||||
var entries = inventory.Packages
|
||||
.Select(BunResolveEntry.FromPackage)
|
||||
.OrderBy(static entry => entry.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(static entry => entry.Version ?? string.Empty, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
return new BunResolveReport(inventory.ScanId, entries);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record BunResolveEntry(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("version")] string? Version,
|
||||
[property: JsonPropertyName("source")] string? Source,
|
||||
[property: JsonPropertyName("integrity")] string? Integrity)
|
||||
{
|
||||
public static BunResolveEntry FromPackage(BunPackageItem package)
|
||||
{
|
||||
return new BunResolveEntry(
|
||||
package.Name,
|
||||
package.Version,
|
||||
package.Source,
|
||||
package.Integrity);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record LockValidationEntry(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("version")] string? Version,
|
||||
|
||||
@@ -20,6 +20,7 @@ using StellaOps.Auth.Client;
|
||||
using StellaOps.Cli.Configuration;
|
||||
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.Services.Models.Transport;
|
||||
|
||||
@@ -960,6 +961,50 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<BunPackageInventory?> GetBunPackagesAsync(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}/bun-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<BunPackageInventory>(SerializerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (inventory is null)
|
||||
{
|
||||
throw new InvalidOperationException("Bun package response payload was empty.");
|
||||
}
|
||||
|
||||
var normalizedScanId = string.IsNullOrWhiteSpace(inventory.ScanId) ? scanId : inventory.ScanId;
|
||||
var packages = inventory.Packages ?? Array.Empty<BunPackageItem>();
|
||||
|
||||
return inventory with
|
||||
{
|
||||
ScanId = normalizedScanId,
|
||||
Packages = packages
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<AdvisoryPipelinePlanResponseModel> CreateAdvisoryPipelinePlanAsync(
|
||||
AdvisoryAiTaskType taskType,
|
||||
AdvisoryPipelinePlanRequestModel request,
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.Cli.Services.Models.AdvisoryAi;
|
||||
using StellaOps.Cli.Services.Models.Bun;
|
||||
using StellaOps.Cli.Services.Models.Ruby;
|
||||
|
||||
namespace StellaOps.Cli.Services;
|
||||
@@ -51,6 +52,8 @@ internal interface IBackendOperationsClient
|
||||
|
||||
Task<RubyPackageInventoryModel?> GetRubyPackagesAsync(string scanId, CancellationToken cancellationToken);
|
||||
|
||||
Task<BunPackageInventory?> GetBunPackagesAsync(string scanId, CancellationToken cancellationToken);
|
||||
|
||||
Task<AdvisoryPipelinePlanResponseModel> CreateAdvisoryPipelinePlanAsync(AdvisoryAiTaskType taskType, AdvisoryPipelinePlanRequestModel request, CancellationToken cancellationToken);
|
||||
|
||||
Task<AdvisoryPipelineOutputModel?> TryGetAdvisoryPipelineOutputAsync(string cacheKey, AdvisoryAiTaskType taskType, string profile, CancellationToken cancellationToken);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Cli.Services.Models.Bun;
|
||||
|
||||
internal sealed record BunPackageItem(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("version")] string? Version,
|
||||
[property: JsonPropertyName("source")] string? Source,
|
||||
[property: JsonPropertyName("resolved")] string? Resolved,
|
||||
[property: JsonPropertyName("integrity")] string? Integrity,
|
||||
[property: JsonPropertyName("isDev")] bool? IsDev,
|
||||
[property: JsonPropertyName("isDirect")] bool? IsDirect,
|
||||
[property: JsonPropertyName("isPatched")] bool? IsPatched,
|
||||
[property: JsonPropertyName("customRegistry")] string? CustomRegistry,
|
||||
[property: JsonPropertyName("metadata")] IDictionary<string, string?>? Metadata);
|
||||
|
||||
internal sealed record BunPackageInventory(
|
||||
[property: JsonPropertyName("scanId")] string ScanId,
|
||||
[property: JsonPropertyName("imageDigest")] string? ImageDigest,
|
||||
[property: JsonPropertyName("generatedAt")] DateTimeOffset? GeneratedAt,
|
||||
[property: JsonPropertyName("packages")] IReadOnlyList<BunPackageItem> Packages);
|
||||
@@ -53,6 +53,7 @@
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Ruby/StellaOps.Scanner.Analyzers.Lang.Ruby.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/StellaOps.Scanner.Analyzers.Lang.Java.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Php/StellaOps.Scanner.Analyzers.Lang.Php.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun/StellaOps.Scanner.Analyzers.Lang.Bun.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Validation/StellaOps.Scanner.Surface.Validation.csproj" />
|
||||
<ProjectReference Include="../../Policy/StellaOps.PolicyDsl/StellaOps.PolicyDsl.csproj" />
|
||||
@@ -61,6 +62,12 @@
|
||||
<ProjectReference Include="../../Attestor/StellaOps.Attestation/StellaOps.Attestation.csproj" />
|
||||
<ProjectReference Include="../../Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj" />
|
||||
<ProjectReference Include="../../Authority/__Libraries/StellaOps.Authority.Storage.Postgres/StellaOps.Authority.Storage.Postgres.csproj" />
|
||||
<ProjectReference Include="../../Scheduler/__Libraries/StellaOps.Scheduler.Storage.Postgres/StellaOps.Scheduler.Storage.Postgres.csproj" />
|
||||
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/StellaOps.Concelier.Storage.Postgres.csproj" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(StellaOpsEnableCryptoPro)' == 'true'">
|
||||
|
||||
@@ -62,6 +62,8 @@ internal static class CliMetrics
|
||||
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 Counter<long> BunInspectCounter = Meter.CreateCounter<long>("stellaops.cli.bun.inspect.count");
|
||||
private static readonly Counter<long> BunResolveCounter = Meter.CreateCounter<long>("stellaops.cli.bun.resolve.count");
|
||||
private static readonly Counter<long> AttestSignCounter = Meter.CreateCounter<long>("stellaops.cli.attest.sign.count");
|
||||
private static readonly Counter<long> AttestVerifyCounter = Meter.CreateCounter<long>("stellaops.cli.attest.verify.count");
|
||||
private static readonly Histogram<double> CommandDurationHistogram = Meter.CreateHistogram<double>("stellaops.cli.command.duration.ms");
|
||||
@@ -153,6 +155,14 @@ internal static class CliMetrics
|
||||
=> PythonInspectCounter.Add(1, WithSealedModeTag(
|
||||
Tag("outcome", string.IsNullOrWhiteSpace(outcome) ? "unknown" : outcome)));
|
||||
|
||||
public static void RecordBunInspect(string outcome)
|
||||
=> BunInspectCounter.Add(1, WithSealedModeTag(
|
||||
Tag("outcome", string.IsNullOrWhiteSpace(outcome) ? "unknown" : outcome)));
|
||||
|
||||
public static void RecordBunResolve(string outcome)
|
||||
=> BunResolveCounter.Add(1, WithSealedModeTag(
|
||||
Tag("outcome", string.IsNullOrWhiteSpace(outcome) ? "unknown" : outcome)));
|
||||
|
||||
/// <summary>
|
||||
/// Records a successful attestation signing operation (CLI-ATTEST-73-001).
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user