sprints completion. new product advisories prepared
This commit is contained in:
@@ -38,10 +38,211 @@ public static class ReachabilityCommandGroup
|
||||
|
||||
reachability.Add(BuildShowCommand(services, verboseOption, cancellationToken));
|
||||
reachability.Add(BuildExportCommand(services, verboseOption, cancellationToken));
|
||||
reachability.Add(BuildTraceExportCommand(services, verboseOption, cancellationToken));
|
||||
|
||||
return reachability;
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_004_CLI_reachability_trace_export (CLI-RT-001)
|
||||
private static Command BuildTraceExportCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scanIdOption = new Option<string>("--scan-id", "-s")
|
||||
{
|
||||
Description = "Scan ID to export traces from",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var outputOption = new Option<string?>("--output", "-o")
|
||||
{
|
||||
Description = "Output file path (default: stdout)"
|
||||
};
|
||||
|
||||
var formatOption = new Option<string>("--format", "-f")
|
||||
{
|
||||
Description = "Export format: json-lines (default), graphson"
|
||||
};
|
||||
formatOption.SetDefaultValue("json-lines");
|
||||
|
||||
var includeRuntimeOption = new Option<bool>("--include-runtime")
|
||||
{
|
||||
Description = "Include runtime evidence (runtimeConfirmed, observationCount)"
|
||||
};
|
||||
includeRuntimeOption.SetDefaultValue(true);
|
||||
|
||||
var minScoreOption = new Option<double?>("--min-score")
|
||||
{
|
||||
Description = "Minimum reachability score filter (0.0-1.0)"
|
||||
};
|
||||
|
||||
var runtimeOnlyOption = new Option<bool>("--runtime-only")
|
||||
{
|
||||
Description = "Only include nodes/edges confirmed at runtime"
|
||||
};
|
||||
|
||||
var serverOption = new Option<string?>("--server")
|
||||
{
|
||||
Description = "Scanner server URL (uses config default if not specified)"
|
||||
};
|
||||
|
||||
var traceExport = new Command("trace", "Export reachability traces with runtime evidence")
|
||||
{
|
||||
scanIdOption,
|
||||
outputOption,
|
||||
formatOption,
|
||||
includeRuntimeOption,
|
||||
minScoreOption,
|
||||
runtimeOnlyOption,
|
||||
serverOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
traceExport.SetAction(async (parseResult, _) =>
|
||||
{
|
||||
var scanId = parseResult.GetValue(scanIdOption) ?? string.Empty;
|
||||
var output = parseResult.GetValue(outputOption);
|
||||
var format = parseResult.GetValue(formatOption) ?? "json-lines";
|
||||
var includeRuntime = parseResult.GetValue(includeRuntimeOption);
|
||||
var minScore = parseResult.GetValue(minScoreOption);
|
||||
var runtimeOnly = parseResult.GetValue(runtimeOnlyOption);
|
||||
var server = parseResult.GetValue(serverOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await HandleTraceExportAsync(
|
||||
services,
|
||||
scanId,
|
||||
output,
|
||||
format,
|
||||
includeRuntime,
|
||||
minScore,
|
||||
runtimeOnly,
|
||||
server,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
return traceExport;
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_004_CLI_reachability_trace_export (CLI-RT-001)
|
||||
private static async Task<int> HandleTraceExportAsync(
|
||||
IServiceProvider services,
|
||||
string scanId,
|
||||
string? outputPath,
|
||||
string format,
|
||||
bool includeRuntime,
|
||||
double? minScore,
|
||||
bool runtimeOnly,
|
||||
string? serverUrl,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
var logger = loggerFactory?.CreateLogger(typeof(ReachabilityCommandGroup));
|
||||
|
||||
try
|
||||
{
|
||||
// Build API URL
|
||||
var baseUrl = serverUrl ?? Environment.GetEnvironmentVariable("STELLA_SCANNER_URL") ?? "http://localhost:5080";
|
||||
var queryParams = new List<string>
|
||||
{
|
||||
$"format={Uri.EscapeDataString(format)}",
|
||||
$"includeRuntimeEvidence={includeRuntime.ToString().ToLowerInvariant()}"
|
||||
};
|
||||
|
||||
if (minScore.HasValue)
|
||||
{
|
||||
queryParams.Add($"minReachabilityScore={minScore.Value:F2}");
|
||||
}
|
||||
|
||||
if (runtimeOnly)
|
||||
{
|
||||
queryParams.Add("runtimeConfirmedOnly=true");
|
||||
}
|
||||
|
||||
var url = $"{baseUrl.TrimEnd('/')}/scans/{Uri.EscapeDataString(scanId)}/reachability/traces/export?{string.Join("&", queryParams)}";
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.Error.WriteLine($"Fetching traces from: {url}");
|
||||
}
|
||||
|
||||
using var httpClient = new System.Net.Http.HttpClient();
|
||||
httpClient.Timeout = TimeSpan.FromMinutes(5);
|
||||
|
||||
var response = await httpClient.GetAsync(url, ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorBody = await response.Content.ReadAsStringAsync(ct);
|
||||
Console.Error.WriteLine($"Error: Server returned {(int)response.StatusCode} {response.ReasonPhrase}");
|
||||
if (!string.IsNullOrWhiteSpace(errorBody))
|
||||
{
|
||||
Console.Error.WriteLine(errorBody);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
// Parse and reformat for determinism
|
||||
var traceExport = JsonSerializer.Deserialize<TraceExportResponse>(content, JsonOptions);
|
||||
|
||||
if (traceExport is null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Failed to parse trace export response");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Output
|
||||
var formattedOutput = JsonSerializer.Serialize(traceExport, JsonOptions);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
await File.WriteAllTextAsync(outputPath, formattedOutput, ct);
|
||||
Console.WriteLine($"Exported traces to: {outputPath}");
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($" Format: {traceExport.Format}");
|
||||
Console.WriteLine($" Nodes: {traceExport.NodeCount}");
|
||||
Console.WriteLine($" Edges: {traceExport.EdgeCount}");
|
||||
Console.WriteLine($" Runtime Coverage: {traceExport.RuntimeCoverage:F1}%");
|
||||
if (traceExport.AverageReachabilityScore.HasValue)
|
||||
{
|
||||
Console.WriteLine($" Avg Reachability Score: {traceExport.AverageReachabilityScore:F2}");
|
||||
}
|
||||
Console.WriteLine($" Content Digest: {traceExport.ContentDigest}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(formattedOutput);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (System.Net.Http.HttpRequestException ex)
|
||||
{
|
||||
logger?.LogError(ex, "Failed to connect to scanner server");
|
||||
Console.Error.WriteLine($"Error: Failed to connect to server: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Request timed out");
|
||||
return 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Trace export command failed unexpectedly");
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static Command BuildShowCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
@@ -782,5 +983,103 @@ public static class ReachabilityCommandGroup
|
||||
public required string Completeness { get; init; }
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_004_CLI_reachability_trace_export
|
||||
// DTOs for trace export endpoint response
|
||||
private sealed record TraceExportResponse
|
||||
{
|
||||
[JsonPropertyName("scanId")]
|
||||
public required string ScanId { get; init; }
|
||||
|
||||
[JsonPropertyName("format")]
|
||||
public required string Format { get; init; }
|
||||
|
||||
[JsonPropertyName("nodeCount")]
|
||||
public int NodeCount { get; init; }
|
||||
|
||||
[JsonPropertyName("edgeCount")]
|
||||
public int EdgeCount { get; init; }
|
||||
|
||||
[JsonPropertyName("runtimeCoverage")]
|
||||
public double RuntimeCoverage { get; init; }
|
||||
|
||||
[JsonPropertyName("averageReachabilityScore")]
|
||||
public double? AverageReachabilityScore { get; init; }
|
||||
|
||||
[JsonPropertyName("contentDigest")]
|
||||
public required string ContentDigest { get; init; }
|
||||
|
||||
[JsonPropertyName("exportedAt")]
|
||||
public DateTimeOffset ExportedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("nodes")]
|
||||
public TraceNodeDto[]? Nodes { get; init; }
|
||||
|
||||
[JsonPropertyName("edges")]
|
||||
public TraceEdgeDto[]? Edges { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TraceNodeDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public required string Id { get; init; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public required string Type { get; init; }
|
||||
|
||||
[JsonPropertyName("symbol")]
|
||||
public string? Symbol { get; init; }
|
||||
|
||||
[JsonPropertyName("file")]
|
||||
public string? File { get; init; }
|
||||
|
||||
[JsonPropertyName("line")]
|
||||
public int? Line { get; init; }
|
||||
|
||||
[JsonPropertyName("purl")]
|
||||
public string? Purl { get; init; }
|
||||
|
||||
[JsonPropertyName("reachabilityScore")]
|
||||
public double? ReachabilityScore { get; init; }
|
||||
|
||||
[JsonPropertyName("runtimeConfirmed")]
|
||||
public bool? RuntimeConfirmed { get; init; }
|
||||
|
||||
[JsonPropertyName("runtimeObservationCount")]
|
||||
public int? RuntimeObservationCount { get; init; }
|
||||
|
||||
[JsonPropertyName("runtimeFirstObserved")]
|
||||
public DateTimeOffset? RuntimeFirstObserved { get; init; }
|
||||
|
||||
[JsonPropertyName("runtimeLastObserved")]
|
||||
public DateTimeOffset? RuntimeLastObserved { get; init; }
|
||||
|
||||
[JsonPropertyName("runtimeEvidenceUri")]
|
||||
public string? RuntimeEvidenceUri { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TraceEdgeDto
|
||||
{
|
||||
[JsonPropertyName("from")]
|
||||
public required string From { get; init; }
|
||||
|
||||
[JsonPropertyName("to")]
|
||||
public required string To { get; init; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; init; }
|
||||
|
||||
[JsonPropertyName("confidence")]
|
||||
public double Confidence { get; init; }
|
||||
|
||||
[JsonPropertyName("reachabilityScore")]
|
||||
public double? ReachabilityScore { get; init; }
|
||||
|
||||
[JsonPropertyName("runtimeConfirmed")]
|
||||
public bool? RuntimeConfirmed { get; init; }
|
||||
|
||||
[JsonPropertyName("runtimeObservationCount")]
|
||||
public int? RuntimeObservationCount { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user