old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -44,6 +44,13 @@ public static class UnknownsCommandGroup
|
||||
unknownsCommand.Add(BuildResolveCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildBudgetCommand(services, verboseOption, cancellationToken));
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001, CLI-UNK-002, CLI-UNK-003)
|
||||
unknownsCommand.Add(BuildSummaryCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildShowCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildProofCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildExportCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildTriageCommand(services, verboseOption, cancellationToken));
|
||||
|
||||
return unknownsCommand;
|
||||
}
|
||||
|
||||
@@ -274,6 +281,194 @@ public static class UnknownsCommandGroup
|
||||
return escalateCommand;
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001)
|
||||
private static Command BuildSummaryCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var formatOption = new Option<string>("--format", new[] { "-f" })
|
||||
{
|
||||
Description = "Output format: table, json"
|
||||
};
|
||||
formatOption.SetDefaultValue("table");
|
||||
|
||||
var summaryCommand = new Command("summary", "Show unknowns summary by band with counts and fingerprints");
|
||||
summaryCommand.Add(formatOption);
|
||||
summaryCommand.Add(verboseOption);
|
||||
|
||||
summaryCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var format = parseResult.GetValue(formatOption) ?? "table";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await HandleSummaryAsync(services, format, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return summaryCommand;
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001)
|
||||
private static Command BuildShowCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var idOption = new Option<string>("--id", new[] { "-i" })
|
||||
{
|
||||
Description = "Unknown ID to show details for",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var formatOption = new Option<string>("--format", new[] { "-f" })
|
||||
{
|
||||
Description = "Output format: table, json"
|
||||
};
|
||||
formatOption.SetDefaultValue("table");
|
||||
|
||||
var showCommand = new Command("show", "Show detailed unknown info including fingerprint, triggers, and next actions");
|
||||
showCommand.Add(idOption);
|
||||
showCommand.Add(formatOption);
|
||||
showCommand.Add(verboseOption);
|
||||
|
||||
showCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var id = parseResult.GetValue(idOption) ?? string.Empty;
|
||||
var format = parseResult.GetValue(formatOption) ?? "table";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await HandleShowAsync(services, id, format, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return showCommand;
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-002)
|
||||
private static Command BuildProofCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var idOption = new Option<string>("--id", new[] { "-i" })
|
||||
{
|
||||
Description = "Unknown ID to get proof for",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var formatOption = new Option<string>("--format", new[] { "-f" })
|
||||
{
|
||||
Description = "Output format: json, envelope"
|
||||
};
|
||||
formatOption.SetDefaultValue("json");
|
||||
|
||||
var proofCommand = new Command("proof", "Get evidence proof for an unknown (fingerprint, triggers, evidence refs)");
|
||||
proofCommand.Add(idOption);
|
||||
proofCommand.Add(formatOption);
|
||||
proofCommand.Add(verboseOption);
|
||||
|
||||
proofCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var id = parseResult.GetValue(idOption) ?? string.Empty;
|
||||
var format = parseResult.GetValue(formatOption) ?? "json";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await HandleProofAsync(services, id, format, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return proofCommand;
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-002)
|
||||
private static Command BuildExportCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var bandOption = new Option<string?>("--band", new[] { "-b" })
|
||||
{
|
||||
Description = "Filter by band: HOT, WARM, COLD, all"
|
||||
};
|
||||
|
||||
var formatOption = new Option<string>("--format", new[] { "-f" })
|
||||
{
|
||||
Description = "Output format: json, csv, ndjson"
|
||||
};
|
||||
formatOption.SetDefaultValue("json");
|
||||
|
||||
var outputOption = new Option<string?>("--output", new[] { "-o" })
|
||||
{
|
||||
Description = "Output file path (default: stdout)"
|
||||
};
|
||||
|
||||
var exportCommand = new Command("export", "Export unknowns with fingerprints and triggers for offline analysis");
|
||||
exportCommand.Add(bandOption);
|
||||
exportCommand.Add(formatOption);
|
||||
exportCommand.Add(outputOption);
|
||||
exportCommand.Add(verboseOption);
|
||||
|
||||
exportCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var band = parseResult.GetValue(bandOption);
|
||||
var format = parseResult.GetValue(formatOption) ?? "json";
|
||||
var output = parseResult.GetValue(outputOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await HandleExportAsync(services, band, format, output, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return exportCommand;
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-003)
|
||||
private static Command BuildTriageCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var idOption = new Option<string>("--id", new[] { "-i" })
|
||||
{
|
||||
Description = "Unknown ID to triage",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var actionOption = new Option<string>("--action", new[] { "-a" })
|
||||
{
|
||||
Description = "Triage action: accept-risk, require-fix, defer, escalate, dispute",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var reasonOption = new Option<string>("--reason", new[] { "-r" })
|
||||
{
|
||||
Description = "Reason for triage decision",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var durationOption = new Option<int?>("--duration-days", new[] { "-d" })
|
||||
{
|
||||
Description = "Duration in days for defer/accept-risk actions"
|
||||
};
|
||||
|
||||
var triageCommand = new Command("triage", "Apply manual triage decision to an unknown (grey queue adjudication)");
|
||||
triageCommand.Add(idOption);
|
||||
triageCommand.Add(actionOption);
|
||||
triageCommand.Add(reasonOption);
|
||||
triageCommand.Add(durationOption);
|
||||
triageCommand.Add(verboseOption);
|
||||
|
||||
triageCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var id = parseResult.GetValue(idOption) ?? string.Empty;
|
||||
var action = parseResult.GetValue(actionOption) ?? string.Empty;
|
||||
var reason = parseResult.GetValue(reasonOption) ?? string.Empty;
|
||||
var duration = parseResult.GetValue(durationOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await HandleTriageAsync(services, id, action, reason, duration, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return triageCommand;
|
||||
}
|
||||
|
||||
private static Command BuildResolveCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
@@ -558,6 +753,452 @@ public static class UnknownsCommandGroup
|
||||
}
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001)
|
||||
private static async Task<int> HandleSummaryAsync(
|
||||
IServiceProvider services,
|
||||
string format,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
|
||||
if (httpClientFactory is null)
|
||||
{
|
||||
logger?.LogError("HTTP client factory not available");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (verbose)
|
||||
{
|
||||
logger?.LogDebug("Fetching unknowns summary");
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var response = await client.GetAsync("/api/v1/policy/unknowns/summary", ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Error: Failed to fetch summary ({response.StatusCode})");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var summary = await response.Content.ReadFromJsonAsync<UnknownsSummaryResponse>(JsonOptions, ct);
|
||||
if (summary is null)
|
||||
{
|
||||
Console.WriteLine("Error: Empty response from server");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (format == "json")
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(summary, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Unknowns Summary");
|
||||
Console.WriteLine("================");
|
||||
Console.WriteLine($" HOT: {summary.Hot,6}");
|
||||
Console.WriteLine($" WARM: {summary.Warm,6}");
|
||||
Console.WriteLine($" COLD: {summary.Cold,6}");
|
||||
Console.WriteLine($" Resolved: {summary.Resolved,6}");
|
||||
Console.WriteLine($" ----------------");
|
||||
Console.WriteLine($" Total: {summary.Total,6}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Summary failed unexpectedly");
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001)
|
||||
private static async Task<int> HandleShowAsync(
|
||||
IServiceProvider services,
|
||||
string id,
|
||||
string format,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
|
||||
if (httpClientFactory is null)
|
||||
{
|
||||
logger?.LogError("HTTP client factory not available");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (verbose)
|
||||
{
|
||||
logger?.LogDebug("Fetching unknown {Id}", id);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var response = await client.GetAsync($"/api/v1/policy/unknowns/{id}", ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Error: Unknown not found ({response.StatusCode})");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownDetailResponse>(JsonOptions, ct);
|
||||
if (result?.Unknown is null)
|
||||
{
|
||||
Console.WriteLine("Error: Empty response from server");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var unknown = result.Unknown;
|
||||
|
||||
if (format == "json")
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(unknown, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Unknown: {unknown.Id}");
|
||||
Console.WriteLine(new string('=', 60));
|
||||
Console.WriteLine($" Package: {unknown.PackageId}@{unknown.PackageVersion}");
|
||||
Console.WriteLine($" Band: {unknown.Band}");
|
||||
Console.WriteLine($" Score: {unknown.Score:F2}");
|
||||
Console.WriteLine($" Reason: {unknown.ReasonCode} ({unknown.ReasonCodeShort})");
|
||||
Console.WriteLine($" First Seen: {unknown.FirstSeenAt:u}");
|
||||
Console.WriteLine($" Last Evaluated: {unknown.LastEvaluatedAt:u}");
|
||||
|
||||
if (!string.IsNullOrEmpty(unknown.FingerprintId))
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Fingerprint");
|
||||
Console.WriteLine($" ID: {unknown.FingerprintId}");
|
||||
}
|
||||
|
||||
if (unknown.Triggers?.Count > 0)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Triggers");
|
||||
foreach (var trigger in unknown.Triggers)
|
||||
{
|
||||
Console.WriteLine($" - {trigger.EventType}@{trigger.EventVersion} ({trigger.ReceivedAt:u})");
|
||||
}
|
||||
}
|
||||
|
||||
if (unknown.NextActions?.Count > 0)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Next Actions");
|
||||
foreach (var action in unknown.NextActions)
|
||||
{
|
||||
Console.WriteLine($" - {action}");
|
||||
}
|
||||
}
|
||||
|
||||
if (unknown.ConflictInfo?.HasConflict == true)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Conflicts");
|
||||
Console.WriteLine($" Severity: {unknown.ConflictInfo.Severity:F2}");
|
||||
Console.WriteLine($" Suggested Path: {unknown.ConflictInfo.SuggestedPath}");
|
||||
foreach (var conflict in unknown.ConflictInfo.Conflicts)
|
||||
{
|
||||
Console.WriteLine($" - {conflict.Type}: {conflict.Signal1} vs {conflict.Signal2}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(unknown.RemediationHint))
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Hint: {unknown.RemediationHint}");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Show failed unexpectedly");
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-002)
|
||||
private static async Task<int> HandleProofAsync(
|
||||
IServiceProvider services,
|
||||
string id,
|
||||
string format,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
|
||||
if (httpClientFactory is null)
|
||||
{
|
||||
logger?.LogError("HTTP client factory not available");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (verbose)
|
||||
{
|
||||
logger?.LogDebug("Fetching proof for unknown {Id}", id);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var response = await client.GetAsync($"/api/v1/policy/unknowns/{id}", ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Error: Unknown not found ({response.StatusCode})");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownDetailResponse>(JsonOptions, ct);
|
||||
if (result?.Unknown is null)
|
||||
{
|
||||
Console.WriteLine("Error: Empty response from server");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var unknown = result.Unknown;
|
||||
|
||||
// Build proof object with deterministic ordering
|
||||
var proof = new UnknownProof
|
||||
{
|
||||
Id = unknown.Id,
|
||||
FingerprintId = unknown.FingerprintId,
|
||||
PackageId = unknown.PackageId,
|
||||
PackageVersion = unknown.PackageVersion,
|
||||
Band = unknown.Band,
|
||||
Score = unknown.Score,
|
||||
ReasonCode = unknown.ReasonCode,
|
||||
Triggers = unknown.Triggers?.OrderBy(t => t.ReceivedAt).ToList() ?? [],
|
||||
EvidenceRefs = unknown.EvidenceRefs?.OrderBy(e => e.Type).ThenBy(e => e.Uri).ToList() ?? [],
|
||||
ObservationState = unknown.ObservationState,
|
||||
ConflictInfo = unknown.ConflictInfo
|
||||
};
|
||||
|
||||
Console.WriteLine(JsonSerializer.Serialize(proof, JsonOptions));
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Proof failed unexpectedly");
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-002)
|
||||
private static async Task<int> HandleExportAsync(
|
||||
IServiceProvider services,
|
||||
string? band,
|
||||
string format,
|
||||
string? outputPath,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
|
||||
if (httpClientFactory is null)
|
||||
{
|
||||
logger?.LogError("HTTP client factory not available");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (verbose)
|
||||
{
|
||||
logger?.LogDebug("Exporting unknowns: band={Band}, format={Format}", band ?? "all", format);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var url = string.IsNullOrEmpty(band) || band == "all"
|
||||
? "/api/v1/policy/unknowns?limit=10000"
|
||||
: $"/api/v1/policy/unknowns?band={band}&limit=10000";
|
||||
|
||||
var response = await client.GetAsync(url, ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Error: Failed to fetch unknowns ({response.StatusCode})");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsListResponse>(JsonOptions, ct);
|
||||
if (result?.Items is null)
|
||||
{
|
||||
Console.WriteLine("Error: Empty response from server");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Deterministic ordering by band priority, then score descending
|
||||
var sorted = result.Items
|
||||
.OrderBy(u => u.Band switch { "hot" => 0, "warm" => 1, "cold" => 2, _ => 3 })
|
||||
.ThenByDescending(u => u.Score)
|
||||
.ToList();
|
||||
|
||||
TextWriter writer = outputPath is not null
|
||||
? new StreamWriter(outputPath)
|
||||
: Console.Out;
|
||||
|
||||
try
|
||||
{
|
||||
switch (format.ToLowerInvariant())
|
||||
{
|
||||
case "csv":
|
||||
await WriteCsvAsync(writer, sorted);
|
||||
break;
|
||||
case "ndjson":
|
||||
foreach (var item in sorted)
|
||||
{
|
||||
await writer.WriteLineAsync(JsonSerializer.Serialize(item, JsonOptions));
|
||||
}
|
||||
break;
|
||||
case "json":
|
||||
default:
|
||||
await writer.WriteLineAsync(JsonSerializer.Serialize(sorted, JsonOptions));
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (outputPath is not null)
|
||||
{
|
||||
await writer.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
if (verbose && outputPath is not null)
|
||||
{
|
||||
Console.WriteLine($"Exported {sorted.Count} unknowns to {outputPath}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Export failed unexpectedly");
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WriteCsvAsync(TextWriter writer, IReadOnlyList<UnknownDto> items)
|
||||
{
|
||||
// CSV header
|
||||
await writer.WriteLineAsync("id,package_id,package_version,band,score,reason_code,fingerprint_id,first_seen_at,last_evaluated_at");
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
await writer.WriteLineAsync(string.Format(
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
"{0},{1},{2},{3},{4:F2},{5},{6},{7:u},{8:u}",
|
||||
item.Id,
|
||||
EscapeCsv(item.PackageId),
|
||||
EscapeCsv(item.PackageVersion),
|
||||
item.Band,
|
||||
item.Score,
|
||||
item.ReasonCode,
|
||||
item.FingerprintId ?? "",
|
||||
item.FirstSeenAt,
|
||||
item.LastEvaluatedAt));
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
{
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-003)
|
||||
private static async Task<int> HandleTriageAsync(
|
||||
IServiceProvider services,
|
||||
string id,
|
||||
string action,
|
||||
string reason,
|
||||
int? durationDays,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
|
||||
if (httpClientFactory is null)
|
||||
{
|
||||
logger?.LogError("HTTP client factory not available");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Validate action
|
||||
var validActions = new[] { "accept-risk", "require-fix", "defer", "escalate", "dispute" };
|
||||
if (!validActions.Contains(action.ToLowerInvariant()))
|
||||
{
|
||||
Console.WriteLine($"Error: Invalid action '{action}'. Valid actions: {string.Join(", ", validActions)}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (verbose)
|
||||
{
|
||||
logger?.LogDebug("Triaging unknown {Id} with action {Action}", id, action);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var request = new TriageRequest(action, reason, durationDays);
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
$"/api/v1/policy/unknowns/{id}/triage",
|
||||
request,
|
||||
JsonOptions,
|
||||
ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(ct);
|
||||
logger?.LogError("Triage failed: {Status}", response.StatusCode);
|
||||
Console.WriteLine($"Error: Triage failed ({response.StatusCode})");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Unknown {id} triaged with action '{action}'.");
|
||||
if (durationDays.HasValue)
|
||||
{
|
||||
Console.WriteLine($"Duration: {durationDays} days");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Triage failed unexpectedly");
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle budget check command.
|
||||
/// Sprint: SPRINT_5100_0004_0001 Task T1
|
||||
@@ -927,5 +1568,102 @@ public static class UnknownsCommandGroup
|
||||
public IReadOnlyDictionary<string, int>? ByReasonCode { get; init; }
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001, CLI-UNK-002, CLI-UNK-003)
|
||||
private sealed record UnknownsSummaryResponse
|
||||
{
|
||||
public int Hot { get; init; }
|
||||
public int Warm { get; init; }
|
||||
public int Cold { get; init; }
|
||||
public int Resolved { get; init; }
|
||||
public int Total { get; init; }
|
||||
}
|
||||
|
||||
private sealed record UnknownDetailResponse
|
||||
{
|
||||
public UnknownDto? Unknown { get; init; }
|
||||
}
|
||||
|
||||
private sealed record UnknownsListResponse
|
||||
{
|
||||
public IReadOnlyList<UnknownDto>? Items { get; init; }
|
||||
public int TotalCount { get; init; }
|
||||
}
|
||||
|
||||
private sealed record UnknownDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string PackageId { get; init; } = string.Empty;
|
||||
public string PackageVersion { get; init; } = string.Empty;
|
||||
public string Band { get; init; } = string.Empty;
|
||||
public decimal Score { get; init; }
|
||||
public decimal UncertaintyFactor { get; init; }
|
||||
public decimal ExploitPressure { get; init; }
|
||||
public DateTimeOffset FirstSeenAt { get; init; }
|
||||
public DateTimeOffset LastEvaluatedAt { get; init; }
|
||||
public string? ResolutionReason { get; init; }
|
||||
public DateTimeOffset? ResolvedAt { get; init; }
|
||||
public string ReasonCode { get; init; } = string.Empty;
|
||||
public string ReasonCodeShort { get; init; } = string.Empty;
|
||||
public string? RemediationHint { get; init; }
|
||||
public string? DetailedHint { get; init; }
|
||||
public string? AutomationCommand { get; init; }
|
||||
public IReadOnlyList<EvidenceRefDto>? EvidenceRefs { get; init; }
|
||||
public string? FingerprintId { get; init; }
|
||||
public IReadOnlyList<TriggerDto>? Triggers { get; init; }
|
||||
public IReadOnlyList<string>? NextActions { get; init; }
|
||||
public ConflictInfoDto? ConflictInfo { get; init; }
|
||||
public string? ObservationState { get; init; }
|
||||
}
|
||||
|
||||
private sealed record EvidenceRefDto
|
||||
{
|
||||
public string Type { get; init; } = string.Empty;
|
||||
public string Uri { get; init; } = string.Empty;
|
||||
public string? Digest { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TriggerDto
|
||||
{
|
||||
public string EventType { get; init; } = string.Empty;
|
||||
public int EventVersion { get; init; }
|
||||
public string? Source { get; init; }
|
||||
public DateTimeOffset ReceivedAt { get; init; }
|
||||
public string? CorrelationId { get; init; }
|
||||
}
|
||||
|
||||
private sealed record ConflictInfoDto
|
||||
{
|
||||
public bool HasConflict { get; init; }
|
||||
public double Severity { get; init; }
|
||||
public string SuggestedPath { get; init; } = string.Empty;
|
||||
public IReadOnlyList<ConflictDetailDto> Conflicts { get; init; } = [];
|
||||
}
|
||||
|
||||
private sealed record ConflictDetailDto
|
||||
{
|
||||
public string Signal1 { get; init; } = string.Empty;
|
||||
public string Signal2 { get; init; } = string.Empty;
|
||||
public string Type { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public double Severity { get; init; }
|
||||
}
|
||||
|
||||
private sealed record UnknownProof
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string? FingerprintId { get; init; }
|
||||
public string PackageId { get; init; } = string.Empty;
|
||||
public string PackageVersion { get; init; } = string.Empty;
|
||||
public string Band { get; init; } = string.Empty;
|
||||
public decimal Score { get; init; }
|
||||
public string ReasonCode { get; init; } = string.Empty;
|
||||
public IReadOnlyList<TriggerDto> Triggers { get; init; } = [];
|
||||
public IReadOnlyList<EvidenceRefDto> EvidenceRefs { get; init; } = [];
|
||||
public string? ObservationState { get; init; }
|
||||
public ConflictInfoDto? ConflictInfo { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TriageRequest(string Action, string Reason, int? DurationDays);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user