sprints completion. new product advisories prepared

This commit is contained in:
master
2026-01-16 16:30:03 +02:00
parent a927d924e3
commit 4ca3ce8fb4
255 changed files with 42434 additions and 1020 deletions

View File

@@ -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
}